diff --git a/graphql/index.tsx b/graphql/index.tsx index d28d7928d..56dfa13d4 100644 --- a/graphql/index.tsx +++ b/graphql/index.tsx @@ -165,6 +165,7 @@ export type Mutation = { logout?: Maybe rejectSubmission?: Maybe removeAlert?: Maybe + removeExercise: Exercise removeExerciseFlag: Exercise reqPwReset: SuccessResponse setStar: SuccessResponse @@ -297,6 +298,10 @@ export type MutationRemoveAlertArgs = { id: Scalars['Int'] } +export type MutationRemoveExerciseArgs = { + id: Scalars['Int'] +} + export type MutationRemoveExerciseFlagArgs = { id: Scalars['Int'] } @@ -1284,6 +1289,15 @@ export type RemoveAlertMutation = { } | null } +export type RemoveExerciseMutationVariables = Exact<{ + id: Scalars['Int'] +}> + +export type RemoveExerciseMutation = { + __typename?: 'Mutation' + removeExercise: { __typename?: 'Exercise'; id: number } +} + export type RemoveExerciseFlagMutationVariables = Exact<{ id: Scalars['Int'] }> @@ -2006,6 +2020,12 @@ export type MutationResolvers< ContextType, RequireFields > + removeExercise?: Resolver< + ResolversTypes['Exercise'], + ParentType, + ContextType, + RequireFields + > removeExerciseFlag?: Resolver< ResolversTypes['Exercise'], ParentType, @@ -5469,6 +5489,87 @@ export type RemoveAlertMutationOptions = Apollo.BaseMutationOptions< RemoveAlertMutation, RemoveAlertMutationVariables > +export const RemoveExerciseDocument = gql` + mutation removeExercise($id: Int!) { + removeExercise(id: $id) { + id + } + } +` +export type RemoveExerciseMutationFn = Apollo.MutationFunction< + RemoveExerciseMutation, + RemoveExerciseMutationVariables +> +export type RemoveExerciseProps< + TChildProps = {}, + TDataName extends string = 'mutate' +> = { + [key in TDataName]: Apollo.MutationFunction< + RemoveExerciseMutation, + RemoveExerciseMutationVariables + > +} & TChildProps +export function withRemoveExercise< + TProps, + TChildProps = {}, + TDataName extends string = 'mutate' +>( + operationOptions?: ApolloReactHoc.OperationOption< + TProps, + RemoveExerciseMutation, + RemoveExerciseMutationVariables, + RemoveExerciseProps + > +) { + return ApolloReactHoc.withMutation< + TProps, + RemoveExerciseMutation, + RemoveExerciseMutationVariables, + RemoveExerciseProps + >(RemoveExerciseDocument, { + alias: 'removeExercise', + ...operationOptions + }) +} + +/** + * __useRemoveExerciseMutation__ + * + * To run a mutation, you first call `useRemoveExerciseMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRemoveExerciseMutation` 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 [removeExerciseMutation, { data, loading, error }] = useRemoveExerciseMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useRemoveExerciseMutation( + baseOptions?: Apollo.MutationHookOptions< + RemoveExerciseMutation, + RemoveExerciseMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useMutation< + RemoveExerciseMutation, + RemoveExerciseMutationVariables + >(RemoveExerciseDocument, options) +} +export type RemoveExerciseMutationHookResult = ReturnType< + typeof useRemoveExerciseMutation +> +export type RemoveExerciseMutationResult = + Apollo.MutationResult +export type RemoveExerciseMutationOptions = Apollo.BaseMutationOptions< + RemoveExerciseMutation, + RemoveExerciseMutationVariables +> export const RemoveExerciseFlagDocument = gql` mutation removeExerciseFlag($id: Int!) { removeExerciseFlag(id: $id) { @@ -6801,6 +6902,7 @@ export type MutationKeySpecifier = ( | 'logout' | 'rejectSubmission' | 'removeAlert' + | 'removeExercise' | 'removeExerciseFlag' | 'reqPwReset' | 'setStar' @@ -6836,6 +6938,7 @@ export type MutationFieldPolicy = { logout?: FieldPolicy | FieldReadFunction rejectSubmission?: FieldPolicy | FieldReadFunction removeAlert?: FieldPolicy | FieldReadFunction + removeExercise?: FieldPolicy | FieldReadFunction removeExerciseFlag?: FieldPolicy | FieldReadFunction reqPwReset?: FieldPolicy | FieldReadFunction setStar?: FieldPolicy | FieldReadFunction diff --git a/graphql/queries/removeExercise.ts b/graphql/queries/removeExercise.ts new file mode 100644 index 000000000..757efcf9f --- /dev/null +++ b/graphql/queries/removeExercise.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client' + +const REMOVE_EXERCISE = gql` + mutation removeExercise($id: Int!) { + removeExercise(id: $id) { + id + } + } +` + +export default REMOVE_EXERCISE diff --git a/graphql/resolvers.ts b/graphql/resolvers.ts index 8e0905dd5..037b665db 100644 --- a/graphql/resolvers.ts +++ b/graphql/resolvers.ts @@ -38,7 +38,8 @@ import { updateExercise, deleteExercise, flagExercise, - removeExerciseFlag + removeExerciseFlag, + removeExercise } from './resolvers/exerciseCrud' import { exerciseSubmissions, @@ -99,6 +100,7 @@ export default { deleteComment, flagExercise, removeExerciseFlag, + removeExercise, unlinkDiscord, addExerciseComment, updateUserNames, diff --git a/graphql/resolvers/exerciseCrud.test.js b/graphql/resolvers/exerciseCrud.test.js index 079577212..4cbfab875 100644 --- a/graphql/resolvers/exerciseCrud.test.js +++ b/graphql/resolvers/exerciseCrud.test.js @@ -8,7 +8,8 @@ import { addExercise, updateExercise, flagExercise, - removeExerciseFlag + removeExerciseFlag, + removeExercise } from './exerciseCrud' const AdminCtx = { @@ -128,7 +129,7 @@ describe('It should update an exercise', () => { }) }) describe('It should test delete', () => { - test('it should delete module', () => { + test('it should delete exercise', () => { prismaMock.exercise.delete.mockResolvedValue({ success: true }) expect(deleteExercise({}, { id: 1 }, AdminCtx)).resolves.toEqual({ success: true @@ -147,7 +148,7 @@ describe('It should test delete', () => { ) ).rejects.toThrowError() }) - test('It should check if user can delte their own exercise', () => { + test('It should check if user can delete their own exercise', () => { expect( deleteExercise( {}, @@ -316,3 +317,70 @@ describe('It should test remove flag', () => { ).rejects.toThrow(new Error('Not authorized to unflag')) }) }) + +describe('It should test set removed flag', () => { + test('it should set exercise removed flag', () => { + prismaMock.exercise.update.mockResolvedValue({ success: true }) + prismaMock.exercise.findUnique.mockResolvedValueOnce({ + ...mockExercises[0] + }) + expect(removeExercise({}, { id: 1 }, AdminCtx)).resolves.toEqual({ + success: true + }) + }) + + test('it should throw error if the exercise is already removed', () => { + prismaMock.exercise.update.mockResolvedValueOnce({ success: true }) + prismaMock.exercise.findUnique.mockResolvedValueOnce({ + ...mockExercises[0], + removedAt: '26/08/2023 08:22:24' + }) + expect(removeExercise({}, { id: 1 }, AdminCtx)).rejects.toThrow( + new Error('Exercise is already removed') + ) + }) + + test('it should throw error if the exercise is not found', () => { + prismaMock.exercise.update.mockResolvedValueOnce({ success: true }) + prismaMock.exercise.findUnique.mockResolvedValueOnce(null) + expect(removeExercise({}, { id: 1 }, AdminCtx)).rejects.toThrow( + new Error('Exercise is not found') + ) + }) + + test('should check id when setting exercise removed flag', () => { + expect( + removeExercise( + {}, + { + id: 1 + }, + { + req: { user: {} } + } + ) + ).rejects.toThrowError() + }) + + test('It should check if user can set the removed flag of their own exercise', () => { + prismaMock.exercise.findUnique.mockResolvedValueOnce({ + ...mockExercises[0] + }) + + expect( + removeExercise( + {}, + { + content: 'testing', + name: 'Using functions to make pie', + lessonId: 1, + testable: false, + authorId: 2 + }, + { + req: { user: { id: 333 } } + } + ) + ).rejects.toThrow(new Error('Not authorized to remove')) + }) +}) diff --git a/graphql/resolvers/exerciseCrud.ts b/graphql/resolvers/exerciseCrud.ts index 4d85b7a0d..5fb8fa015 100644 --- a/graphql/resolvers/exerciseCrud.ts +++ b/graphql/resolvers/exerciseCrud.ts @@ -4,7 +4,8 @@ import { MutationUpdateExerciseArgs, MutationDeleteExerciseArgs, MutationFlagExerciseArgs, - MutationRemoveExerciseFlagArgs + MutationRemoveExerciseFlagArgs, + MutationRemoveExerciseArgs } from '..' import { Context } from '../../@types/helpers' import type { Exercise } from '@prisma/client' @@ -102,6 +103,46 @@ export const deleteExercise = withUserContainer< }) }) +export const removeExercise = withUserContainer< + Promise, + MutationRemoveExerciseArgs +>(async (_parent: void, args: MutationRemoveExerciseArgs, ctx: Context) => { + const { req } = ctx + const { id } = args + + const exercise = await prisma.exercise.findUnique({ + where: { + id + } + }) + + if (!exercise) { + throw new Error('Exercise is not found') + } + + const authorId = req.user?.id + + if (!isAdmin(req) && exercise.authorId !== authorId) { + throw new Error('Not authorized to remove') + } + + if (exercise.removedAt) { + throw new Error('Exercise is already removed') + } + + return prisma.exercise.update({ + where: { id }, + data: { + removedAt: new Date(), + removedById: authorId as number + }, + include: { + author: true, + module: true + } + }) +}) + export const flagExercise = withUserContainer< Promise, MutationFlagExerciseArgs diff --git a/graphql/typeDefs.ts b/graphql/typeDefs.ts index c7a07c463..453106381 100644 --- a/graphql/typeDefs.ts +++ b/graphql/typeDefs.ts @@ -81,6 +81,7 @@ export default gql` testStr: String explanation: String ): Exercise! + removeExercise(id: Int!): Exercise! updateExercise( id: Int! moduleId: Int!