From 0776e59cce525c371b359f4256766e8031633d39 Mon Sep 17 00:00:00 2001 From: Bryan Jennings Date: Thu, 29 Sep 2022 10:53:36 -0700 Subject: [PATCH 1/9] Add exercise submissions to prisma --- .../migration.sql | 15 ++++ prisma/schema.prisma | 69 +++++++++++-------- 2 files changed, 56 insertions(+), 28 deletions(-) create mode 100644 prisma/migrations/20220929035259_add_exercise_submissions/migration.sql diff --git a/prisma/migrations/20220929035259_add_exercise_submissions/migration.sql b/prisma/migrations/20220929035259_add_exercise_submissions/migration.sql new file mode 100644 index 000000000..4204dfbf8 --- /dev/null +++ b/prisma/migrations/20220929035259_add_exercise_submissions/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "exerciseSubmissions" ( + "id" SERIAL NOT NULL, + "exerciseId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "userAnswer" TEXT NOT NULL, + + CONSTRAINT "exerciseSubmissions_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "exerciseSubmissions" ADD CONSTRAINT "exerciseSubmissions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "exerciseSubmissions" ADD CONSTRAINT "exerciseSubmissions_exerciseId_fkey" FOREIGN KEY ("exerciseId") REFERENCES "exercises"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f0d69faf3..87103c790 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -127,33 +127,34 @@ model UserLesson { } model User { - id Int @id @default(autoincrement()) - name String @db.VarChar(255) - username String @db.VarChar(255) - password String? @db.VarChar(255) - email String @db.VarChar(255) + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + username String @db.VarChar(255) + password String? @db.VarChar(255) + email String @db.VarChar(255) gsId Int? isOnline Boolean? - createdAt DateTime @default(now()) @db.Timestamptz(6) - updatedAt DateTime @updatedAt @db.Timestamptz(6) - isAdmin Boolean @default(false) - forgotToken String? @db.VarChar(255) - cliToken String? @db.VarChar(255) - emailVerificationToken String? @db.VarChar(255) - tokenExpiration DateTime? @db.Timestamptz(6) - discordRefreshToken String? @db.VarChar(255) - discordAccessToken String? @db.VarChar(255) - discordAccessTokenExpires DateTime? @db.Timestamptz(6) - discordId String? @db.VarChar(255) - cliVersion String? @db.VarChar(255) + createdAt DateTime @default(now()) @db.Timestamptz(6) + updatedAt DateTime @updatedAt @db.Timestamptz(6) + isAdmin Boolean @default(false) + forgotToken String? @db.VarChar(255) + cliToken String? @db.VarChar(255) + emailVerificationToken String? @db.VarChar(255) + tokenExpiration DateTime? @db.Timestamptz(6) + discordRefreshToken String? @db.VarChar(255) + discordAccessToken String? @db.VarChar(255) + discordAccessTokenExpires DateTime? @db.Timestamptz(6) + discordId String? @db.VarChar(255) + cliVersion String? @db.VarChar(255) comments Comment[] exercises Exercise[] - Exercise Exercise[] @relation("flaggedExercises") + exerciseSubmissions ExerciseSubmission[] + Exercise Exercise[] @relation("flaggedExercises") modules Module[] - starsMentor Star[] @relation("starMentor") - starsGiven Star[] @relation("starStudent") - submissionsReviewed Submission[] @relation("userReviewedSubmissions") - submissions Submission[] @relation("userSubmissions") + starsMentor Star[] @relation("starMentor") + starsGiven Star[] @relation("starStudent") + submissionsReviewed Submission[] @relation("userReviewedSubmissions") + submissions Submission[] @relation("userSubmissions") userLessons UserLesson[] @@map("users") @@ -176,9 +177,9 @@ model Module { } model Exercise { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) @db.Timestamptz(6) - updatedAt DateTime @updatedAt @db.Timestamptz(6) + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @db.Timestamptz(6) + updatedAt DateTime @updatedAt @db.Timestamptz(6) authorId Int moduleId Int description String @@ -188,9 +189,21 @@ model Exercise { flagReason String? flaggedAt DateTime? flaggedById Int? - author User @relation(fields: [authorId], references: [id], onDelete: Cascade) - flaggedBy User? @relation("flaggedExercises", fields: [flaggedById], references: [id]) - module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + flaggedBy User? @relation("flaggedExercises", fields: [flaggedById], references: [id]) + module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade) + submissions ExerciseSubmission[] @@map("exercises") } + +model ExerciseSubmission { + id Int @id @default(autoincrement()) + exerciseId Int + exercise Exercise @relation(fields: [exerciseId], references: [id], onDelete: Cascade) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userAnswer String + + @@map("exerciseSubmissions") +} From df8c9ddc3e0225ce35ce0d58e8687d2189f7fcf8 Mon Sep 17 00:00:00 2001 From: Bryan Jennings Date: Thu, 29 Sep 2022 11:20:07 -0700 Subject: [PATCH 2/9] Add exerciseSubmissions GraphQL resolver which returns all exercise submissions --- graphql/index.tsx | 50 +++++++++++++++++++++ graphql/resolvers.ts | 3 ++ graphql/resolvers/exerciseSubmissionCrud.ts | 10 +++++ graphql/typeDefs.ts | 8 ++++ 4 files changed, 71 insertions(+) create mode 100644 graphql/resolvers/exerciseSubmissionCrud.ts diff --git a/graphql/index.tsx b/graphql/index.tsx index 4225add71..8644da5ac 100644 --- a/graphql/index.tsx +++ b/graphql/index.tsx @@ -87,6 +87,14 @@ export type Exercise = { testStr?: Maybe } +export type ExerciseSubmission = { + __typename?: 'ExerciseSubmission' + exercise: Exercise + id: Scalars['Int'] + user: User + userAnswer: Scalars['String'] +} + export type Lesson = { __typename?: 'Lesson' challenges: Array @@ -315,6 +323,7 @@ export type Query = { __typename?: 'Query' alerts: Array allUsers?: Maybe>> + exerciseSubmissions: Array exercises: Array getLessonMentors?: Maybe>> getPreviousSubmissions?: Maybe> @@ -1399,6 +1408,7 @@ export type ResolversTypes = ResolversObject<{ Challenge: ResolverTypeWrapper Comment: ResolverTypeWrapper Exercise: ResolverTypeWrapper + ExerciseSubmission: ResolverTypeWrapper Int: ResolverTypeWrapper Lesson: ResolverTypeWrapper Module: ResolverTypeWrapper @@ -1423,6 +1433,7 @@ export type ResolversParentTypes = ResolversObject<{ Challenge: Challenge Comment: Comment Exercise: Exercise + ExerciseSubmission: ExerciseSubmission Int: Scalars['Int'] Lesson: Lesson Module: Module @@ -1523,6 +1534,17 @@ export type ExerciseResolvers< __isTypeOf?: IsTypeOfResolverFn }> +export type ExerciseSubmissionResolvers< + ContextType = Context, + ParentType extends ResolversParentTypes['ExerciseSubmission'] = ResolversParentTypes['ExerciseSubmission'] +> = ResolversObject<{ + exercise?: Resolver + id?: Resolver + user?: Resolver + userAnswer?: Resolver + __isTypeOf?: IsTypeOfResolverFn +}> + export type LessonResolvers< ContextType = Context, ParentType extends ResolversParentTypes['Lesson'] = ResolversParentTypes['Lesson'] @@ -1780,6 +1802,11 @@ export type QueryResolvers< ParentType, ContextType > + exerciseSubmissions?: Resolver< + Array, + ParentType, + ContextType + > exercises?: Resolver< Array, ParentType, @@ -1952,6 +1979,7 @@ export type Resolvers = ResolversObject<{ Challenge?: ChallengeResolvers Comment?: CommentResolvers Exercise?: ExerciseResolvers + ExerciseSubmission?: ExerciseSubmissionResolvers Lesson?: LessonResolvers Module?: ModuleResolvers Mutation?: MutationResolvers @@ -5565,6 +5593,19 @@ export type ExerciseFieldPolicy = { module?: FieldPolicy | FieldReadFunction testStr?: FieldPolicy | FieldReadFunction } +export type ExerciseSubmissionKeySpecifier = ( + | 'exercise' + | 'id' + | 'user' + | 'userAnswer' + | ExerciseSubmissionKeySpecifier +)[] +export type ExerciseSubmissionFieldPolicy = { + exercise?: FieldPolicy | FieldReadFunction + id?: FieldPolicy | FieldReadFunction + user?: FieldPolicy | FieldReadFunction + userAnswer?: FieldPolicy | FieldReadFunction +} export type LessonKeySpecifier = ( | 'challenges' | 'chatUrl' @@ -5677,6 +5718,7 @@ export type MutationFieldPolicy = { export type QueryKeySpecifier = ( | 'alerts' | 'allUsers' + | 'exerciseSubmissions' | 'exercises' | 'getLessonMentors' | 'getPreviousSubmissions' @@ -5691,6 +5733,7 @@ export type QueryKeySpecifier = ( export type QueryFieldPolicy = { alerts?: FieldPolicy | FieldReadFunction allUsers?: FieldPolicy | FieldReadFunction + exerciseSubmissions?: FieldPolicy | FieldReadFunction exercises?: FieldPolicy | FieldReadFunction getLessonMentors?: FieldPolicy | FieldReadFunction getPreviousSubmissions?: FieldPolicy | FieldReadFunction @@ -5862,6 +5905,13 @@ export type StrictTypedTypePolicies = { | (() => undefined | ExerciseKeySpecifier) fields?: ExerciseFieldPolicy } + ExerciseSubmission?: Omit & { + keyFields?: + | false + | ExerciseSubmissionKeySpecifier + | (() => undefined | ExerciseSubmissionKeySpecifier) + fields?: ExerciseSubmissionFieldPolicy + } Lesson?: Omit & { keyFields?: | false diff --git a/graphql/resolvers.ts b/graphql/resolvers.ts index f835f2d33..2bcbebdbc 100644 --- a/graphql/resolvers.ts +++ b/graphql/resolvers.ts @@ -39,6 +39,8 @@ import { flagExercise, removeExerciseFlag } from './resolvers/exerciseCrud' +import { exerciseSubmissions } from './resolvers/exerciseSubmissionCrud' + export default { Query: { submissions, @@ -48,6 +50,7 @@ export default { userInfo, lessons, exercises, + exerciseSubmissions, modules, session, alerts, diff --git a/graphql/resolvers/exerciseSubmissionCrud.ts b/graphql/resolvers/exerciseSubmissionCrud.ts new file mode 100644 index 000000000..4111dea1e --- /dev/null +++ b/graphql/resolvers/exerciseSubmissionCrud.ts @@ -0,0 +1,10 @@ +import prisma from '../../prisma' + +export const exerciseSubmissions = () => { + return prisma.exerciseSubmission.findMany({ + include: { + exercise: true, + user: true + } + }) +} diff --git a/graphql/typeDefs.ts b/graphql/typeDefs.ts index c02fb5d3e..70163ec33 100644 --- a/graphql/typeDefs.ts +++ b/graphql/typeDefs.ts @@ -13,6 +13,7 @@ export default gql` submissions(lessonId: Int!): [Submission!] alerts: [Alert!]! getPreviousSubmissions(challengeId: Int!, userId: Int!): [Submission!] + exerciseSubmissions: [ExerciseSubmission!]! } type TokenResponse { @@ -266,4 +267,11 @@ export default gql` flaggedBy: User flaggedById: Int } + + type ExerciseSubmission { + id: Int! + user: User! + exercise: Exercise! + userAnswer: String! + } ` From 499013be0a179783086fb0813a5d0a40320411b7 Mon Sep 17 00:00:00 2001 From: Bryan Jennings Date: Thu, 29 Sep 2022 11:27:53 -0700 Subject: [PATCH 3/9] Filter exercise submissions so user only gets their exercise submissions --- graphql/resolvers/exerciseSubmissionCrud.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/graphql/resolvers/exerciseSubmissionCrud.ts b/graphql/resolvers/exerciseSubmissionCrud.ts index 4111dea1e..94328c790 100644 --- a/graphql/resolvers/exerciseSubmissionCrud.ts +++ b/graphql/resolvers/exerciseSubmissionCrud.ts @@ -1,10 +1,23 @@ +import { Context } from '../../@types/helpers' import prisma from '../../prisma' -export const exerciseSubmissions = () => { +export const exerciseSubmissions = ( + _parent: void, + _args: void, + context: Context +) => { + const userId = context.req.user?.id + if (!userId) return [] + return prisma.exerciseSubmission.findMany({ include: { exercise: true, user: true + }, + where: { + user: { + id: userId + } } }) } From 2e04076ba811a5ffeb21d33bd14978cf84d14572 Mon Sep 17 00:00:00 2001 From: Bryan Jennings Date: Thu, 29 Sep 2022 12:19:57 -0700 Subject: [PATCH 4/9] Add addExerciseSubmission GraphQL resolver --- graphql/index.tsx | 17 ++++++++++ graphql/resolvers.ts | 6 +++- graphql/resolvers/exerciseSubmissionCrud.ts | 35 +++++++++++++++++---- graphql/typeDefs.ts | 4 +++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/graphql/index.tsx b/graphql/index.tsx index 8644da5ac..6c2e72fc3 100644 --- a/graphql/index.tsx +++ b/graphql/index.tsx @@ -128,6 +128,7 @@ export type Mutation = { addAlert?: Maybe>> addComment?: Maybe addExercise: Exercise + addExerciseSubmission: ExerciseSubmission addModule: Module changeAdminRights?: Maybe changePw?: Maybe @@ -182,6 +183,11 @@ export type MutationAddExerciseArgs = { testStr?: InputMaybe } +export type MutationAddExerciseSubmissionArgs = { + exerciseId: Scalars['Int'] + userAnswer: Scalars['String'] +} + export type MutationAddModuleArgs = { content: Scalars['String'] lessonId: Scalars['Int'] @@ -1621,6 +1627,15 @@ export type MutationResolvers< 'answer' | 'description' | 'moduleId' > > + addExerciseSubmission?: Resolver< + ResolversTypes['ExerciseSubmission'], + ParentType, + ContextType, + RequireFields< + MutationAddExerciseSubmissionArgs, + 'exerciseId' | 'userAnswer' + > + > addModule?: Resolver< ResolversTypes['Module'], ParentType, @@ -5659,6 +5674,7 @@ export type MutationKeySpecifier = ( | 'addAlert' | 'addComment' | 'addExercise' + | 'addExerciseSubmission' | 'addModule' | 'changeAdminRights' | 'changePw' @@ -5690,6 +5706,7 @@ export type MutationFieldPolicy = { addAlert?: FieldPolicy | FieldReadFunction addComment?: FieldPolicy | FieldReadFunction addExercise?: FieldPolicy | FieldReadFunction + addExerciseSubmission?: FieldPolicy | FieldReadFunction addModule?: FieldPolicy | FieldReadFunction changeAdminRights?: FieldPolicy | FieldReadFunction changePw?: FieldPolicy | FieldReadFunction diff --git a/graphql/resolvers.ts b/graphql/resolvers.ts index 2bcbebdbc..e5f6aefd7 100644 --- a/graphql/resolvers.ts +++ b/graphql/resolvers.ts @@ -39,7 +39,10 @@ import { flagExercise, removeExerciseFlag } from './resolvers/exerciseCrud' -import { exerciseSubmissions } from './resolvers/exerciseSubmissionCrud' +import { + exerciseSubmissions, + addExerciseSubmission +} from './resolvers/exerciseSubmissionCrud' export default { Query: { @@ -69,6 +72,7 @@ export default { addExercise, updateExercise, deleteExercise, + addExerciseSubmission, login, logout, signup, diff --git a/graphql/resolvers/exerciseSubmissionCrud.ts b/graphql/resolvers/exerciseSubmissionCrud.ts index 94328c790..aa3a5e27f 100644 --- a/graphql/resolvers/exerciseSubmissionCrud.ts +++ b/graphql/resolvers/exerciseSubmissionCrud.ts @@ -1,3 +1,4 @@ +import { MutationAddExerciseSubmissionArgs } from '..' import { Context } from '../../@types/helpers' import prisma from '../../prisma' @@ -11,13 +12,35 @@ export const exerciseSubmissions = ( return prisma.exerciseSubmission.findMany({ include: { - exercise: true, + exercise: { include: { module: { include: { lesson: true } } } }, user: true }, - where: { - user: { - id: userId - } - } + where: { user: { id: userId } } + }) +} + +export const addExerciseSubmission = async ( + _parent: void, + { exerciseId, userAnswer }: MutationAddExerciseSubmissionArgs, + context: Context +) => { + const userId = context.req.user?.id + if (!userId) throw new Error('User should be logged in.') + + const exerciseSubmission = await prisma.exerciseSubmission.findFirst({ + where: { userId, exerciseId } + }) + + if (exerciseSubmission) { + return prisma.exerciseSubmission.update({ + data: { exerciseId, userId, userAnswer }, + where: { id: exerciseSubmission.id }, + include: { exercise: true, user: true } + }) + } + + return prisma.exerciseSubmission.create({ + data: { exerciseId, userId, userAnswer }, + include: { exercise: true, user: true } }) } diff --git a/graphql/typeDefs.ts b/graphql/typeDefs.ts index 70163ec33..75f70ce1b 100644 --- a/graphql/typeDefs.ts +++ b/graphql/typeDefs.ts @@ -89,6 +89,10 @@ export default gql` flagExercise(id: Int!, flagReason: String!): Exercise removeExerciseFlag(id: Int!): Exercise! deleteExercise(id: Int!): Exercise! + addExerciseSubmission( + exerciseId: Int! + userAnswer: String! + ): ExerciseSubmission! createLesson( description: String! docUrl: String From caee7cb04cece9e07d0973e66bfb6fe88c048a5c Mon Sep 17 00:00:00 2001 From: Bryan Jennings Date: Thu, 29 Sep 2022 12:35:32 -0700 Subject: [PATCH 5/9] Get exercise submissions from backend on DOJO exercises page --- graphql/index.tsx | 11 +++++++++++ graphql/queries/getExercises.ts | 6 ++++++ pages/exercises/[lessonSlug].tsx | 16 +++++++++++++--- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/graphql/index.tsx b/graphql/index.tsx index 6c2e72fc3..146b1852b 100644 --- a/graphql/index.tsx +++ b/graphql/index.tsx @@ -855,6 +855,11 @@ export type GetExercisesQuery = { lesson: { __typename?: 'Lesson'; slug: string } } }> + exerciseSubmissions: Array<{ + __typename?: 'ExerciseSubmission' + userAnswer: string + exercise: { __typename?: 'Exercise'; id: number } + }> } export type GetFlaggedExercisesQueryVariables = Exact<{ [key: string]: never }> @@ -3586,6 +3591,12 @@ export const GetExercisesDocument = gql` answer explanation } + exerciseSubmissions { + exercise { + id + } + userAnswer + } } ` export type GetExercisesProps< diff --git a/graphql/queries/getExercises.ts b/graphql/queries/getExercises.ts index a7d34a9c2..ed47c4e50 100644 --- a/graphql/queries/getExercises.ts +++ b/graphql/queries/getExercises.ts @@ -26,6 +26,12 @@ const GET_EXERCISES = gql` answer explanation } + exerciseSubmissions { + exercise { + id + } + userAnswer + } } ` diff --git a/pages/exercises/[lessonSlug].tsx b/pages/exercises/[lessonSlug].tsx index 038bff993..04d9fe63b 100644 --- a/pages/exercises/[lessonSlug].tsx +++ b/pages/exercises/[lessonSlug].tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import Layout from '../../components/Layout' import withQueryLoader, { QueryDataProps @@ -19,15 +19,25 @@ import styles from '../../scss/exercises.module.scss' const Exercises: React.FC> = ({ queryData }) => { - const { lessons, alerts, exercises } = queryData + const { lessons, alerts, exercises, exerciseSubmissions } = queryData const router = useRouter() const [exerciseIndex, setExerciseIndex] = useState(-1) const [userAnswers, setUserAnswers] = useState>({}) + useEffect(() => { + setUserAnswers( + Object.fromEntries( + exerciseSubmissions.map(submission => [ + submission.exercise.id, + submission.userAnswer + ]) + ) + ) + }, [exerciseSubmissions]) if (!router.isReady) return const slug = router.query.lessonSlug as string - if (!lessons || !alerts || !exercises) + if (!lessons || !alerts || !exercises || !exerciseSubmissions) return const currentLesson = lessons.find(lesson => lesson.slug === slug) From bbf65b27b9abc35c9b8fe60fcca92e92249d8f3a Mon Sep 17 00:00:00 2001 From: Bryan Jennings Date: Thu, 29 Sep 2022 12:56:19 -0700 Subject: [PATCH 6/9] Add exerciseSubmissions to mock exercise data --- __dummy__/getExercisesData.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/__dummy__/getExercisesData.ts b/__dummy__/getExercisesData.ts index b3ee44d86..1a4ee00ce 100644 --- a/__dummy__/getExercisesData.ts +++ b/__dummy__/getExercisesData.ts @@ -92,7 +92,8 @@ const getExercisesData: GetExercisesQuery = { answer: '-1', explanation: null } - ] + ], + exerciseSubmissions: [{ exercise: { id: 1 }, userAnswer: '15' }] } export default getExercisesData From 4f7d1f6e2756248f532984ba43682200c255211e Mon Sep 17 00:00:00 2001 From: Bryan Jennings Date: Thu, 29 Sep 2022 13:57:11 -0700 Subject: [PATCH 7/9] Add tests for exercise submission resolvers --- .../resolvers/exerciseSubmissionCrud.test.js | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 graphql/resolvers/exerciseSubmissionCrud.test.js diff --git a/graphql/resolvers/exerciseSubmissionCrud.test.js b/graphql/resolvers/exerciseSubmissionCrud.test.js new file mode 100644 index 000000000..ff91e0d7a --- /dev/null +++ b/graphql/resolvers/exerciseSubmissionCrud.test.js @@ -0,0 +1,79 @@ +/** + * @jest-environment node + */ +import prismaMock from '../../__tests__/utils/prismaMock' +import { + exerciseSubmissions, + addExerciseSubmission +} from './exerciseSubmissionCrud' + +describe('exerciseSubmissions resolver', () => { + test('Should return an empty array if the user is not logged in', () => { + const mockContext = { req: { user: null } } + + expect(exerciseSubmissions(undefined, undefined, mockContext)).toEqual([]) + }) + + test("Should return user's exercise submissions of the logged in user", () => { + const mockExerciseSubmissions = [{ id: 1 }, { id: 2 }] + const mockContext = { req: { user: { id: 1 } } } + prismaMock.exerciseSubmission.findMany.mockResolvedValue( + mockExerciseSubmissions + ) + + expect( + exerciseSubmissions(undefined, undefined, mockContext) + ).resolves.toEqual(mockExerciseSubmissions) + }) +}) + +describe('addExerciseSubmission resolver', () => { + test("Should throw an error if the user isn't logged in", () => { + const mockContext = { req: { user: null } } + const mockArgs = { exerciseId: 2, userAnswer: '123' } + + expect( + addExerciseSubmission(undefined, mockArgs, mockContext) + ).rejects.toEqual(new Error('User should be logged in.')) + }) + + test('Should update an exercise submission if it already exists', () => { + const mockContext = { req: { user: { id: 1 } } } + const mockArgs = { exerciseId: 2, userAnswer: '123' } + const mockExerciseSubmission = { + id: 1, + exercise: { id: 2 }, + user: { id: 1 }, + userAnswer: '123' + } + prismaMock.exerciseSubmission.findFirst.mockResolvedValue( + mockExerciseSubmission + ) + prismaMock.exerciseSubmission.update.mockResolvedValue( + mockExerciseSubmission + ) + + expect( + addExerciseSubmission(undefined, mockArgs, mockContext) + ).resolves.toEqual(mockExerciseSubmission) + }) + + test("Should create a new exercise submission if one doesn't already exist", () => { + const mockContext = { req: { user: { id: 1 } } } + const mockArgs = { exerciseId: 2, userAnswer: '123' } + const mockExerciseSubmission = { + id: 1, + exercise: { id: 2 }, + user: { id: 1 }, + userAnswer: '123' + } + prismaMock.exerciseSubmission.findFirst.mockResolvedValue(null) + prismaMock.exerciseSubmission.create.mockResolvedValue( + mockExerciseSubmission + ) + + expect( + addExerciseSubmission(undefined, mockArgs, mockContext) + ).resolves.toEqual(mockExerciseSubmission) + }) +}) From 323a31426818cab75c90b53239db32133784d4cd Mon Sep 17 00:00:00 2001 From: Bryan Jennings Date: Thu, 29 Sep 2022 19:39:31 -0700 Subject: [PATCH 8/9] Remove module, lesson, and user data from exercise submission resolver to improve performance --- __dummy__/getExercisesData.ts | 2 +- graphql/index.tsx | 22 ++++++++++----------- graphql/queries/getExercises.ts | 4 +--- graphql/resolvers/exerciseSubmissionCrud.ts | 10 ++-------- graphql/typeDefs.ts | 4 ++-- pages/exercises/[lessonSlug].tsx | 2 +- 6 files changed, 17 insertions(+), 27 deletions(-) diff --git a/__dummy__/getExercisesData.ts b/__dummy__/getExercisesData.ts index 1a4ee00ce..9c66a4324 100644 --- a/__dummy__/getExercisesData.ts +++ b/__dummy__/getExercisesData.ts @@ -93,7 +93,7 @@ const getExercisesData: GetExercisesQuery = { explanation: null } ], - exerciseSubmissions: [{ exercise: { id: 1 }, userAnswer: '15' }] + exerciseSubmissions: [{ exerciseId: 1, userAnswer: '15' }] } export default getExercisesData diff --git a/graphql/index.tsx b/graphql/index.tsx index 146b1852b..7065d3348 100644 --- a/graphql/index.tsx +++ b/graphql/index.tsx @@ -89,10 +89,10 @@ export type Exercise = { export type ExerciseSubmission = { __typename?: 'ExerciseSubmission' - exercise: Exercise + exerciseId: Scalars['Int'] id: Scalars['Int'] - user: User userAnswer: Scalars['String'] + userId: Scalars['Int'] } export type Lesson = { @@ -857,8 +857,8 @@ export type GetExercisesQuery = { }> exerciseSubmissions: Array<{ __typename?: 'ExerciseSubmission' + exerciseId: number userAnswer: string - exercise: { __typename?: 'Exercise'; id: number } }> } @@ -1549,10 +1549,10 @@ export type ExerciseSubmissionResolvers< ContextType = Context, ParentType extends ResolversParentTypes['ExerciseSubmission'] = ResolversParentTypes['ExerciseSubmission'] > = ResolversObject<{ - exercise?: Resolver + exerciseId?: Resolver id?: Resolver - user?: Resolver userAnswer?: Resolver + userId?: Resolver __isTypeOf?: IsTypeOfResolverFn }> @@ -3592,9 +3592,7 @@ export const GetExercisesDocument = gql` explanation } exerciseSubmissions { - exercise { - id - } + exerciseId userAnswer } } @@ -5620,17 +5618,17 @@ export type ExerciseFieldPolicy = { testStr?: FieldPolicy | FieldReadFunction } export type ExerciseSubmissionKeySpecifier = ( - | 'exercise' + | 'exerciseId' | 'id' - | 'user' | 'userAnswer' + | 'userId' | ExerciseSubmissionKeySpecifier )[] export type ExerciseSubmissionFieldPolicy = { - exercise?: FieldPolicy | FieldReadFunction + exerciseId?: FieldPolicy | FieldReadFunction id?: FieldPolicy | FieldReadFunction - user?: FieldPolicy | FieldReadFunction userAnswer?: FieldPolicy | FieldReadFunction + userId?: FieldPolicy | FieldReadFunction } export type LessonKeySpecifier = ( | 'challenges' diff --git a/graphql/queries/getExercises.ts b/graphql/queries/getExercises.ts index ed47c4e50..69c259b3e 100644 --- a/graphql/queries/getExercises.ts +++ b/graphql/queries/getExercises.ts @@ -27,9 +27,7 @@ const GET_EXERCISES = gql` explanation } exerciseSubmissions { - exercise { - id - } + exerciseId userAnswer } } diff --git a/graphql/resolvers/exerciseSubmissionCrud.ts b/graphql/resolvers/exerciseSubmissionCrud.ts index aa3a5e27f..78fd72a6c 100644 --- a/graphql/resolvers/exerciseSubmissionCrud.ts +++ b/graphql/resolvers/exerciseSubmissionCrud.ts @@ -11,10 +11,6 @@ export const exerciseSubmissions = ( if (!userId) return [] return prisma.exerciseSubmission.findMany({ - include: { - exercise: { include: { module: { include: { lesson: true } } } }, - user: true - }, where: { user: { id: userId } } }) } @@ -34,13 +30,11 @@ export const addExerciseSubmission = async ( if (exerciseSubmission) { return prisma.exerciseSubmission.update({ data: { exerciseId, userId, userAnswer }, - where: { id: exerciseSubmission.id }, - include: { exercise: true, user: true } + where: { id: exerciseSubmission.id } }) } return prisma.exerciseSubmission.create({ - data: { exerciseId, userId, userAnswer }, - include: { exercise: true, user: true } + data: { exerciseId, userId, userAnswer } }) } diff --git a/graphql/typeDefs.ts b/graphql/typeDefs.ts index 75f70ce1b..1dfac77b2 100644 --- a/graphql/typeDefs.ts +++ b/graphql/typeDefs.ts @@ -274,8 +274,8 @@ export default gql` type ExerciseSubmission { id: Int! - user: User! - exercise: Exercise! + userId: Int! + exerciseId: Int! userAnswer: String! } ` diff --git a/pages/exercises/[lessonSlug].tsx b/pages/exercises/[lessonSlug].tsx index 04d9fe63b..041d3e0ed 100644 --- a/pages/exercises/[lessonSlug].tsx +++ b/pages/exercises/[lessonSlug].tsx @@ -27,7 +27,7 @@ const Exercises: React.FC> = ({ setUserAnswers( Object.fromEntries( exerciseSubmissions.map(submission => [ - submission.exercise.id, + submission.exerciseId, submission.userAnswer ]) ) From 584f7b13fea02b80184ef93971906d90c8623f0d Mon Sep 17 00:00:00 2001 From: Bryan Jennings Date: Thu, 29 Sep 2022 20:08:01 -0700 Subject: [PATCH 9/9] Add more expects to exercise submissions resolver tests --- .../resolvers/exerciseSubmissionCrud.test.js | 40 +++++++++++++------ graphql/resolvers/exerciseSubmissionCrud.ts | 2 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/graphql/resolvers/exerciseSubmissionCrud.test.js b/graphql/resolvers/exerciseSubmissionCrud.test.js index ff91e0d7a..782ebd7b2 100644 --- a/graphql/resolvers/exerciseSubmissionCrud.test.js +++ b/graphql/resolvers/exerciseSubmissionCrud.test.js @@ -14,36 +14,39 @@ describe('exerciseSubmissions resolver', () => { expect(exerciseSubmissions(undefined, undefined, mockContext)).toEqual([]) }) - test("Should return user's exercise submissions of the logged in user", () => { + test("Should return user's exercise submissions of the logged in user", async () => { const mockExerciseSubmissions = [{ id: 1 }, { id: 2 }] const mockContext = { req: { user: { id: 1 } } } prismaMock.exerciseSubmission.findMany.mockResolvedValue( mockExerciseSubmissions ) - expect( + await expect( exerciseSubmissions(undefined, undefined, mockContext) ).resolves.toEqual(mockExerciseSubmissions) + expect(prismaMock.exerciseSubmission.findMany).toBeCalledWith({ + where: { userId: 1 } + }) }) }) describe('addExerciseSubmission resolver', () => { - test("Should throw an error if the user isn't logged in", () => { + test("Should throw an error if the user isn't logged in", async () => { const mockContext = { req: { user: null } } const mockArgs = { exerciseId: 2, userAnswer: '123' } - expect( + await expect( addExerciseSubmission(undefined, mockArgs, mockContext) ).rejects.toEqual(new Error('User should be logged in.')) }) - test('Should update an exercise submission if it already exists', () => { + test('Should update an exercise submission if it already exists', async () => { const mockContext = { req: { user: { id: 1 } } } const mockArgs = { exerciseId: 2, userAnswer: '123' } const mockExerciseSubmission = { id: 1, - exercise: { id: 2 }, - user: { id: 1 }, + exerciseId: 2, + userId: 1, userAnswer: '123' } prismaMock.exerciseSubmission.findFirst.mockResolvedValue( @@ -53,18 +56,25 @@ describe('addExerciseSubmission resolver', () => { mockExerciseSubmission ) - expect( + await expect( addExerciseSubmission(undefined, mockArgs, mockContext) ).resolves.toEqual(mockExerciseSubmission) + expect(prismaMock.exerciseSubmission.findFirst).toBeCalledWith({ + where: { exerciseId: 2, userId: 1 } + }) + expect(prismaMock.exerciseSubmission.update).toBeCalledWith({ + data: { exerciseId: 2, userAnswer: '123', userId: 1 }, + where: { id: 1 } + }) }) - test("Should create a new exercise submission if one doesn't already exist", () => { + test("Should create a new exercise submission if one doesn't already exist", async () => { const mockContext = { req: { user: { id: 1 } } } const mockArgs = { exerciseId: 2, userAnswer: '123' } const mockExerciseSubmission = { id: 1, - exercise: { id: 2 }, - user: { id: 1 }, + exerciseId: 2, + userId: 1, userAnswer: '123' } prismaMock.exerciseSubmission.findFirst.mockResolvedValue(null) @@ -72,8 +82,14 @@ describe('addExerciseSubmission resolver', () => { mockExerciseSubmission ) - expect( + await expect( addExerciseSubmission(undefined, mockArgs, mockContext) ).resolves.toEqual(mockExerciseSubmission) + expect(prismaMock.exerciseSubmission.findFirst).toBeCalledWith({ + where: { exerciseId: 2, userId: 1 } + }) + expect(prismaMock.exerciseSubmission.create).toBeCalledWith({ + data: { exerciseId: 2, userAnswer: '123', userId: 1 } + }) }) }) diff --git a/graphql/resolvers/exerciseSubmissionCrud.ts b/graphql/resolvers/exerciseSubmissionCrud.ts index 78fd72a6c..cd7fd1c5e 100644 --- a/graphql/resolvers/exerciseSubmissionCrud.ts +++ b/graphql/resolvers/exerciseSubmissionCrud.ts @@ -11,7 +11,7 @@ export const exerciseSubmissions = ( if (!userId) return [] return prisma.exerciseSubmission.findMany({ - where: { user: { id: userId } } + where: { userId } }) }