diff --git a/graphql/index.tsx b/graphql/index.tsx index 561226857..26b0400ae 100644 --- a/graphql/index.tsx +++ b/graphql/index.tsx @@ -87,6 +87,22 @@ export type Exercise = { testStr?: Maybe } +export type ExerciseComment = { + __typename?: 'ExerciseComment' + author: User + authorId: Scalars['Int'] + content: Scalars['String'] + createdAt: Scalars['String'] + exercise: Exercise + exerciseId: Scalars['Int'] + id: Scalars['Int'] + parent?: Maybe + parentId?: Maybe + replies?: Maybe>> + updatedAt?: Maybe + userPic?: Maybe +} + export type ExerciseSubmission = { __typename?: 'ExerciseSubmission' exerciseId: Scalars['Int'] @@ -128,6 +144,7 @@ export type Mutation = { addAlert?: Maybe>> addComment?: Maybe addExercise: Exercise + addExerciseComment: ExerciseComment addExerciseSubmission: ExerciseSubmission addModule: Module changeAdminRights?: Maybe @@ -183,6 +200,13 @@ export type MutationAddExerciseArgs = { testStr?: InputMaybe } +export type MutationAddExerciseCommentArgs = { + content: Scalars['String'] + exerciseId: Scalars['Int'] + parentId?: InputMaybe + userPic?: InputMaybe +} + export type MutationAddExerciseSubmissionArgs = { exerciseId: Scalars['Int'] userAnswer: Scalars['String'] @@ -448,6 +472,57 @@ export type UserLesson = { userId?: Maybe } +export type LessonAndChallengeInfoFragment = { + __typename?: 'Lesson' + id: number + docUrl?: string | null + githubUrl?: string | null + videoUrl?: string | null + chatUrl?: string | null + order: number + description: string + title: string + challenges: Array<{ + __typename?: 'Challenge' + id: number + description: string + lessonId: number + title: string + order: number + }> +} + +export type SubmissionsInfoFragment = { + __typename?: 'Submission' + id: number + status: SubmissionStatus + diff?: string | null + comment?: string | null + challengeId: number + lessonId: number + createdAt?: string | null + updatedAt: string + challenge: { __typename?: 'Challenge'; title: string; description: string } + user: { __typename?: 'User'; id: number; username: string } + reviewer?: { + __typename?: 'User' + id: number + username: string + name: string + } | null + comments?: Array<{ + __typename?: 'Comment' + id: number + content: string + submissionId: number + createdAt: string + authorId: number + line?: number | null + fileName?: string | null + author?: { __typename?: 'User'; username: string; name: string } | null + }> | null +} + export type AcceptSubmissionMutationVariables = Exact<{ submissionId: Scalars['Int'] comment: Scalars['String'] @@ -728,57 +803,6 @@ export type FlagExerciseMutation = { flagExercise?: { __typename?: 'Exercise'; id: number } | null } -export type LessonAndChallengeInfoFragment = { - __typename?: 'Lesson' - id: number - docUrl?: string | null - githubUrl?: string | null - videoUrl?: string | null - chatUrl?: string | null - order: number - description: string - title: string - challenges: Array<{ - __typename?: 'Challenge' - id: number - description: string - lessonId: number - title: string - order: number - }> -} - -export type SubmissionsInfoFragment = { - __typename?: 'Submission' - id: number - status: SubmissionStatus - diff?: string | null - comment?: string | null - challengeId: number - lessonId: number - createdAt?: string | null - updatedAt: string - challenge: { __typename?: 'Challenge'; title: string; description: string } - user: { __typename?: 'User'; id: number; username: string } - reviewer?: { - __typename?: 'User' - id: number - username: string - name: string - } | null - comments?: Array<{ - __typename?: 'Comment' - id: number - content: string - submissionId: number - createdAt: string - authorId: number - line?: number | null - fileName?: string | null - author?: { __typename?: 'User'; username: string; name: string } | null - }> | null -} - export type GetAppQueryVariables = Exact<{ [key: string]: never }> export type GetAppQuery = { @@ -1353,6 +1377,27 @@ export type UserInfoQuery = { } | null } +export type AddExerciseCommentMutationVariables = Exact<{ + exerciseId: Scalars['Int'] + content: Scalars['String'] + parentId?: InputMaybe + userPic?: InputMaybe +}> + +export type AddExerciseCommentMutation = { + __typename?: 'Mutation' + addExerciseComment: { + __typename?: 'ExerciseComment' + id: number + exerciseId: number + authorId: number + content: string + userPic?: string | null + createdAt: string + parentId?: number | null + } +} + export type WithIndex = TObject & Record export type ResolversObject = WithIndex @@ -1469,6 +1514,7 @@ export type ResolversTypes = ResolversObject<{ Challenge: ResolverTypeWrapper Comment: ResolverTypeWrapper Exercise: ResolverTypeWrapper + ExerciseComment: ResolverTypeWrapper ExerciseSubmission: ResolverTypeWrapper Int: ResolverTypeWrapper Lesson: ResolverTypeWrapper @@ -1494,6 +1540,7 @@ export type ResolversParentTypes = ResolversObject<{ Challenge: Challenge Comment: Comment Exercise: Exercise + ExerciseComment: ExerciseComment ExerciseSubmission: ExerciseSubmission Int: Scalars['Int'] Lesson: Lesson @@ -1595,6 +1642,33 @@ export type ExerciseResolvers< __isTypeOf?: IsTypeOfResolverFn }> +export type ExerciseCommentResolvers< + ContextType = Context, + ParentType extends ResolversParentTypes['ExerciseComment'] = ResolversParentTypes['ExerciseComment'] +> = ResolversObject<{ + author?: Resolver + authorId?: Resolver + content?: Resolver + createdAt?: Resolver + exercise?: Resolver + exerciseId?: Resolver + id?: Resolver + parent?: Resolver< + Maybe, + ParentType, + ContextType + > + parentId?: Resolver, ParentType, ContextType> + replies?: Resolver< + Maybe>>, + ParentType, + ContextType + > + updatedAt?: Resolver, ParentType, ContextType> + userPic?: Resolver, ParentType, ContextType> + __isTypeOf?: IsTypeOfResolverFn +}> + export type ExerciseSubmissionResolvers< ContextType = Context, ParentType extends ResolversParentTypes['ExerciseSubmission'] = ResolversParentTypes['ExerciseSubmission'] @@ -1682,6 +1756,12 @@ export type MutationResolvers< 'answer' | 'description' | 'moduleId' > > + addExerciseComment?: Resolver< + ResolversTypes['ExerciseComment'], + ParentType, + ContextType, + RequireFields + > addExerciseSubmission?: Resolver< ResolversTypes['ExerciseSubmission'], ParentType, @@ -2056,6 +2136,7 @@ export type Resolvers = ResolversObject<{ Challenge?: ChallengeResolvers Comment?: CommentResolvers Exercise?: ExerciseResolvers + ExerciseComment?: ExerciseCommentResolvers ExerciseSubmission?: ExerciseSubmissionResolvers Lesson?: LessonResolvers Module?: ModuleResolvers @@ -5768,6 +5849,106 @@ export type UserInfoQueryResult = Apollo.QueryResult< UserInfoQuery, UserInfoQueryVariables > +export const AddExerciseCommentDocument = gql` + mutation AddExerciseComment( + $exerciseId: Int! + $content: String! + $parentId: Int + $userPic: String + ) { + addExerciseComment( + exerciseId: $exerciseId + content: $content + parentId: $parentId + userPic: $userPic + ) { + id + exerciseId + authorId + content + userPic + createdAt + parentId + } + } +` +export type AddExerciseCommentMutationFn = Apollo.MutationFunction< + AddExerciseCommentMutation, + AddExerciseCommentMutationVariables +> +export type AddExerciseCommentProps< + TChildProps = {}, + TDataName extends string = 'mutate' +> = { + [key in TDataName]: Apollo.MutationFunction< + AddExerciseCommentMutation, + AddExerciseCommentMutationVariables + > +} & TChildProps +export function withAddExerciseComment< + TProps, + TChildProps = {}, + TDataName extends string = 'mutate' +>( + operationOptions?: ApolloReactHoc.OperationOption< + TProps, + AddExerciseCommentMutation, + AddExerciseCommentMutationVariables, + AddExerciseCommentProps + > +) { + return ApolloReactHoc.withMutation< + TProps, + AddExerciseCommentMutation, + AddExerciseCommentMutationVariables, + AddExerciseCommentProps + >(AddExerciseCommentDocument, { + alias: 'addExerciseComment', + ...operationOptions + }) +} + +/** + * __useAddExerciseCommentMutation__ + * + * To run a mutation, you first call `useAddExerciseCommentMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAddExerciseCommentMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [addExerciseCommentMutation, { data, loading, error }] = useAddExerciseCommentMutation({ + * variables: { + * exerciseId: // value for 'exerciseId' + * content: // value for 'content' + * parentId: // value for 'parentId' + * userPic: // value for 'userPic' + * }, + * }); + */ +export function useAddExerciseCommentMutation( + baseOptions?: Apollo.MutationHookOptions< + AddExerciseCommentMutation, + AddExerciseCommentMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useMutation< + AddExerciseCommentMutation, + AddExerciseCommentMutationVariables + >(AddExerciseCommentDocument, options) +} +export type AddExerciseCommentMutationHookResult = ReturnType< + typeof useAddExerciseCommentMutation +> +export type AddExerciseCommentMutationResult = + Apollo.MutationResult +export type AddExerciseCommentMutationOptions = Apollo.BaseMutationOptions< + AddExerciseCommentMutation, + AddExerciseCommentMutationVariables +> export type AlertKeySpecifier = ( | 'id' | 'text' @@ -5861,6 +6042,35 @@ export type ExerciseFieldPolicy = { module?: FieldPolicy | FieldReadFunction testStr?: FieldPolicy | FieldReadFunction } +export type ExerciseCommentKeySpecifier = ( + | 'author' + | 'authorId' + | 'content' + | 'createdAt' + | 'exercise' + | 'exerciseId' + | 'id' + | 'parent' + | 'parentId' + | 'replies' + | 'updatedAt' + | 'userPic' + | ExerciseCommentKeySpecifier +)[] +export type ExerciseCommentFieldPolicy = { + author?: FieldPolicy | FieldReadFunction + authorId?: FieldPolicy | FieldReadFunction + content?: FieldPolicy | FieldReadFunction + createdAt?: FieldPolicy | FieldReadFunction + exercise?: FieldPolicy | FieldReadFunction + exerciseId?: FieldPolicy | FieldReadFunction + id?: FieldPolicy | FieldReadFunction + parent?: FieldPolicy | FieldReadFunction + parentId?: FieldPolicy | FieldReadFunction + replies?: FieldPolicy | FieldReadFunction + updatedAt?: FieldPolicy | FieldReadFunction + userPic?: FieldPolicy | FieldReadFunction +} export type ExerciseSubmissionKeySpecifier = ( | 'exerciseId' | 'id' @@ -5927,6 +6137,7 @@ export type MutationKeySpecifier = ( | 'addAlert' | 'addComment' | 'addExercise' + | 'addExerciseComment' | 'addExerciseSubmission' | 'addModule' | 'changeAdminRights' @@ -5959,6 +6170,7 @@ export type MutationFieldPolicy = { addAlert?: FieldPolicy | FieldReadFunction addComment?: FieldPolicy | FieldReadFunction addExercise?: FieldPolicy | FieldReadFunction + addExerciseComment?: FieldPolicy | FieldReadFunction addExerciseSubmission?: FieldPolicy | FieldReadFunction addModule?: FieldPolicy | FieldReadFunction changeAdminRights?: FieldPolicy | FieldReadFunction @@ -6179,6 +6391,13 @@ export type StrictTypedTypePolicies = { | (() => undefined | ExerciseKeySpecifier) fields?: ExerciseFieldPolicy } + ExerciseComment?: Omit & { + keyFields?: + | false + | ExerciseCommentKeySpecifier + | (() => undefined | ExerciseCommentKeySpecifier) + fields?: ExerciseCommentFieldPolicy + } ExerciseSubmission?: Omit & { keyFields?: | false diff --git a/graphql/queries/addExerciseComment.ts b/graphql/queries/addExerciseComment.ts new file mode 100644 index 000000000..0ae08a588 --- /dev/null +++ b/graphql/queries/addExerciseComment.ts @@ -0,0 +1,26 @@ +import { gql } from '@apollo/client' + +const ADD_EXERCISE_COMMENT = gql` + mutation AddExerciseComment( + $exerciseId: Int! + $content: String! + $parentId: Int + $userPic: String + ) { + addExerciseComment( + exerciseId: $exerciseId + content: $content + parentId: $parentId + userPic: $userPic + ) { + id + exerciseId + authorId + content + userPic + createdAt + parentId + } + } +` +export default ADD_EXERCISE_COMMENT diff --git a/graphql/resolvers.ts b/graphql/resolvers.ts index f0deb4119..ca734d73b 100644 --- a/graphql/resolvers.ts +++ b/graphql/resolvers.ts @@ -44,6 +44,7 @@ import { exerciseSubmissions, addExerciseSubmission } from './resolvers/exerciseSubmissionCrud' +import { addExerciseComment } from './resolvers/exerciseCommentCrud' export default { Query: { @@ -91,6 +92,7 @@ export default { deleteComment, flagExercise, removeExerciseFlag, - unlinkDiscord + unlinkDiscord, + addExerciseComment } } diff --git a/graphql/resolvers/exerciseCommentCrud.test.js b/graphql/resolvers/exerciseCommentCrud.test.js new file mode 100644 index 000000000..fe40075a2 --- /dev/null +++ b/graphql/resolvers/exerciseCommentCrud.test.js @@ -0,0 +1,50 @@ +import prismaMock from '../../__tests__/utils/prismaMock' +import { addExerciseComment } from './exerciseCommentCrud' + +describe('addExerciseComment resolver tests', () => { + test('Should throw error if user is invalid or not loggedin', async () => { + const mockContext = { req: { user: null } } + const mockArgs = { + exerciseId: 1, + content: 'no user', + parentId: 1, + userPic: undefined + } + + await expect( + addExerciseComment(undefined, mockArgs, mockContext) + ).rejects.toEqual(new Error('User should be logged in')) + }) + + test('Should create a new exerciseComment in prisma', async () => { + const mockContext = { req: { user: { id: 1 } } } + const mockArgs = { + exerciseId: 1, + content: 'there is user', + parentId: null, + userPic: null + } + const mockExerciseComment = { + id: 1, + exerciseId: 1, + authorId: 1, + content: 'there is user', + userPic: null + } + + prismaMock.exerciseComment.create.mockResolvedValue(mockExerciseComment) + await expect( + addExerciseComment(undefined, mockArgs, mockContext) + ).resolves.toEqual(mockExerciseComment) + + expect(prismaMock.exerciseComment.create).toBeCalledWith({ + data: { + authorId: 1, + content: 'there is user', + exerciseId: 1, + parentId: null, + userPic: null + } + }) + }) +}) diff --git a/graphql/resolvers/exerciseCommentCrud.ts b/graphql/resolvers/exerciseCommentCrud.ts new file mode 100644 index 000000000..616625410 --- /dev/null +++ b/graphql/resolvers/exerciseCommentCrud.ts @@ -0,0 +1,16 @@ +import { MutationAddExerciseCommentArgs } from '..' +import { Context } from '../../@types/helpers' +import prisma from '../../prisma' + +export const addExerciseComment = async ( + _parent: void, + { content, exerciseId, parentId, userPic }: MutationAddExerciseCommentArgs, + context: Context +) => { + const authorId = context.req.user?.id + if (!authorId) throw new Error('User should be logged in') + + return prisma.exerciseComment.create({ + data: { authorId, content, exerciseId, parentId, userPic } + }) +} diff --git a/graphql/typeDefs.ts b/graphql/typeDefs.ts index 945217650..1796213f0 100644 --- a/graphql/typeDefs.ts +++ b/graphql/typeDefs.ts @@ -94,6 +94,12 @@ export default gql` exerciseId: Int! userAnswer: String! ): ExerciseSubmission! + addExerciseComment( + exerciseId: Int! + content: String! + parentId: Int + userPic: String + ): ExerciseComment! createLesson( description: String! docUrl: String @@ -280,4 +286,19 @@ export default gql` exerciseId: Int! userAnswer: String! } + + type ExerciseComment { + id: Int! + createdAt: String! + updatedAt: String + authorId: Int! + exerciseId: Int! + userPic: String + content: String! + parentId: Int + exercise: Exercise! + author: User! + parent: ExerciseComment + replies: [ExerciseComment] + } `