Add comments to Prisma Schema and GraphQL server (#162)

* Lowercase all relations in prisma/graphql schema

* Add Comments data model and graphql schema

* Make comments availalble on the api through resolvers and guard them

* Update front graphql schema

* Fix PR
This commit is contained in:
Charles Bochet
2023-05-31 15:41:53 +02:00
committed by GitHub
parent 8bd91139ca
commit a3a3c1924f
311 changed files with 8480 additions and 202 deletions
@@ -0,0 +1,38 @@
import { Resolver, Args, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { PrismaService } from 'src/database/prisma.service';
import { Workspace } from '../@generated/workspace/workspace.model';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { CommentThread } from '../@generated/comment-thread/comment-thread.model';
import { CreateOneCommentThreadArgs } from '../@generated/comment-thread/create-one-comment-thread.args';
import { CreateOneCommentThreadGuard } from './guards/create-one-comment-thread.guard';
@UseGuards(JwtAuthGuard)
@Resolver(() => CommentThread)
export class CommentThreadResolver {
constructor(private readonly prismaService: PrismaService) {}
@UseGuards(CreateOneCommentThreadGuard)
@Mutation(() => CommentThread, {
nullable: false,
})
async createOneCommentThread(
@Args() args: CreateOneCommentThreadArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<CommentThread> {
const newCommentData = args.data.comments?.createMany?.data
? args.data.comments?.createMany?.data?.map((comment) => ({
...comment,
...{ workspaceId: workspace.id },
}))
: [];
return this.prismaService.commentThread.create({
data: {
...args.data,
...{ comments: { createMany: { data: newCommentData } } },
...{ workspace: { connect: { id: workspace.id } } },
},
});
}
}
@@ -0,0 +1,31 @@
import { Resolver, Args, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/guards/jwt.auth.guard';
import { PrismaService } from 'src/database/prisma.service';
import { Workspace } from '../@generated/workspace/workspace.model';
import { AuthWorkspace } from './decorators/auth-workspace.decorator';
import { CreateOneCommentArgs } from '../@generated/comment/create-one-comment.args';
import { Comment } from '../@generated/comment/comment.model';
import { CreateOneCommentGuard } from './guards/create-one-comment.guard';
@UseGuards(JwtAuthGuard)
@Resolver(() => Comment)
export class CommentResolver {
constructor(private readonly prismaService: PrismaService) {}
@UseGuards(CreateOneCommentGuard)
@Mutation(() => Comment, {
nullable: false,
})
async createOneComment(
@Args() args: CreateOneCommentArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<Comment> {
return this.prismaService.comment.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
},
});
}
}
@@ -0,0 +1,104 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class CreateOneCommentThreadGuard implements CanActivate {
constructor(private prismaService: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
const args = gqlContext.getArgs();
const targets = args.data?.commentThreadTargets?.createMany?.data;
const comments = args.data?.comments?.createMany?.data;
const workspaceId = await request.workspace;
if (!targets || targets.length === 0) {
throw new HttpException(
{ reason: 'Missing commentThreadTargets' },
HttpStatus.BAD_REQUEST,
);
}
await targets.map(async (target) => {
if (!target.commentableId || !target.commentableType) {
throw new HttpException(
{
reason:
'Missing commentThreadTarget.commentableId or commentThreadTarget.commentableType',
},
HttpStatus.BAD_REQUEST,
);
}
if (!['Person', 'Company'].includes(target.commentableType)) {
throw new HttpException(
{ reason: 'Invalid commentThreadTarget.commentableType' },
HttpStatus.BAD_REQUEST,
);
}
const targetEntity = await this.prismaService[
target.commentableType
].findUnique({
where: { id: target.commentableId },
});
if (targetEntity.workspaceId !== workspaceId) {
throw new HttpException(
{ reason: 'CommentThreadTarget not found' },
HttpStatus.NOT_FOUND,
);
}
});
if (!comments) {
return true;
}
await comments.map(async (comment) => {
if (!comment.authorId) {
throw new HttpException(
{ reason: 'Missing comment.authorId' },
HttpStatus.BAD_REQUEST,
);
}
const author = await this.prismaService.user.findUnique({
where: { id: comment.authorId },
});
if (!author) {
throw new HttpException(
{ reason: 'Comment.authorId not found' },
HttpStatus.NOT_FOUND,
);
}
const userWorkspaceMember =
await this.prismaService.workspaceMember.findFirst({
where: { userId: author.id },
});
if (
!userWorkspaceMember ||
userWorkspaceMember.workspaceId !== workspaceId
) {
throw new HttpException(
{ reason: 'Comment.authorId not found' },
HttpStatus.NOT_FOUND,
);
}
});
return true;
}
}
@@ -0,0 +1,71 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class CreateOneCommentGuard implements CanActivate {
constructor(private prismaService: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
const args = gqlContext.getArgs();
const authorId = args.data?.author?.connect?.id;
const commentThreadId = args.data?.commentThread?.connect?.id;
if (!authorId || !commentThreadId) {
throw new HttpException(
{ reason: 'Missing author or commentThread' },
HttpStatus.BAD_REQUEST,
);
}
const author = await this.prismaService.user.findUnique({
where: { id: authorId },
});
const commentThread = await this.prismaService.commentThread.findUnique({
where: { id: commentThreadId },
});
if (!author || !commentThread) {
throw new HttpException(
{ reason: 'Author or commentThread not found' },
HttpStatus.NOT_FOUND,
);
}
const userWorkspaceMember =
await this.prismaService.workspaceMember.findFirst({
where: { userId: author.id },
});
if (!userWorkspaceMember) {
throw new HttpException(
{ reason: 'Author or commentThread not found' },
HttpStatus.NOT_FOUND,
);
}
const workspace = await request.workspace;
if (
userWorkspaceMember.workspaceId !== workspace.id ||
commentThread.workspaceId !== workspace.id
) {
throw new HttpException(
{ reason: 'Author or commentThread not found' },
HttpStatus.NOT_FOUND,
);
}
return true;
}
}
@@ -0,0 +1,22 @@
import * as TypeGraphQL from '@nestjs/graphql';
import { CommentThread } from 'src/api/@generated/comment-thread/comment-thread.model';
import { Comment } from 'src/api/@generated/comment/comment.model';
import { PrismaService } from 'src/database/prisma.service';
@TypeGraphQL.Resolver(() => CommentThread)
export class CommentThreadRelationsResolver {
constructor(private readonly prismaService: PrismaService) {}
@TypeGraphQL.ResolveField(() => [Comment], {
nullable: false,
})
async comments(
@TypeGraphQL.Root() commentThread: CommentThread,
): Promise<Comment[]> {
return this.prismaService.comment.findMany({
where: {
commentThreadId: commentThread.id,
},
});
}
}
@@ -1,4 +1,5 @@
import * as TypeGraphQL from '@nestjs/graphql';
import { CommentThread } from 'src/api/@generated/comment-thread/comment-thread.model';
import { Company } from 'src/api/@generated/company/company.model';
import { User } from 'src/api/@generated/user/user.model';
import { Workspace } from 'src/api/@generated/workspace/workspace.model';
@@ -35,4 +36,22 @@ export class CompanyRelationsResolver {
})
.workspace({});
}
@TypeGraphQL.ResolveField(() => [CommentThread], {
nullable: false,
})
async commentThreads(
@TypeGraphQL.Root() company: Company,
): Promise<CommentThread[]> {
return this.prismaService.commentThread.findMany({
where: {
commentThreadTargets: {
some: {
commentableId: company.id,
commentableType: 'Company',
},
},
},
});
}
}
@@ -1,4 +1,5 @@
import * as TypeGraphQL from '@nestjs/graphql';
import { CommentThread } from 'src/api/@generated/comment-thread/comment-thread.model';
import { Company } from 'src/api/@generated/company/company.model';
import { Person } from 'src/api/@generated/person/person.model';
import { Workspace } from 'src/api/@generated/workspace/workspace.model';
@@ -33,4 +34,22 @@ export class PersonRelationsResolver {
})
.workspace({});
}
@TypeGraphQL.ResolveField(() => [CommentThread], {
nullable: false,
})
async commentThreads(
@TypeGraphQL.Root() person: Person,
): Promise<CommentThread[]> {
return await this.prismaService.commentThread.findMany({
where: {
commentThreadTargets: {
some: {
commentableId: person.id,
commentableType: 'Person',
},
},
},
});
}
}
@@ -15,7 +15,7 @@ export class UserRelationsResolver {
@TypeGraphQL.ResolveField(() => WorkspaceMember, {
nullable: true,
})
async WorkspaceMember(
async workspaceMember(
@TypeGraphQL.Parent() user: User,
): Promise<WorkspaceMember | null> {
return await this.prismaService.user
@@ -24,7 +24,7 @@ export class UserRelationsResolver {
id: user.id,
},
})
.WorkspaceMember({});
.workspaceMember({});
}
@TypeGraphQL.ResolveField(() => [Company], {
@@ -49,7 +49,7 @@ export class UserRelationsResolver {
@TypeGraphQL.ResolveField(() => [RefreshToken], {
nullable: false,
})
async RefreshTokens(
async refreshTokens(
@TypeGraphQL.Parent() user: User,
@TypeGraphQL.Info() info: GraphQLResolveInfo,
@TypeGraphQL.Args() args: FindManyRefreshTokenArgs,
@@ -60,7 +60,7 @@ export class UserRelationsResolver {
id: user.id,
},
})
.RefreshTokens({
.refreshTokens({
...args,
});
}