From 5906595937223c5ad6445a42f56cf68e8c4eec6a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 01:35:48 +0000 Subject: [PATCH] Restructure practice tests and clean up progress tracking This commit restructures how practice test questions are stored by moving them from a JSONB column in the `practice_tests` table into a dedicated `practice_test_questions` table. This allows for better individual question querying and updating. Key changes: - Database migration to create the new `practice_test_questions` table and migrate existing JSONB data. - Updated GraphQL schema: - `TermATP` snapshots now use `termSnapshot` and `defSnapshot`. - Added `id` to the `Question` type. - Replaced `updatePracticeTest` with `updatePracticeTestQuestion` to allow targeted FRQ updates. - Removed all references to the unused Leitner system and term progress history. - Updated mutations and resolvers: - `RecordPracticeTest` now populates the new questions table. - `UpdatePracticeTestQuestion` implements manual FRQ correctness overrides and synchronizes practice test accuracy and term progress stats. - `PracticeTest.Questions` resolver now fetches and reconstructs questions from the database. - Cleanup of obsolete dataloaders, mapping tables, and models. - Updated integration tests to match the new schema and logic. --- ...20_restructure_practice_test_questions.sql | 99 ++ graph/generated.go | 1055 ++++------------- graph/loader/loader.go | 113 +- graph/model/models_gen.go | 21 +- graph/model/practice_test_model.go | 15 +- graph/model/progress_history.go | 11 - graph/model/term_progress.go | 2 - graph/mutation.graphqls | 6 +- graph/query.graphqls | 5 +- graph/resolver/mutation.resolvers.go | 426 +++---- graph/resolver/query.resolvers.go | 85 +- graph/resolver/term.resolvers.go | 10 - graph/term.graphqls | 13 - tests/practice_test_test.go | 73 +- tests/term_progress_test.go | 6 +- 15 files changed, 669 insertions(+), 1271 deletions(-) create mode 100644 db/migrations/202606290020_restructure_practice_test_questions.sql delete mode 100644 graph/model/progress_history.go diff --git a/db/migrations/202606290020_restructure_practice_test_questions.sql b/db/migrations/202606290020_restructure_practice_test_questions.sql new file mode 100644 index 0000000..e6b3ab8 --- /dev/null +++ b/db/migrations/202606290020_restructure_practice_test_questions.sql @@ -0,0 +1,99 @@ +-- migrate:up + +CREATE TYPE public.question_type AS ENUM ('MCQ', 'TFQ', 'FRQ'); + +CREATE TABLE public.practice_test_questions ( + id uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, + practice_test_id uuid NOT NULL REFERENCES public.practice_tests(id) ON DELETE CASCADE, + term_id uuid NOT NULL REFERENCES public.terms(id) ON DELETE CASCADE, + term_snapshot text NOT NULL, + def_snapshot text NOT NULL, + type public.question_type NOT NULL, + answer_with public.answer_with_enum NOT NULL, + correct boolean NOT NULL, + position integer NOT NULL, + data jsonb NOT NULL, + UNIQUE (practice_test_id, position) +); + +-- Migrate data +INSERT INTO public.practice_test_questions ( + practice_test_id, term_id, term_snapshot, def_snapshot, type, answer_with, correct, position, data +) +SELECT + pt.id, + (q->COALESCE(sub.type_lower, 'mcq')->'term'->>'id')::uuid, + q->COALESCE(sub.type_lower, 'mcq')->'term'->>'term', + q->COALESCE(sub.type_lower, 'mcq')->'term'->>'def', + sub.type_upper, + (q->COALESCE(sub.type_lower, 'mcq')->>'answerWith')::public.answer_with_enum, + CASE + WHEN sub.type_upper = 'FRQ' THEN + COALESCE((q->'frq'->>'correct')::boolean, false) OR COALESCE((q->'frq'->>'userMarkedCorrect')::boolean, false) + ELSE + COALESCE((q->sub.type_lower->>'correct')::boolean, false) + END, + pos::int - 1, + CASE + WHEN sub.type_upper = 'MCQ' THEN + jsonb_build_object( + 'distractors', ( + SELECT jsonb_agg( + jsonb_build_object( + 'id', d->>'id', + 'termSnapshot', d->>'term', + 'defSnapshot', d->>'def' + ) + ) + FROM jsonb_array_elements(q->'mcq'->'distractors') d + ), + 'correctChoiceIndex', (q->'mcq'->>'correctChoiceIndex')::int, + 'answeredIndex', (q->'mcq'->>'answeredIndex')::int + ) + WHEN sub.type_upper = 'TFQ' THEN + jsonb_build_object( + 'answeredBool', (q->'tfq'->>'answeredBool')::boolean, + 'distractor', CASE + WHEN q->'tfq'->'distractor' IS NOT NULL AND q->'tfq'->'distractor' != 'null'::jsonb THEN + jsonb_build_object( + 'id', q->'tfq'->'distractor'->>'id', + 'termSnapshot', q->'tfq'->'distractor'->>'term', + 'defSnapshot', q->'tfq'->'distractor'->>'def' + ) + ELSE NULL + END + ) + WHEN sub.type_upper = 'FRQ' THEN + jsonb_build_object( + 'answeredString', q->'frq'->>'answeredString', + 'userMarkedCorrect', COALESCE((q->'frq'->>'userMarkedCorrect')::boolean, false) + ) + END +FROM public.practice_tests pt +CROSS JOIN LATERAL jsonb_array_elements(pt.questions) WITH ORDINALITY AS q_arr(q, pos) +CROSS JOIN LATERAL ( + SELECT + CASE + WHEN q ? 'mcq' THEN 'mcq' + WHEN q ? 'tfq' THEN 'tfq' + WHEN q ? 'frq' THEN 'frq' + END as type_lower, + CASE + WHEN q ? 'mcq' THEN 'MCQ'::public.question_type + WHEN q ? 'tfq' THEN 'TFQ'::public.question_type + WHEN q ? 'frq' THEN 'FRQ'::public.question_type + END as type_upper +) sub; + +ALTER TABLE public.practice_tests DROP COLUMN questions; +DROP TABLE IF EXISTS public.practice_test_question_terms; +DROP TABLE IF EXISTS public.practice_test_distractor_terms; +DROP TABLE IF EXISTS public.term_progress_history; +ALTER TABLE public.term_progress DROP COLUMN IF EXISTS term_leitner_system_box; +ALTER TABLE public.term_progress DROP COLUMN IF EXISTS def_leitner_system_box; + +GRANT SELECT, INSERT, UPDATE, DELETE ON public.practice_test_questions TO quizfreely_api; + +-- migrate:down +DROP TABLE public.practice_test_questions; +DROP TYPE public.question_type; diff --git a/graph/generated.go b/graph/generated.go index aae10ca..5fad070 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -131,28 +131,28 @@ type ComplexityRoot struct { } Mutation struct { - CreateFolder func(childComplexity int, name string) int - CreateStudyset func(childComplexity int, studyset model.StudysetInput, draft bool, folderID *string) int - CreateTerms func(childComplexity int, studysetID string, terms []*model.NewTermInput) int - DeleteFolder func(childComplexity int, id string) int - DeleteStudyset func(childComplexity int, id string) int - DeleteTerms func(childComplexity int, studysetID string, ids []string) int - RecordConfusedTerms func(childComplexity int, confusedTerms []*model.TermConfusionPairInput) int - RecordFsrsReviewLog func(childComplexity int, termID string, reviewLog model.FSRSReviewLogInput) int - RecordMatchActivity func(childComplexity int, input model.MatchActivityInput) int - RecordPracticeTest func(childComplexity int, input model.PracticeTestInput) int - RemoveStudysetFromFolder func(childComplexity int, studysetID string) int - RenameFolder func(childComplexity int, id string, name string) int - SaveStudyset func(childComplexity int, studysetID string) int - SetStudysetFolder func(childComplexity int, studysetID string, folderID string) int - SetStudysetSeoIndexing func(childComplexity int, studysetID string, approved bool) int - UnsaveStudyset func(childComplexity int, studysetID string) int - UpdateFsrsCard func(childComplexity int, termID string, card model.FSRSCardInput) int - UpdatePracticeTest func(childComplexity int, id string, input model.PracticeTestInput) int - UpdateStudyset func(childComplexity int, id string, studyset *model.StudysetInput, draft bool) int - UpdateTermProgress func(childComplexity int, termProgress []*model.TermProgressInput) int - UpdateTerms func(childComplexity int, studysetID string, terms []*model.TermInput) int - UpdateUser func(childComplexity int, displayName *string) int + CreateFolder func(childComplexity int, name string) int + CreateStudyset func(childComplexity int, studyset model.StudysetInput, draft bool, folderID *string) int + CreateTerms func(childComplexity int, studysetID string, terms []*model.NewTermInput) int + DeleteFolder func(childComplexity int, id string) int + DeleteStudyset func(childComplexity int, id string) int + DeleteTerms func(childComplexity int, studysetID string, ids []string) int + RecordConfusedTerms func(childComplexity int, confusedTerms []*model.TermConfusionPairInput) int + RecordFsrsReviewLog func(childComplexity int, termID string, reviewLog model.FSRSReviewLogInput) int + RecordMatchActivity func(childComplexity int, input model.MatchActivityInput) int + RecordPracticeTest func(childComplexity int, input model.PracticeTestInput) int + RemoveStudysetFromFolder func(childComplexity int, studysetID string) int + RenameFolder func(childComplexity int, id string, name string) int + SaveStudyset func(childComplexity int, studysetID string) int + SetStudysetFolder func(childComplexity int, studysetID string, folderID string) int + SetStudysetSeoIndexing func(childComplexity int, studysetID string, approved bool) int + UnsaveStudyset func(childComplexity int, studysetID string) int + UpdateFsrsCard func(childComplexity int, termID string, card model.FSRSCardInput) int + UpdatePracticeTestQuestion func(childComplexity int, id string, correct bool, userMarkedCorrect *bool) int + UpdateStudyset func(childComplexity int, id string, studyset *model.StudysetInput, draft bool) int + UpdateTermProgress func(childComplexity int, termProgress []*model.TermProgressInput) int + UpdateTerms func(childComplexity int, studysetID string, terms []*model.TermInput) int + UpdateUser func(childComplexity int, displayName *string) int } PageInfo struct { @@ -199,6 +199,7 @@ type ComplexityRoot struct { Question struct { Frq func(childComplexity int) int + ID func(childComplexity int) int Mcq func(childComplexity int) int Tfq func(childComplexity int) int } @@ -255,7 +256,6 @@ type ComplexityRoot struct { ID func(childComplexity int) int PracticeTests func(childComplexity int) int Progress func(childComplexity int) int - ProgressHistory func(childComplexity int) int SortOrder func(childComplexity int) int Term func(childComplexity int) int TermImageURL func(childComplexity int) int @@ -265,9 +265,9 @@ type ComplexityRoot struct { } TermATP struct { - Def func(childComplexity int) int - ID func(childComplexity int) int - Term func(childComplexity int) int + DefSnapshot func(childComplexity int) int + ID func(childComplexity int) int + TermSnapshot func(childComplexity int) int } TermConfusionPair struct { @@ -280,28 +280,17 @@ type ComplexityRoot struct { } TermProgress struct { - DefCorrectCount func(childComplexity int) int - DefFirstReviewedAt func(childComplexity int) int - DefIncorrectCount func(childComplexity int) int - DefLastReviewedAt func(childComplexity int) int - DefLeitnerSystemBox func(childComplexity int) int - DefReviewCount func(childComplexity int) int - ID func(childComplexity int) int - TermCorrectCount func(childComplexity int) int - TermFirstReviewedAt func(childComplexity int) int - TermIncorrectCount func(childComplexity int) int - TermLastReviewedAt func(childComplexity int) int - TermLeitnerSystemBox func(childComplexity int) int - TermReviewCount func(childComplexity int) int - } - - TermProgressHistory struct { - DefCorrectCount func(childComplexity int) int - DefIncorrectCount func(childComplexity int) int - ID func(childComplexity int) int - TermCorrectCount func(childComplexity int) int - TermIncorrectCount func(childComplexity int) int - Timestamp func(childComplexity int) int + DefCorrectCount func(childComplexity int) int + DefFirstReviewedAt func(childComplexity int) int + DefIncorrectCount func(childComplexity int) int + DefLastReviewedAt func(childComplexity int) int + DefReviewCount func(childComplexity int) int + ID func(childComplexity int) int + TermCorrectCount func(childComplexity int) int + TermFirstReviewedAt func(childComplexity int) int + TermIncorrectCount func(childComplexity int) int + TermLastReviewedAt func(childComplexity int) int + TermReviewCount func(childComplexity int) int } User struct { @@ -329,7 +318,7 @@ type MutationResolver interface { UpdateTermProgress(ctx context.Context, termProgress []*model.TermProgressInput) ([]*model.TermProgress, error) RecordConfusedTerms(ctx context.Context, confusedTerms []*model.TermConfusionPairInput) (*bool, error) RecordPracticeTest(ctx context.Context, input model.PracticeTestInput) (*model.PracticeTest, error) - UpdatePracticeTest(ctx context.Context, id string, input model.PracticeTestInput) (*model.PracticeTest, error) + UpdatePracticeTestQuestion(ctx context.Context, id string, correct bool, userMarkedCorrect *bool) (*model.Question, error) CreateFolder(ctx context.Context, name string) (*model.Folder, error) RenameFolder(ctx context.Context, id string, name string) (*model.Folder, error) DeleteFolder(ctx context.Context, id string) (*string, error) @@ -386,7 +375,6 @@ type SubjectResolver interface { } type TermResolver interface { Progress(ctx context.Context, obj *model.Term) (*model.TermProgress, error) - ProgressHistory(ctx context.Context, obj *model.Term) ([]*model.TermProgressHistory, error) TopConfusionPairs(ctx context.Context, obj *model.Term) ([]*model.TermConfusionPair, error) TopReverseConfusionPairs(ctx context.Context, obj *model.Term) ([]*model.TermConfusionPair, error) FsrsCard(ctx context.Context, obj *model.Term) (*model.FSRSCard, error) @@ -978,17 +966,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Mutation.UpdateFsrsCard(childComplexity, args["termId"].(string), args["card"].(model.FSRSCardInput)), true - case "Mutation.updatePracticeTest": - if e.complexity.Mutation.UpdatePracticeTest == nil { + case "Mutation.updatePracticeTestQuestion": + if e.complexity.Mutation.UpdatePracticeTestQuestion == nil { break } - args, err := ec.field_Mutation_updatePracticeTest_args(ctx, rawArgs) + args, err := ec.field_Mutation_updatePracticeTestQuestion_args(ctx, rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.UpdatePracticeTest(childComplexity, args["id"].(string), args["input"].(model.PracticeTestInput)), true + return e.complexity.Mutation.UpdatePracticeTestQuestion(childComplexity, args["id"].(string), args["correct"].(bool), args["userMarkedCorrect"].(*bool)), true case "Mutation.updateStudyset": if e.complexity.Mutation.UpdateStudyset == nil { @@ -1371,6 +1359,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Question.Frq(childComplexity), true + case "Question.id": + if e.complexity.Question.ID == nil { + break + } + + return e.complexity.Question.ID(childComplexity), true + case "Question.mcq": if e.complexity.Question.Mcq == nil { break @@ -1642,13 +1637,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Term.Progress(childComplexity), true - case "Term.progressHistory": - if e.complexity.Term.ProgressHistory == nil { - break - } - - return e.complexity.Term.ProgressHistory(childComplexity), true - case "Term.sortOrder": if e.complexity.Term.SortOrder == nil { break @@ -1691,12 +1679,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Term.UpdatedAt(childComplexity), true - case "TermATP.def": - if e.complexity.TermATP.Def == nil { + case "TermATP.defSnapshot": + if e.complexity.TermATP.DefSnapshot == nil { break } - return e.complexity.TermATP.Def(childComplexity), true + return e.complexity.TermATP.DefSnapshot(childComplexity), true case "TermATP.id": if e.complexity.TermATP.ID == nil { @@ -1705,12 +1693,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.TermATP.ID(childComplexity), true - case "TermATP.term": - if e.complexity.TermATP.Term == nil { + case "TermATP.termSnapshot": + if e.complexity.TermATP.TermSnapshot == nil { break } - return e.complexity.TermATP.Term(childComplexity), true + return e.complexity.TermATP.TermSnapshot(childComplexity), true case "TermConfusionPair.answeredWith": if e.complexity.TermConfusionPair.AnsweredWith == nil { @@ -1782,13 +1770,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.TermProgress.DefLastReviewedAt(childComplexity), true - case "TermProgress.defLeitnerSystemBox": - if e.complexity.TermProgress.DefLeitnerSystemBox == nil { - break - } - - return e.complexity.TermProgress.DefLeitnerSystemBox(childComplexity), true - case "TermProgress.defReviewCount": if e.complexity.TermProgress.DefReviewCount == nil { break @@ -1831,13 +1812,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.TermProgress.TermLastReviewedAt(childComplexity), true - case "TermProgress.termLeitnerSystemBox": - if e.complexity.TermProgress.TermLeitnerSystemBox == nil { - break - } - - return e.complexity.TermProgress.TermLeitnerSystemBox(childComplexity), true - case "TermProgress.termReviewCount": if e.complexity.TermProgress.TermReviewCount == nil { break @@ -1845,48 +1819,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.TermProgress.TermReviewCount(childComplexity), true - case "TermProgressHistory.defCorrectCount": - if e.complexity.TermProgressHistory.DefCorrectCount == nil { - break - } - - return e.complexity.TermProgressHistory.DefCorrectCount(childComplexity), true - - case "TermProgressHistory.defIncorrectCount": - if e.complexity.TermProgressHistory.DefIncorrectCount == nil { - break - } - - return e.complexity.TermProgressHistory.DefIncorrectCount(childComplexity), true - - case "TermProgressHistory.id": - if e.complexity.TermProgressHistory.ID == nil { - break - } - - return e.complexity.TermProgressHistory.ID(childComplexity), true - - case "TermProgressHistory.termCorrectCount": - if e.complexity.TermProgressHistory.TermCorrectCount == nil { - break - } - - return e.complexity.TermProgressHistory.TermCorrectCount(childComplexity), true - - case "TermProgressHistory.termIncorrectCount": - if e.complexity.TermProgressHistory.TermIncorrectCount == nil { - break - } - - return e.complexity.TermProgressHistory.TermIncorrectCount(childComplexity), true - - case "TermProgressHistory.timestamp": - if e.complexity.TermProgressHistory.Timestamp == nil { - break - } - - return e.complexity.TermProgressHistory.Timestamp(childComplexity), true - case "User.displayName": if e.complexity.User.DisplayName == nil { break @@ -2360,7 +2292,7 @@ func (ec *executionContext) field_Mutation_updateFsrsCard_args(ctx context.Conte return args, nil } -func (ec *executionContext) field_Mutation_updatePracticeTest_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { +func (ec *executionContext) field_Mutation_updatePracticeTestQuestion_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} arg0, err := graphql.ProcessArgField(ctx, rawArgs, "id", ec.unmarshalNID2string) @@ -2368,11 +2300,16 @@ func (ec *executionContext) field_Mutation_updatePracticeTest_args(ctx context.C return nil, err } args["id"] = arg0 - arg1, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNPracticeTestInput2quizfreelyᚋapiᚋgraphᚋmodelᚐPracticeTestInput) + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "correct", ec.unmarshalNBoolean2bool) + if err != nil { + return nil, err + } + args["correct"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "userMarkedCorrect", ec.unmarshalOBoolean2ᚖbool) if err != nil { return nil, err } - args["input"] = arg1 + args["userMarkedCorrect"] = arg2 return args, nil } @@ -3209,10 +3146,10 @@ func (ec *executionContext) fieldContext_FRQ_term(_ context.Context, field graph switch field.Name { case "id": return ec.fieldContext_TermATP_id(ctx, field) - case "term": - return ec.fieldContext_TermATP_term(ctx, field) - case "def": - return ec.fieldContext_TermATP_def(ctx, field) + case "termSnapshot": + return ec.fieldContext_TermATP_termSnapshot(ctx, field) + case "defSnapshot": + return ec.fieldContext_TermATP_defSnapshot(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type TermATP", field.Name) }, @@ -4736,10 +4673,10 @@ func (ec *executionContext) fieldContext_MCQ_term(_ context.Context, field graph switch field.Name { case "id": return ec.fieldContext_TermATP_id(ctx, field) - case "term": - return ec.fieldContext_TermATP_term(ctx, field) - case "def": - return ec.fieldContext_TermATP_def(ctx, field) + case "termSnapshot": + return ec.fieldContext_TermATP_termSnapshot(ctx, field) + case "defSnapshot": + return ec.fieldContext_TermATP_defSnapshot(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type TermATP", field.Name) }, @@ -4964,10 +4901,10 @@ func (ec *executionContext) fieldContext_MCQ_distractors(_ context.Context, fiel switch field.Name { case "id": return ec.fieldContext_TermATP_id(ctx, field) - case "term": - return ec.fieldContext_TermATP_term(ctx, field) - case "def": - return ec.fieldContext_TermATP_def(ctx, field) + case "termSnapshot": + return ec.fieldContext_TermATP_termSnapshot(ctx, field) + case "defSnapshot": + return ec.fieldContext_TermATP_defSnapshot(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type TermATP", field.Name) }, @@ -5365,8 +5302,6 @@ func (ec *executionContext) fieldContext_Mutation_createTerms(ctx context.Contex return ec.fieldContext_Term_sortOrder(ctx, field) case "progress": return ec.fieldContext_Term_progress(ctx, field) - case "progressHistory": - return ec.fieldContext_Term_progressHistory(ctx, field) case "topConfusionPairs": return ec.fieldContext_Term_topConfusionPairs(ctx, field) case "topReverseConfusionPairs": @@ -5449,8 +5384,6 @@ func (ec *executionContext) fieldContext_Mutation_updateTerms(ctx context.Contex return ec.fieldContext_Term_sortOrder(ctx, field) case "progress": return ec.fieldContext_Term_progress(ctx, field) - case "progressHistory": - return ec.fieldContext_Term_progressHistory(ctx, field) case "topConfusionPairs": return ec.fieldContext_Term_topConfusionPairs(ctx, field) case "topReverseConfusionPairs": @@ -5711,10 +5644,6 @@ func (ec *executionContext) fieldContext_Mutation_updateTermProgress(ctx context return ec.fieldContext_TermProgress_defCorrectCount(ctx, field) case "defIncorrectCount": return ec.fieldContext_TermProgress_defIncorrectCount(ctx, field) - case "termLeitnerSystemBox": - return ec.fieldContext_TermProgress_termLeitnerSystemBox(ctx, field) - case "defLeitnerSystemBox": - return ec.fieldContext_TermProgress_defLeitnerSystemBox(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type TermProgress", field.Name) }, @@ -5851,8 +5780,8 @@ func (ec *executionContext) fieldContext_Mutation_recordPracticeTest(ctx context return fc, nil } -func (ec *executionContext) _Mutation_updatePracticeTest(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_updatePracticeTest(ctx, field) +func (ec *executionContext) _Mutation_updatePracticeTestQuestion(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updatePracticeTestQuestion(ctx, field) if err != nil { return graphql.Null } @@ -5865,7 +5794,7 @@ func (ec *executionContext) _Mutation_updatePracticeTest(ctx context.Context, fi }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdatePracticeTest(rctx, fc.Args["id"].(string), fc.Args["input"].(model.PracticeTestInput)) + return ec.resolvers.Mutation().UpdatePracticeTestQuestion(rctx, fc.Args["id"].(string), fc.Args["correct"].(bool), fc.Args["userMarkedCorrect"].(*bool)) }) if err != nil { ec.Error(ctx, err) @@ -5874,12 +5803,12 @@ func (ec *executionContext) _Mutation_updatePracticeTest(ctx context.Context, fi if resTmp == nil { return graphql.Null } - res := resTmp.(*model.PracticeTest) + res := resTmp.(*model.Question) fc.Result = res - return ec.marshalOPracticeTest2ᚖquizfreelyᚋapiᚋgraphᚋmodelᚐPracticeTest(ctx, field.Selections, res) + return ec.marshalOQuestion2ᚖquizfreelyᚋapiᚋgraphᚋmodelᚐQuestion(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_updatePracticeTest(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_updatePracticeTestQuestion(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -5888,19 +5817,15 @@ func (ec *executionContext) fieldContext_Mutation_updatePracticeTest(ctx context Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_PracticeTest_id(ctx, field) - case "timestamp": - return ec.fieldContext_PracticeTest_timestamp(ctx, field) - case "studysetIds": - return ec.fieldContext_PracticeTest_studysetIds(ctx, field) - case "questionsCorrect": - return ec.fieldContext_PracticeTest_questionsCorrect(ctx, field) - case "questionsTotal": - return ec.fieldContext_PracticeTest_questionsTotal(ctx, field) - case "questions": - return ec.fieldContext_PracticeTest_questions(ctx, field) + return ec.fieldContext_Question_id(ctx, field) + case "mcq": + return ec.fieldContext_Question_mcq(ctx, field) + case "tfq": + return ec.fieldContext_Question_tfq(ctx, field) + case "frq": + return ec.fieldContext_Question_frq(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type PracticeTest", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Question", field.Name) }, } defer func() { @@ -5910,7 +5835,7 @@ func (ec *executionContext) fieldContext_Mutation_updatePracticeTest(ctx context } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_updatePracticeTest_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_updatePracticeTestQuestion_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -6959,6 +6884,8 @@ func (ec *executionContext) fieldContext_PracticeTest_questions(_ context.Contex IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { + case "id": + return ec.fieldContext_Question_id(ctx, field) case "mcq": return ec.fieldContext_Question_mcq(ctx, field) case "tfq": @@ -7267,8 +7194,6 @@ func (ec *executionContext) fieldContext_Query_term(ctx context.Context, field g return ec.fieldContext_Term_sortOrder(ctx, field) case "progress": return ec.fieldContext_Term_progress(ctx, field) - case "progressHistory": - return ec.fieldContext_Term_progressHistory(ctx, field) case "topConfusionPairs": return ec.fieldContext_Term_topConfusionPairs(ctx, field) case "topReverseConfusionPairs": @@ -8500,6 +8425,50 @@ func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field return fc, nil } +func (ec *executionContext) _Question_id(ctx context.Context, field graphql.CollectedField, obj *model.Question) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Question_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Question_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Question", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Question_mcq(ctx context.Context, field graphql.CollectedField, obj *model.Question) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Question_mcq(ctx, field) if err != nil { @@ -9075,8 +9044,6 @@ func (ec *executionContext) fieldContext_Studyset_terms(_ context.Context, field return ec.fieldContext_Term_sortOrder(ctx, field) case "progress": return ec.fieldContext_Term_progress(ctx, field) - case "progressHistory": - return ec.fieldContext_Term_progressHistory(ctx, field) case "topConfusionPairs": return ec.fieldContext_Term_topConfusionPairs(ctx, field) case "topReverseConfusionPairs": @@ -9828,10 +9795,10 @@ func (ec *executionContext) fieldContext_TFQ_term(_ context.Context, field graph switch field.Name { case "id": return ec.fieldContext_TermATP_id(ctx, field) - case "term": - return ec.fieldContext_TermATP_term(ctx, field) - case "def": - return ec.fieldContext_TermATP_def(ctx, field) + case "termSnapshot": + return ec.fieldContext_TermATP_termSnapshot(ctx, field) + case "defSnapshot": + return ec.fieldContext_TermATP_defSnapshot(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type TermATP", field.Name) }, @@ -10009,10 +9976,10 @@ func (ec *executionContext) fieldContext_TFQ_distractor(_ context.Context, field switch field.Name { case "id": return ec.fieldContext_TermATP_id(ctx, field) - case "term": - return ec.fieldContext_TermATP_term(ctx, field) - case "def": - return ec.fieldContext_TermATP_def(ctx, field) + case "termSnapshot": + return ec.fieldContext_TermATP_termSnapshot(ctx, field) + case "defSnapshot": + return ec.fieldContext_TermATP_defSnapshot(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type TermATP", field.Name) }, @@ -10330,10 +10297,6 @@ func (ec *executionContext) fieldContext_Term_progress(_ context.Context, field return ec.fieldContext_TermProgress_defCorrectCount(ctx, field) case "defIncorrectCount": return ec.fieldContext_TermProgress_defIncorrectCount(ctx, field) - case "termLeitnerSystemBox": - return ec.fieldContext_TermProgress_termLeitnerSystemBox(ctx, field) - case "defLeitnerSystemBox": - return ec.fieldContext_TermProgress_defLeitnerSystemBox(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type TermProgress", field.Name) }, @@ -10341,61 +10304,6 @@ func (ec *executionContext) fieldContext_Term_progress(_ context.Context, field return fc, nil } -func (ec *executionContext) _Term_progressHistory(ctx context.Context, field graphql.CollectedField, obj *model.Term) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Term_progressHistory(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Term().ProgressHistory(rctx, obj) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.([]*model.TermProgressHistory) - fc.Result = res - return ec.marshalOTermProgressHistory2ᚕᚖquizfreelyᚋapiᚋgraphᚋmodelᚐTermProgressHistory(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_Term_progressHistory(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Term", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_TermProgressHistory_id(ctx, field) - case "timestamp": - return ec.fieldContext_TermProgressHistory_timestamp(ctx, field) - case "termCorrectCount": - return ec.fieldContext_TermProgressHistory_termCorrectCount(ctx, field) - case "termIncorrectCount": - return ec.fieldContext_TermProgressHistory_termIncorrectCount(ctx, field) - case "defCorrectCount": - return ec.fieldContext_TermProgressHistory_defCorrectCount(ctx, field) - case "defIncorrectCount": - return ec.fieldContext_TermProgressHistory_defIncorrectCount(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type TermProgressHistory", field.Name) - }, - } - return fc, nil -} - func (ec *executionContext) _Term_topConfusionPairs(ctx context.Context, field graphql.CollectedField, obj *model.Term) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Term_topConfusionPairs(ctx, field) if err != nil { @@ -10812,8 +10720,8 @@ func (ec *executionContext) fieldContext_TermATP_id(_ context.Context, field gra return fc, nil } -func (ec *executionContext) _TermATP_term(ctx context.Context, field graphql.CollectedField, obj *model.TermAtp) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermATP_term(ctx, field) +func (ec *executionContext) _TermATP_termSnapshot(ctx context.Context, field graphql.CollectedField, obj *model.TermAtp) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TermATP_termSnapshot(ctx, field) if err != nil { return graphql.Null } @@ -10826,7 +10734,7 @@ func (ec *executionContext) _TermATP_term(ctx context.Context, field graphql.Col }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Term, nil + return obj.TermSnapshot, nil }) if err != nil { ec.Error(ctx, err) @@ -10843,7 +10751,7 @@ func (ec *executionContext) _TermATP_term(ctx context.Context, field graphql.Col return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TermATP_term(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_TermATP_termSnapshot(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "TermATP", Field: field, @@ -10856,8 +10764,8 @@ func (ec *executionContext) fieldContext_TermATP_term(_ context.Context, field g return fc, nil } -func (ec *executionContext) _TermATP_def(ctx context.Context, field graphql.CollectedField, obj *model.TermAtp) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermATP_def(ctx, field) +func (ec *executionContext) _TermATP_defSnapshot(ctx context.Context, field graphql.CollectedField, obj *model.TermAtp) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TermATP_defSnapshot(ctx, field) if err != nil { return graphql.Null } @@ -10870,7 +10778,7 @@ func (ec *executionContext) _TermATP_def(ctx context.Context, field graphql.Coll }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Def, nil + return obj.DefSnapshot, nil }) if err != nil { ec.Error(ctx, err) @@ -10887,7 +10795,7 @@ func (ec *executionContext) _TermATP_def(ctx context.Context, field graphql.Coll return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TermATP_def(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_TermATP_defSnapshot(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "TermATP", Field: field, @@ -10997,8 +10905,6 @@ func (ec *executionContext) fieldContext_TermConfusionPair_term(_ context.Contex return ec.fieldContext_Term_sortOrder(ctx, field) case "progress": return ec.fieldContext_Term_progress(ctx, field) - case "progressHistory": - return ec.fieldContext_Term_progressHistory(ctx, field) case "topConfusionPairs": return ec.fieldContext_Term_topConfusionPairs(ctx, field) case "topReverseConfusionPairs": @@ -11073,8 +10979,6 @@ func (ec *executionContext) fieldContext_TermConfusionPair_confusedTerm(_ contex return ec.fieldContext_Term_sortOrder(ctx, field) case "progress": return ec.fieldContext_Term_progress(ctx, field) - case "progressHistory": - return ec.fieldContext_Term_progressHistory(ctx, field) case "topConfusionPairs": return ec.fieldContext_Term_topConfusionPairs(ctx, field) case "topReverseConfusionPairs": @@ -11691,8 +11595,8 @@ func (ec *executionContext) fieldContext_TermProgress_defIncorrectCount(_ contex return fc, nil } -func (ec *executionContext) _TermProgress_termLeitnerSystemBox(ctx context.Context, field graphql.CollectedField, obj *model.TermProgress) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermProgress_termLeitnerSystemBox(ctx, field) +func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_id(ctx, field) if err != nil { return graphql.Null } @@ -11705,35 +11609,38 @@ func (ec *executionContext) _TermProgress_termLeitnerSystemBox(ctx context.Conte }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.TermLeitnerSystemBox, nil + return obj.ID, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*int32) + res := resTmp.(*string) fc.Result = res - return ec.marshalOInt2ᚖint32(ctx, field.Selections, res) + return ec.marshalNID2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TermProgress_termLeitnerSystemBox(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_User_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "TermProgress", + Object: "User", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") + return nil, errors.New("field of type ID does not have child fields") }, } return fc, nil } -func (ec *executionContext) _TermProgress_defLeitnerSystemBox(ctx context.Context, field graphql.CollectedField, obj *model.TermProgress) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermProgress_defLeitnerSystemBox(ctx, field) +func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_username(ctx, field) if err != nil { return graphql.Null } @@ -11746,7 +11653,7 @@ func (ec *executionContext) _TermProgress_defLeitnerSystemBox(ctx context.Contex }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.DefLeitnerSystemBox, nil + return obj.Username, nil }) if err != nil { ec.Error(ctx, err) @@ -11755,26 +11662,26 @@ func (ec *executionContext) _TermProgress_defLeitnerSystemBox(ctx context.Contex if resTmp == nil { return graphql.Null } - res := resTmp.(*int32) + res := resTmp.(*string) fc.Result = res - return ec.marshalOInt2ᚖint32(ctx, field.Selections, res) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TermProgress_defLeitnerSystemBox(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_User_username(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "TermProgress", + Object: "User", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } -func (ec *executionContext) _TermProgressHistory_id(ctx context.Context, field graphql.CollectedField, obj *model.TermProgressHistory) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermProgressHistory_id(ctx, field) +func (ec *executionContext) _User_displayName(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_displayName(ctx, field) if err != nil { return graphql.Null } @@ -11787,7 +11694,7 @@ func (ec *executionContext) _TermProgressHistory_id(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.ID, nil + return obj.DisplayName, nil }) if err != nil { ec.Error(ctx, err) @@ -11801,24 +11708,24 @@ func (ec *executionContext) _TermProgressHistory_id(ctx context.Context, field g } res := resTmp.(*string) fc.Result = res - return ec.marshalNID2ᚖstring(ctx, field.Selections, res) + return ec.marshalNString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TermProgressHistory_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_User_displayName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "TermProgressHistory", + Object: "User", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ID does not have child fields") + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } -func (ec *executionContext) _TermProgressHistory_timestamp(ctx context.Context, field graphql.CollectedField, obj *model.TermProgressHistory) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermProgressHistory_timestamp(ctx, field) +func (ec *executionContext) _User_studysets(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_studysets(ctx, field) if err != nil { return graphql.Null } @@ -11831,7 +11738,7 @@ func (ec *executionContext) _TermProgressHistory_timestamp(ctx context.Context, }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Timestamp, nil + return ec.resolvers.User().Studysets(rctx, obj, fc.Args["first"].(*int32), fc.Args["after"].(*string), fc.Args["last"].(*int32), fc.Args["before"].(*string), fc.Args["includePrivate"].(*bool)) }) if err != nil { ec.Error(ctx, err) @@ -11843,26 +11750,43 @@ func (ec *executionContext) _TermProgressHistory_timestamp(ctx context.Context, } return graphql.Null } - res := resTmp.(*string) + res := resTmp.(*model.StudysetConnection) fc.Result = res - return ec.marshalNString2ᚖstring(ctx, field.Selections, res) + return ec.marshalNStudysetConnection2ᚖquizfreelyᚋapiᚋgraphᚋmodelᚐStudysetConnection(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TermProgressHistory_timestamp(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_User_studysets(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "TermProgressHistory", + Object: "User", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + switch field.Name { + case "edges": + return ec.fieldContext_StudysetConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_StudysetConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type StudysetConnection", field.Name) }, } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_User_studysets_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } return fc, nil } -func (ec *executionContext) _TermProgressHistory_termCorrectCount(ctx context.Context, field graphql.CollectedField, obj *model.TermProgressHistory) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermProgressHistory_termCorrectCount(ctx, field) +func (ec *executionContext) _User_studysetCount(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_studysetCount(ctx, field) if err != nil { return graphql.Null } @@ -11875,383 +11799,29 @@ func (ec *executionContext) _TermProgressHistory_termCorrectCount(ctx context.Co }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.TermCorrectCount, nil + return ec.resolvers.User().StudysetCount(rctx, obj, fc.Args["includePrivate"].(*bool)) }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*int32) + res := resTmp.(int32) fc.Result = res - return ec.marshalOInt2ᚖint32(ctx, field.Selections, res) + return ec.marshalNInt2int32(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_TermProgressHistory_termCorrectCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_User_studysetCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "TermProgressHistory", + Object: "User", Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _TermProgressHistory_termIncorrectCount(ctx context.Context, field graphql.CollectedField, obj *model.TermProgressHistory) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermProgressHistory_termIncorrectCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.TermIncorrectCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*int32) - fc.Result = res - return ec.marshalOInt2ᚖint32(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_TermProgressHistory_termIncorrectCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TermProgressHistory", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _TermProgressHistory_defCorrectCount(ctx context.Context, field graphql.CollectedField, obj *model.TermProgressHistory) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermProgressHistory_defCorrectCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DefCorrectCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*int32) - fc.Result = res - return ec.marshalOInt2ᚖint32(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_TermProgressHistory_defCorrectCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TermProgressHistory", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _TermProgressHistory_defIncorrectCount(ctx context.Context, field graphql.CollectedField, obj *model.TermProgressHistory) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_TermProgressHistory_defIncorrectCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DefIncorrectCount, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*int32) - fc.Result = res - return ec.marshalOInt2ᚖint32(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_TermProgressHistory_defIncorrectCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "TermProgressHistory", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_User_id(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ID, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalNID2ᚖstring(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_User_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "User", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ID does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_User_username(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Username, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_User_username(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "User", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _User_displayName(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_User_displayName(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DisplayName, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalNString2ᚖstring(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_User_displayName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "User", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _User_studysets(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_User_studysets(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.User().Studysets(rctx, obj, fc.Args["first"].(*int32), fc.Args["after"].(*string), fc.Args["last"].(*int32), fc.Args["before"].(*string), fc.Args["includePrivate"].(*bool)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.StudysetConnection) - fc.Result = res - return ec.marshalNStudysetConnection2ᚖquizfreelyᚋapiᚋgraphᚋmodelᚐStudysetConnection(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_User_studysets(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "User", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "edges": - return ec.fieldContext_StudysetConnection_edges(ctx, field) - case "pageInfo": - return ec.fieldContext_StudysetConnection_pageInfo(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type StudysetConnection", field.Name) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_User_studysets_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - -func (ec *executionContext) _User_studysetCount(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_User_studysetCount(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.User().StudysetCount(rctx, obj, fc.Args["includePrivate"].(*bool)) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int32) - fc.Result = res - return ec.marshalNInt2int32(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_User_studysetCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "User", - Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Int does not have child fields") }, @@ -14764,7 +14334,7 @@ func (ec *executionContext) unmarshalInputTermATPInput(ctx context.Context, obj asMap[k] = v } - fieldsInOrder := [...]string{"id", "term", "def"} + fieldsInOrder := [...]string{"id", "termSnapshot", "defSnapshot"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -14778,20 +14348,20 @@ func (ec *executionContext) unmarshalInputTermATPInput(ctx context.Context, obj return it, err } it.ID = data - case "term": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("term")) + case "termSnapshot": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("termSnapshot")) data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Term = data - case "def": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("def")) + it.TermSnapshot = data + case "defSnapshot": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("defSnapshot")) data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Def = data + it.DefSnapshot = data } } @@ -14908,7 +14478,7 @@ func (ec *executionContext) unmarshalInputTermProgressInput(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"termId", "termReviewedAt", "defReviewedAt", "termLeitnerSystemBox", "defLeitnerSystemBox", "termCorrectIncrease", "termIncorrectIncrease", "defCorrectIncrease", "defIncorrectIncrease"} + fieldsInOrder := [...]string{"termId", "termReviewedAt", "defReviewedAt", "termCorrectIncrease", "termIncorrectIncrease", "defCorrectIncrease", "defIncorrectIncrease"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -14936,20 +14506,6 @@ func (ec *executionContext) unmarshalInputTermProgressInput(ctx context.Context, return it, err } it.DefReviewedAt = data - case "termLeitnerSystemBox": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("termLeitnerSystemBox")) - data, err := ec.unmarshalOInt2ᚖint32(ctx, v) - if err != nil { - return it, err - } - it.TermLeitnerSystemBox = data - case "defLeitnerSystemBox": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("defLeitnerSystemBox")) - data, err := ec.unmarshalOInt2ᚖint32(ctx, v) - if err != nil { - return it, err - } - it.DefLeitnerSystemBox = data case "termCorrectIncrease": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("termCorrectIncrease")) data, err := ec.unmarshalOInt2ᚖint32(ctx, v) @@ -15680,9 +15236,9 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_recordPracticeTest(ctx, field) }) - case "updatePracticeTest": + case "updatePracticeTestQuestion": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_updatePracticeTest(ctx, field) + return ec._Mutation_updatePracticeTestQuestion(ctx, field) }) case "createFolder": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -16434,6 +15990,11 @@ func (ec *executionContext) _Question(ctx context.Context, sel ast.SelectionSet, switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Question") + case "id": + out.Values[i] = ec._Question_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "mcq": out.Values[i] = ec._Question_mcq(ctx, field, obj) case "tfq": @@ -17077,39 +16638,6 @@ func (ec *executionContext) _Term(ctx context.Context, sel ast.SelectionSet, obj continue } - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "progressHistory": - field := field - - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Term_progressHistory(ctx, field, obj) - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue - } - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "topConfusionPairs": field := field @@ -17322,13 +16850,13 @@ func (ec *executionContext) _TermATP(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { out.Invalids++ } - case "term": - out.Values[i] = ec._TermATP_term(ctx, field, obj) + case "termSnapshot": + out.Values[i] = ec._TermATP_termSnapshot(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "def": - out.Values[i] = ec._TermATP_def(ctx, field, obj) + case "defSnapshot": + out.Values[i] = ec._TermATP_defSnapshot(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -17526,62 +17054,6 @@ func (ec *executionContext) _TermProgress(ctx context.Context, sel ast.Selection if out.Values[i] == graphql.Null { out.Invalids++ } - case "termLeitnerSystemBox": - out.Values[i] = ec._TermProgress_termLeitnerSystemBox(ctx, field, obj) - case "defLeitnerSystemBox": - out.Values[i] = ec._TermProgress_defLeitnerSystemBox(ctx, field, obj) - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.deferred, int32(len(deferred))) - - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } - - return out -} - -var termProgressHistoryImplementors = []string{"TermProgressHistory"} - -func (ec *executionContext) _TermProgressHistory(ctx context.Context, sel ast.SelectionSet, obj *model.TermProgressHistory) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, termProgressHistoryImplementors) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("TermProgressHistory") - case "id": - out.Values[i] = ec._TermProgressHistory_id(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "timestamp": - out.Values[i] = ec._TermProgressHistory_timestamp(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "termCorrectCount": - out.Values[i] = ec._TermProgressHistory_termCorrectCount(ctx, field, obj) - case "termIncorrectCount": - out.Values[i] = ec._TermProgressHistory_termIncorrectCount(ctx, field, obj) - case "defCorrectCount": - out.Values[i] = ec._TermProgressHistory_defCorrectCount(ctx, field, obj) - case "defIncorrectCount": - out.Values[i] = ec._TermProgressHistory_defIncorrectCount(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -19369,6 +18841,13 @@ func (ec *executionContext) marshalOPracticeTest2ᚖquizfreelyᚋapiᚋgraphᚋm return ec._PracticeTest(ctx, sel, v) } +func (ec *executionContext) marshalOQuestion2ᚖquizfreelyᚋapiᚋgraphᚋmodelᚐQuestion(ctx context.Context, sel ast.SelectionSet, v *model.Question) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Question(ctx, sel, v) +} + func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v any) (*string, error) { if v == nil { return nil, nil @@ -19725,54 +19204,6 @@ func (ec *executionContext) marshalOTermProgress2ᚖquizfreelyᚋapiᚋgraphᚋm return ec._TermProgress(ctx, sel, v) } -func (ec *executionContext) marshalOTermProgressHistory2ᚕᚖquizfreelyᚋapiᚋgraphᚋmodelᚐTermProgressHistory(ctx context.Context, sel ast.SelectionSet, v []*model.TermProgressHistory) graphql.Marshaler { - if v == nil { - return graphql.Null - } - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalOTermProgressHistory2ᚖquizfreelyᚋapiᚋgraphᚋmodelᚐTermProgressHistory(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() - - return ret -} - -func (ec *executionContext) marshalOTermProgressHistory2ᚖquizfreelyᚋapiᚋgraphᚋmodelᚐTermProgressHistory(ctx context.Context, sel ast.SelectionSet, v *model.TermProgressHistory) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return ec._TermProgressHistory(ctx, sel, v) -} - func (ec *executionContext) marshalOUser2ᚖquizfreelyᚋapiᚋgraphᚋmodelᚐUser(ctx context.Context, sel ast.SelectionSet, v *model.User) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graph/loader/loader.go b/graph/loader/loader.go index 2c27620..d0253b7 100644 --- a/graph/loader/loader.go +++ b/graph/loader/loader.go @@ -239,7 +239,6 @@ func (dr *dataReader) getTermsProgress(ctx context.Context, termIDs []string) ([ to_char(tp.def_first_reviewed_at, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as def_first_reviewed_at, to_char(tp.def_last_reviewed_at, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as def_last_reviewed_at, tp.def_review_count, - tp.term_leitner_system_box, tp.def_leitner_system_box, tp.term_correct_count, tp.term_incorrect_count, tp.def_correct_count, tp.def_incorrect_count FROM unnest($1::uuid[]) WITH ORDINALITY AS input(term_id, og_order) @@ -264,8 +263,6 @@ ORDER BY input.og_order`, DefFirstReviewedAt *string `db:"def_first_reviewed_at"` DefLastReviewedAt *string `db:"def_last_reviewed_at"` DefReviewCount *int32 `db:"def_review_count"` - TermLeitnerSystemBox *int32 `db:"term_leitner_system_box"` - DefLeitnerSystemBox *int32 `db:"def_leitner_system_box"` TermCorrectCount *int32 `db:"term_correct_count"` TermIncorrectCount *int32 `db:"term_incorrect_count"` DefCorrectCount *int32 `db:"def_correct_count"` @@ -291,8 +288,6 @@ ORDER BY input.og_order`, DefFirstReviewedAt: tp.DefFirstReviewedAt, DefLastReviewedAt: tp.DefLastReviewedAt, DefReviewCount: tp.DefReviewCount, - TermLeitnerSystemBox: tp.TermLeitnerSystemBox, - DefLeitnerSystemBox: tp.DefLeitnerSystemBox, } if tp.TermCorrectCount != nil { modelTp.TermCorrectCount = *tp.TermCorrectCount @@ -313,67 +308,6 @@ ORDER BY input.og_order`, return termsProgress, nil } -func (dr *dataReader) getTermsProgressHistory(ctx context.Context, termIDs []string) ([][]*model.TermProgressHistory, []error) { - authedUser := auth.AuthedUserContext(ctx) - if authedUser == nil || authedUser.ID == nil { - return nil, nil - } - - type dbTermProgressHistory struct { - ID *string `db:"id"` - TermID *string `db:"term_id"` - Timestamp *string `db:"timestamp"` - TermCorrectCount *int32 `db:"term_correct_count"` - TermIncorrectCount *int32 `db:"term_incorrect_count"` - DefCorrectCount *int32 `db:"def_correct_count"` - DefIncorrectCount *int32 `db:"def_incorrect_count"` - } - - var dbHistory []*dbTermProgressHistory - err := pgxscan.Select( - ctx, - dr.db, - &dbHistory, - `SELECT tph.id, - tph.term_id, - to_char(tph.timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as timestamp, - tph.term_correct_count, - tph.term_incorrect_count, - tph.def_correct_count, - tph.def_incorrect_count -FROM unnest($1::uuid[]) WITH ORDINALITY AS input(term_id, og_order) -LEFT JOIN term_progress_history tph - ON tph.term_id = input.term_id - AND tph.user_id = $2 -ORDER BY input.og_order ASC, tph.timestamp DESC`, - termIDs, - authedUser.ID, - ) - if err != nil { - return nil, []error{err} - } - - grouped := make(map[string][]*model.TermProgressHistory) - for _, dbTph := range dbHistory { - if dbTph.ID != nil && dbTph.TermID != nil { - grouped[*dbTph.TermID] = append(grouped[*dbTph.TermID], &model.TermProgressHistory{ - ID: dbTph.ID, - Timestamp: dbTph.Timestamp, - TermCorrectCount: dbTph.TermCorrectCount, - TermIncorrectCount: dbTph.TermIncorrectCount, - DefCorrectCount: dbTph.DefCorrectCount, - DefIncorrectCount: dbTph.DefIncorrectCount, - }) - } - } - - orderedProgressHistory := make([][]*model.TermProgressHistory, len(termIDs)) - for i, id := range termIDs { - orderedProgressHistory[i] = grouped[id] - } - - return orderedProgressHistory, nil -} func (dr *dataReader) getTermsTopConfusionPairs(ctx context.Context, termIDs []string) ([][]*model.TermConfusionPair, []error) { authedUser := auth.AuthedUserContext(ctx) @@ -520,12 +454,11 @@ func (dr *dataReader) getPracticeTestsByStudysetIDs(ctx context.Context, studyse } type dbPracticeTest struct { - ID *string `db:"id"` - Timestamp *string `db:"timestamp"` - QuestionsCorrect *int32 `db:"questions_correct"` - QuestionsTotal *int32 `db:"questions_total"` - Questions []*model.Question `db:"questions"` - StudysetID *string `db:"studyset_id"` + ID *string `db:"id"` + Timestamp *string `db:"timestamp"` + QuestionsCorrect *int32 `db:"questions_correct"` + QuestionsTotal *int32 `db:"questions_total"` + StudysetID *string `db:"studyset_id"` } var dbPracticeTests []*dbPracticeTest @@ -538,7 +471,6 @@ func (dr *dataReader) getPracticeTestsByStudysetIDs(ctx context.Context, studyse to_char(pt.timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as timestamp, pt.questions_correct, pt.questions_total, - pt.questions, input.studyset_id FROM unnest($1::uuid[]) WITH ORDINALITY AS input(studyset_id, og_order) JOIN practice_test_studysets pts ON pts.studyset_id = input.studyset_id @@ -560,7 +492,6 @@ ORDER BY input.og_order ASC, pt.timestamp DESC`, Timestamp: pt.Timestamp, QuestionsCorrect: pt.QuestionsCorrect, QuestionsTotal: pt.QuestionsTotal, - Questions: pt.Questions, }) } } @@ -657,12 +588,11 @@ func (dr *dataReader) getPracticeTestsByTermIDs(ctx context.Context, termIDs []s } type dbPracticeTest struct { - ID *string `db:"id"` - Timestamp *string `db:"timestamp"` - QuestionsCorrect *int32 `db:"questions_correct"` - QuestionsTotal *int32 `db:"questions_total"` - Questions []*model.Question `db:"questions"` - TermID *string `db:"term_id"` + ID *string `db:"id"` + Timestamp *string `db:"timestamp"` + QuestionsCorrect *int32 `db:"questions_correct"` + QuestionsTotal *int32 `db:"questions_total"` + TermID *string `db:"term_id"` } var dbPracticeTests []*dbPracticeTest @@ -674,15 +604,10 @@ func (dr *dataReader) getPracticeTestsByTermIDs(ctx context.Context, termIDs []s to_char(pt.timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as timestamp, pt.questions_correct, pt.questions_total, - pt.questions, input.term_id FROM unnest($1::uuid[]) WITH ORDINALITY AS input(term_id, og_order) -JOIN ( - SELECT practice_test_id, term_id FROM practice_test_question_terms - UNION - SELECT practice_test_id, term_id FROM practice_test_distractor_terms -) mapping ON mapping.term_id = input.term_id -JOIN practice_tests pt ON pt.id = mapping.practice_test_id +JOIN practice_test_questions ptq ON ptq.term_id = input.term_id +JOIN practice_tests pt ON pt.id = ptq.practice_test_id WHERE pt.user_id = $2 GROUP BY input.term_id, input.og_order, pt.id ORDER BY input.og_order ASC, pt.timestamp DESC`, @@ -701,7 +626,6 @@ ORDER BY input.og_order ASC, pt.timestamp DESC`, Timestamp: pt.Timestamp, QuestionsCorrect: pt.QuestionsCorrect, QuestionsTotal: pt.QuestionsTotal, - Questions: pt.Questions, }) } } @@ -796,7 +720,6 @@ type Loaders struct { TermByStudysetIDLoader *dataloadgen.Loader[string, []*model.Term] TermsCountByStudysetIDLoader *dataloadgen.Loader[string, *int32] TermProgressLoader *dataloadgen.Loader[string, *model.TermProgress] - TermProgressHistoryLoader *dataloadgen.Loader[string, []*model.TermProgressHistory] TermTopConfusionPairsLoader *dataloadgen.Loader[string, []*model.TermConfusionPair] TermTopReverseConfusionPairsLoader *dataloadgen.Loader[string, []*model.TermConfusionPair] PracticeTestByStudysetIDLoader *dataloadgen.Loader[string, []*model.PracticeTest] @@ -818,7 +741,6 @@ func NewLoaders(db *pgxpool.Pool, usercontentBaseURL *string) *Loaders { TermByStudysetIDLoader: dataloadgen.NewLoader(dr.getTermsByStudysetIDs, dataloadgen.WithWait(time.Millisecond)), TermsCountByStudysetIDLoader: dataloadgen.NewLoader(dr.getTermsCountByStudysetIDs, dataloadgen.WithWait(time.Millisecond)), TermProgressLoader: dataloadgen.NewLoader(dr.getTermsProgress, dataloadgen.WithWait(time.Millisecond)), - TermProgressHistoryLoader: dataloadgen.NewLoader(dr.getTermsProgressHistory, dataloadgen.WithWait(time.Millisecond)), TermTopConfusionPairsLoader: dataloadgen.NewLoader(dr.getTermsTopConfusionPairs, dataloadgen.WithWait(time.Millisecond)), TermTopReverseConfusionPairsLoader: dataloadgen.NewLoader(dr.getTermsTopReverseConfusionPairs, dataloadgen.WithWait(time.Millisecond)), PracticeTestByStudysetIDLoader: dataloadgen.NewLoader(dr.getPracticeTestsByStudysetIDs, dataloadgen.WithWait(time.Millisecond)), @@ -901,17 +823,6 @@ func GetTermsProgress(ctx context.Context, termIDs []string) ([]*model.TermProgr return loaders.TermProgressLoader.LoadAll(ctx, termIDs) } -// GetTermProgressHistory returns a single term's progress history -func GetTermProgressHistory(ctx context.Context, termID string) ([]*model.TermProgressHistory, error) { - loaders := For(ctx) - return loaders.TermProgressHistoryLoader.Load(ctx, termID) -} - -// GetTermsProgressHistory returns many terms' progress histories -func GetTermsProgressHistory(ctx context.Context, termIDs []string) ([][]*model.TermProgressHistory, error) { - loaders := For(ctx) - return loaders.TermProgressHistoryLoader.LoadAll(ctx, termIDs) -} // GetTermTopReverseConfusionPairs returns a single term's confusion pairs func GetTermTopReverseConfusionPairs(ctx context.Context, termID string) ([]*model.TermConfusionPair, error) { diff --git a/graph/model/models_gen.go b/graph/model/models_gen.go index 8616f9a..e9221b5 100644 --- a/graph/model/models_gen.go +++ b/graph/model/models_gen.go @@ -139,9 +139,10 @@ type Query struct { } type Question struct { - Mcq *Mcq `json:"mcq,omitempty"` - Tfq *Tfq `json:"tfq,omitempty"` - Frq *Frq `json:"frq,omitempty"` + ID string `json:"id"` + Mcq *Mcq `json:"mcq,omitempty"` + Tfq *Tfq `json:"tfq,omitempty"` + Frq *Frq `json:"frq,omitempty"` } type QuestionInput struct { @@ -183,15 +184,15 @@ type TFQInput struct { } type TermAtp struct { - ID string `json:"id"` - Term string `json:"term"` - Def string `json:"def"` + ID string `json:"id"` + TermSnapshot string `json:"termSnapshot"` + DefSnapshot string `json:"defSnapshot"` } type TermATPInput struct { - ID string `json:"id"` - Term string `json:"term"` - Def string `json:"def"` + ID string `json:"id"` + TermSnapshot string `json:"termSnapshot"` + DefSnapshot string `json:"defSnapshot"` } type TermConfusionPairInput struct { @@ -213,8 +214,6 @@ type TermProgressInput struct { TermID string `json:"termId"` TermReviewedAt *string `json:"termReviewedAt,omitempty"` DefReviewedAt *string `json:"defReviewedAt,omitempty"` - TermLeitnerSystemBox *int32 `json:"termLeitnerSystemBox,omitempty"` - DefLeitnerSystemBox *int32 `json:"defLeitnerSystemBox,omitempty"` TermCorrectIncrease *int32 `json:"termCorrectIncrease,omitempty"` TermIncorrectIncrease *int32 `json:"termIncorrectIncrease,omitempty"` DefCorrectIncrease *int32 `json:"defCorrectIncrease,omitempty"` diff --git a/graph/model/practice_test_model.go b/graph/model/practice_test_model.go index d0e2922..73e3584 100644 --- a/graph/model/practice_test_model.go +++ b/graph/model/practice_test_model.go @@ -7,5 +7,18 @@ type PracticeTest struct { Timestamp *string `json:"timestamp,omitempty" db:"timestamp"` QuestionsCorrect *int32 `json:"questionsCorrect,omitempty" db:"questions_correct"` QuestionsTotal *int32 `json:"questionsTotal,omitempty" db:"questions_total"` - Questions []*Question `json:"questions,omitempty" db:"questions"` + Questions []*Question `json:"questions,omitempty"` +} + +type QuestionRow struct { + ID string `db:"id"` + PracticeTestID string `db:"practice_test_id"` + TermID string `db:"term_id"` + TermSnapshot string `db:"term_snapshot"` + DefSnapshot string `db:"def_snapshot"` + Type string `db:"type"` + AnswerWith AnswerWith `db:"answer_with"` + Correct bool `db:"correct"` + Position int32 `db:"position"` + Data []byte `db:"data"` } diff --git a/graph/model/progress_history.go b/graph/model/progress_history.go deleted file mode 100644 index 939179e..0000000 --- a/graph/model/progress_history.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -type TermProgressHistory struct { - ID *string `json:"id,omitempty" db:"id"` - TermID *string `json:"termId,omitempty" db:"term_id"` - Timestamp *string `json:"timestamp,omitempty" db:"timestamp"` - TermCorrectCount *int32 `json:"termCorrectCount,omitempty" db:"term_correct_count"` - TermIncorrectCount *int32 `json:"termIncorrectCount,omitempty" db:"term_incorrect_count"` - DefCorrectCount *int32 `json:"defCorrectCount,omitempty" db:"def_correct_count"` - DefIncorrectCount *int32 `json:"defIncorrectCount,omitempty" db:"def_incorrect_count"` -} diff --git a/graph/model/term_progress.go b/graph/model/term_progress.go index 45c6702..41de44a 100644 --- a/graph/model/term_progress.go +++ b/graph/model/term_progress.go @@ -12,6 +12,4 @@ type TermProgress struct { TermIncorrectCount int32 `json:"termIncorrectCount" db:"term_incorrect_count"` DefCorrectCount int32 `json:"defCorrectCount" db:"def_correct_count"` DefIncorrectCount int32 `json:"defIncorrectCount" db:"def_incorrect_count"` - TermLeitnerSystemBox *int32 `json:"termLeitnerSystemBox,omitempty" db:"term_leitner_system_box"` - DefLeitnerSystemBox *int32 `json:"defLeitnerSystemBox,omitempty" db:"def_leitner_system_box"` } diff --git a/graph/mutation.graphqls b/graph/mutation.graphqls index 67beaa3..748b3a4 100644 --- a/graph/mutation.graphqls +++ b/graph/mutation.graphqls @@ -9,7 +9,7 @@ type Mutation { updateTermProgress(termProgress: [TermProgressInput!]!): [TermProgress!] recordConfusedTerms(confusedTerms: [TermConfusionPairInput]): Boolean recordPracticeTest(input: PracticeTestInput!): PracticeTest - updatePracticeTest(id: ID!, input: PracticeTestInput!): PracticeTest + updatePracticeTestQuestion(id: ID!, correct: Boolean!, userMarkedCorrect: Boolean): Question createFolder(name: String!): Folder renameFolder(id: ID!, name: String!): Folder deleteFolder(id: ID!): ID @@ -33,8 +33,8 @@ input QuestionInput @oneOf { } input TermATPInput { id: ID! - term: String! - def: String! + termSnapshot: String! + defSnapshot: String! } input MCQInput { term: TermATPInput! diff --git a/graph/query.graphqls b/graph/query.graphqls index 00bc6d5..32980f4 100644 --- a/graph/query.graphqls +++ b/graph/query.graphqls @@ -42,14 +42,15 @@ type PracticeTest { questions: [Question!]! } type Question { + id: ID! mcq: MCQ tfq: TFQ frq: FRQ } type TermATP { id: ID! - term: String! - def: String! + termSnapshot: String! + defSnapshot: String! } type MCQ { term: TermATP! diff --git a/graph/resolver/mutation.resolvers.go b/graph/resolver/mutation.resolvers.go index a62ce76..6e4e9bb 100644 --- a/graph/resolver/mutation.resolvers.go +++ b/graph/resolver/mutation.resolvers.go @@ -6,6 +6,7 @@ package resolver import ( "context" + "encoding/json" "errors" "fmt" "quizfreely/api/auth" @@ -385,16 +386,16 @@ func (r *mutationResolver) UpdateTermProgress(ctx context.Context, termProgress // ---- Build bulk insert ---- valueStrings := make([]string, 0, len(termProgress)) - valueArgs := make([]interface{}, 0, len(termProgress)*10) + valueArgs := make([]interface{}, 0, len(termProgress)*8) termIDs := make([]string, len(termProgress)) for i, p := range termProgress { termIDs[i] = p.TermID - // Each row has 10 parameters (adjust if needed) - base := i*10 + 1 + // Each row has 8 parameters + base := i*8 + 1 valueStrings = append(valueStrings, fmt.Sprintf( - "($%d::uuid,$%d::uuid,$%d::timestamptz,$%d::timestamptz,CASE WHEN $%d IS NOT NULL THEN 1 ELSE 0 END,$%d::timestamptz,$%d::timestamptz,CASE WHEN $%d IS NOT NULL THEN 1 ELSE 0 END,$%d::int,$%d::int,COALESCE($%d::int,0),COALESCE($%d::int,0),COALESCE($%d::int,0),COALESCE($%d::int,0))", - base, base+1, base+2, base+2, base+2, base+3, base+3, base+3, base+4, base+5, base+6, base+7, base+8, base+9, + "($%d::uuid,$%d::uuid,$%d::timestamptz,$%d::timestamptz,CASE WHEN $%d IS NOT NULL THEN 1 ELSE 0 END,$%d::timestamptz,$%d::timestamptz,CASE WHEN $%d IS NOT NULL THEN 1 ELSE 0 END,COALESCE($%d::int,0),COALESCE($%d::int,0),COALESCE($%d::int,0),COALESCE($%d::int,0))", + base, base+1, base+2, base+2, base+2, base+3, base+3, base+3, base+4, base+5, base+6, base+7, )) valueArgs = append(valueArgs, @@ -402,8 +403,6 @@ func (r *mutationResolver) UpdateTermProgress(ctx context.Context, termProgress authedUser.ID, p.TermReviewedAt, p.DefReviewedAt, - p.TermLeitnerSystemBox, - p.DefLeitnerSystemBox, p.TermCorrectIncrease, p.TermIncorrectIncrease, p.DefCorrectIncrease, @@ -418,7 +417,6 @@ func (r *mutationResolver) UpdateTermProgress(ctx context.Context, termProgress term_review_count, def_first_reviewed_at, def_last_reviewed_at, def_review_count, - term_leitner_system_box, def_leitner_system_box, term_correct_count, term_incorrect_count, def_correct_count, def_incorrect_count ) @@ -427,7 +425,6 @@ func (r *mutationResolver) UpdateTermProgress(ctx context.Context, termProgress v.term_review_count::int, v.def_first_reviewed_at::timestamptz, v.def_last_reviewed_at::timestamptz, v.def_review_count::int, - v.term_leitner_system_box::int, v.def_leitner_system_box::int, v.term_correct_count::int, v.term_incorrect_count::int, v.def_correct_count::int, v.def_incorrect_count::int FROM (VALUES %s) AS v( @@ -436,7 +433,6 @@ func (r *mutationResolver) UpdateTermProgress(ctx context.Context, termProgress term_review_count, def_first_reviewed_at, def_last_reviewed_at, def_review_count, - term_leitner_system_box, def_leitner_system_box, term_correct_count, term_incorrect_count, def_correct_count, def_incorrect_count ) @@ -446,8 +442,6 @@ func (r *mutationResolver) UpdateTermProgress(ctx context.Context, termProgress ON CONFLICT (term_id, user_id) DO UPDATE SET term_last_reviewed_at = COALESCE(EXCLUDED.term_last_reviewed_at, term_progress.term_last_reviewed_at), def_last_reviewed_at = COALESCE(EXCLUDED.def_last_reviewed_at, term_progress.def_last_reviewed_at), - term_leitner_system_box = EXCLUDED.term_leitner_system_box, - def_leitner_system_box = EXCLUDED.def_leitner_system_box, term_review_count = term_progress.term_review_count + (CASE WHEN EXCLUDED.term_last_reviewed_at IS NOT NULL THEN 1 ELSE 0 END), def_review_count = term_progress.def_review_count + @@ -463,8 +457,7 @@ func (r *mutationResolver) UpdateTermProgress(ctx context.Context, termProgress to_char(term_progress.def_first_reviewed_at, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as def_first_reviewed_at, to_char(term_progress.def_last_reviewed_at, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as def_last_reviewed_at, term_progress.def_review_count, - term_progress.term_leitner_system_box, term_progress.def_leitner_system_box, - term_progress.term_correct_count, term_progress.term_incorrect_count, + term_progress.term_correct_count, term_incorrect_count, term_progress.def_correct_count, term_progress.def_incorrect_count `, strings.Join(valueStrings, ",")) @@ -477,33 +470,6 @@ func (r *mutationResolver) UpdateTermProgress(ctx context.Context, termProgress return nil, fmt.Errorf("some terms not found or not accessible") } - // ---- Bulk insert into history ---- - if len(results) > 0 { - histVals := make([]string, 0, len(results)) - histArgs := make([]interface{}, 0, len(results)*6) - for i, tp := range results { - base := i*6 + 1 - histVals = append(histVals, fmt.Sprintf("($%d,$%d,$%d,$%d,$%d,$%d)", base, base+1, base+2, base+3, base+4, base+5)) - histArgs = append(histArgs, - authedUser.ID, - termIDs[i], - tp.TermCorrectCount, - tp.TermIncorrectCount, - tp.DefCorrectCount, - tp.DefIncorrectCount, - ) - } - histQuery := fmt.Sprintf(` - INSERT INTO term_progress_history ( - user_id, term_id, term_correct_count, term_incorrect_count, - def_correct_count, def_incorrect_count - ) VALUES %s`, strings.Join(histVals, ",")) - - if _, err := tx.Exec(ctx, histQuery, histArgs...); err != nil { - return nil, fmt.Errorf("failed to insert into term_progress_history: %w", err) - } - } - if err := tx.Commit(ctx); err != nil { return nil, fmt.Errorf("failed to commit transaction: %w", err) } @@ -608,49 +574,92 @@ func (r *mutationResolver) RecordPracticeTest(ctx context.Context, input model.P nowStr := time.Now().Format(time.RFC3339) termProgressMap := make(map[string]*model.TermProgressInput) - var questionTermIDs []string - var distractorTermIDs []string + var questionRows []model.QuestionRow + var allTermIDs []string - for _, q := range input.Questions { + for i, q := range input.Questions { if q == nil { continue } var termID string + var termSnapshot, defSnapshot string + var qType string var answerWith model.AnswerWith correct := false + var data interface{} if q.Mcq != nil && q.Mcq.Term != nil { termID = q.Mcq.Term.ID - questionTermIDs = append(questionTermIDs, termID) + termSnapshot = q.Mcq.Term.TermSnapshot + defSnapshot = q.Mcq.Term.DefSnapshot + qType = "MCQ" + answerWith = q.Mcq.AnswerWith + correct = q.Mcq.Correct + data = map[string]interface{}{ + "distractors": q.Mcq.Distractors, + "correctChoiceIndex": q.Mcq.CorrectChoiceIndex, + "answeredIndex": q.Mcq.AnsweredIndex, + } + allTermIDs = append(allTermIDs, termID) for _, d := range q.Mcq.Distractors { if d != nil { - distractorTermIDs = append(distractorTermIDs, d.ID) + allTermIDs = append(allTermIDs, d.ID) } } - answerWith = q.Mcq.AnswerWith - correct = q.Mcq.Correct } else if q.Tfq != nil && q.Tfq.Term != nil { termID = q.Tfq.Term.ID - questionTermIDs = append(questionTermIDs, termID) - if q.Tfq.Distractor != nil { - distractorTermIDs = append(distractorTermIDs, q.Tfq.Distractor.ID) - } + termSnapshot = q.Tfq.Term.TermSnapshot + defSnapshot = q.Tfq.Term.DefSnapshot + qType = "TFQ" answerWith = q.Tfq.AnswerWith correct = q.Tfq.Correct + data = map[string]interface{}{ + "answeredBool": q.Tfq.AnsweredBool, + "distractor": q.Tfq.Distractor, + } + allTermIDs = append(allTermIDs, termID) + if q.Tfq.Distractor != nil { + allTermIDs = append(allTermIDs, q.Tfq.Distractor.ID) + } } else if q.Frq != nil && q.Frq.Term != nil { termID = q.Frq.Term.ID - questionTermIDs = append(questionTermIDs, termID) + termSnapshot = q.Frq.Term.TermSnapshot + defSnapshot = q.Frq.Term.DefSnapshot + qType = "FRQ" answerWith = q.Frq.AnswerWith correct = q.Frq.Correct + userMarkedCorrect := false if q.Frq.UserMarkedCorrect != nil && *q.Frq.UserMarkedCorrect { correct = true + userMarkedCorrect = true + } + data = map[string]interface{}{ + "answeredString": q.Frq.AnsweredString, + "userMarkedCorrect": userMarkedCorrect, } + allTermIDs = append(allTermIDs, termID) } if correct { questionsCorrect++ } + dataBytes, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal question data: %w", err) + } + + questionRows = append(questionRows, model.QuestionRow{ + TermID: termID, + TermSnapshot: termSnapshot, + DefSnapshot: defSnapshot, + Type: qType, + AnswerWith: answerWith, + Correct: correct, + Position: int32(i), + Data: dataBytes, + }) + if termID == "" { continue } @@ -680,13 +689,9 @@ func (r *mutationResolver) RecordPracticeTest(ctx context.Context, input model.P if answerWith == model.AnswerWithDef { defIncorrectIncrease = 1 tp.DefReviewedAt = &nowStr - box := int32(1) - tp.DefLeitnerSystemBox = &box } else { termIncorrectIncrease = 1 tp.TermReviewedAt = &nowStr - box := int32(1) - tp.TermLeitnerSystemBox = &box } } @@ -712,7 +717,6 @@ func (r *mutationResolver) RecordPracticeTest(ctx context.Context, input model.P } } - allTermIDs := append(questionTermIDs, distractorTermIDs...) var studysetIDs []string if len(allTermIDs) > 0 { var studysets []struct { @@ -744,48 +748,33 @@ func (r *mutationResolver) RecordPracticeTest(ctx context.Context, input model.P tx, &practiceTest, `INSERT INTO practice_tests - (timestamp, user_id, questions_correct, questions_total, questions) -VALUES (now(), $1, $2, $3, $4) + (timestamp, user_id, questions_correct, questions_total) +VALUES (now(), $1, $2, $3) RETURNING id, to_char(timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as timestamp, questions_correct, - questions_total, - questions`, + questions_total`, authedUser.ID, questionsCorrect, questionsTotal, - input.Questions, ) if err != nil { return nil, fmt.Errorf("database error in RecordPracticeTest: %w", err) } - if len(questionTermIDs) > 0 { - placeholders := make([]string, len(questionTermIDs)) - args := make([]interface{}, len(questionTermIDs)+1) - args[0] = practiceTest.ID - for i, termID := range questionTermIDs { - placeholders[i] = fmt.Sprintf("($1, $%d)", i+2) - args[i+1] = termID - } - sql := fmt.Sprintf("INSERT INTO practice_test_question_terms (practice_test_id, term_id) VALUES %s", strings.Join(placeholders, ",")) - if _, err := tx.Exec(ctx, sql, args...); err != nil { - return nil, fmt.Errorf("failed to insert question terms: %w", err) - } - } - - if len(distractorTermIDs) > 0 { - placeholders := make([]string, len(distractorTermIDs)) - args := make([]interface{}, len(distractorTermIDs)+1) - args[0] = practiceTest.ID - for i, termID := range distractorTermIDs { - placeholders[i] = fmt.Sprintf("($1, $%d)", i+2) - args[i+1] = termID + if len(questionRows) > 0 { + placeholders := make([]string, len(questionRows)) + args := make([]interface{}, 0, len(questionRows)*9+1) + args = append(args, practiceTest.ID) + for i, qr := range questionRows { + base := i*8 + 2 + placeholders[i] = fmt.Sprintf("($1, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d)", base, base+1, base+2, base+3, base+4, base+5, base+6, base+7) + args = append(args, qr.TermID, qr.TermSnapshot, qr.DefSnapshot, qr.Type, qr.AnswerWith, qr.Correct, qr.Position, qr.Data) } - sql := fmt.Sprintf("INSERT INTO practice_test_distractor_terms (practice_test_id, term_id) VALUES %s", strings.Join(placeholders, ",")) + sql := fmt.Sprintf("INSERT INTO practice_test_questions (practice_test_id, term_id, term_snapshot, def_snapshot, type, answer_with, correct, position, data) VALUES %s", strings.Join(placeholders, ",")) if _, err := tx.Exec(ctx, sql, args...); err != nil { - return nil, fmt.Errorf("failed to insert distractor terms: %w", err) + return nil, fmt.Errorf("failed to insert practice test questions: %w", err) } } @@ -810,15 +799,16 @@ RETURNING } valueStrings := make([]string, 0, len(termProgress)) - valueArgs := make([]interface{}, 0, len(termProgress)*10) + valueArgs := make([]interface{}, 0, len(termProgress)*8) termIDs := make([]string, len(termProgress)) for i, p := range termProgress { termIDs[i] = p.TermID - base := i*10 + 1 + // Each row has 8 parameters + base := i*8 + 1 valueStrings = append(valueStrings, fmt.Sprintf( - "($%d::uuid,$%d::uuid,$%d::timestamptz,$%d::timestamptz,CASE WHEN $%d IS NOT NULL THEN 1 ELSE 0 END,$%d::timestamptz,$%d::timestamptz,CASE WHEN $%d IS NOT NULL THEN 1 ELSE 0 END,$%d::int,$%d::int,COALESCE($%d::int,0),COALESCE($%d::int,0),COALESCE($%d::int,0),COALESCE($%d::int,0))", - base, base+1, base+2, base+2, base+2, base+3, base+3, base+3, base+4, base+5, base+6, base+7, base+8, base+9, + "($%d::uuid,$%d::uuid,$%d::timestamptz,$%d::timestamptz,CASE WHEN $%d IS NOT NULL THEN 1 ELSE 0 END,$%d::timestamptz,$%d::timestamptz,CASE WHEN $%d IS NOT NULL THEN 1 ELSE 0 END,COALESCE($%d::int,0),COALESCE($%d::int,0),COALESCE($%d::int,0),COALESCE($%d::int,0))", + base, base+1, base+2, base+2, base+2, base+3, base+3, base+3, base+4, base+5, base+6, base+7, )) valueArgs = append(valueArgs, @@ -826,8 +816,6 @@ RETURNING authedUser.ID, p.TermReviewedAt, p.DefReviewedAt, - p.TermLeitnerSystemBox, - p.DefLeitnerSystemBox, p.TermCorrectIncrease, p.TermIncorrectIncrease, p.DefCorrectIncrease, @@ -842,7 +830,6 @@ RETURNING term_review_count, def_first_reviewed_at, def_last_reviewed_at, def_review_count, - term_leitner_system_box, def_leitner_system_box, term_correct_count, term_incorrect_count, def_correct_count, def_incorrect_count ) @@ -851,7 +838,6 @@ RETURNING v.term_review_count::int, v.def_first_reviewed_at::timestamptz, v.def_last_reviewed_at::timestamptz, v.def_review_count::int, - v.term_leitner_system_box::int, v.def_leitner_system_box::int, v.term_correct_count::int, v.term_incorrect_count::int, v.def_correct_count::int, v.def_incorrect_count::int FROM (VALUES %s) AS v( @@ -860,7 +846,6 @@ RETURNING term_review_count, def_first_reviewed_at, def_last_reviewed_at, def_review_count, - term_leitner_system_box, def_leitner_system_box, term_correct_count, term_incorrect_count, def_correct_count, def_incorrect_count ) @@ -870,8 +855,6 @@ RETURNING ON CONFLICT (term_id, user_id) DO UPDATE SET term_last_reviewed_at = COALESCE(EXCLUDED.term_last_reviewed_at, term_progress.term_last_reviewed_at), def_last_reviewed_at = COALESCE(EXCLUDED.def_last_reviewed_at, term_progress.def_last_reviewed_at), - term_leitner_system_box = EXCLUDED.term_leitner_system_box, - def_leitner_system_box = EXCLUDED.def_leitner_system_box, term_review_count = term_progress.term_review_count + (CASE WHEN EXCLUDED.term_last_reviewed_at IS NOT NULL THEN 1 ELSE 0 END), def_review_count = term_progress.def_review_count + @@ -887,7 +870,6 @@ RETURNING to_char(term_progress.def_first_reviewed_at, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as def_first_reviewed_at, to_char(term_progress.def_last_reviewed_at, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as def_last_reviewed_at, term_progress.def_review_count, - term_progress.term_leitner_system_box, term_progress.def_leitner_system_box, term_progress.term_correct_count, term_progress.term_incorrect_count, term_progress.def_correct_count, term_progress.def_incorrect_count `, strings.Join(valueStrings, ",")) @@ -896,32 +878,6 @@ RETURNING if err := pgxscan.Select(ctx, tx, &results, query, valueArgs...); err != nil { return nil, fmt.Errorf("bulk upsert of term progress failed: %w", err) } - - if len(results) > 0 { - histVals := make([]string, 0, len(results)) - histArgs := make([]interface{}, 0, len(results)*6) - for i, tp := range results { - base := i*6 + 1 - histVals = append(histVals, fmt.Sprintf("($%d,$%d,$%d,$%d,$%d,$%d)", base, base+1, base+2, base+3, base+4, base+5)) - histArgs = append(histArgs, - authedUser.ID, - termIDs[i], - tp.TermCorrectCount, - tp.TermIncorrectCount, - tp.DefCorrectCount, - tp.DefIncorrectCount, - ) - } - histQuery := fmt.Sprintf(` - INSERT INTO term_progress_history ( - user_id, term_id, term_correct_count, term_incorrect_count, - def_correct_count, def_incorrect_count - ) VALUES %s`, strings.Join(histVals, ",")) - - if _, err := tx.Exec(ctx, histQuery, histArgs...); err != nil { - return nil, fmt.Errorf("failed to insert into term_progress_history: %w", err) - } - } } if err := tx.Commit(ctx); err != nil { @@ -931,8 +887,8 @@ RETURNING return &practiceTest, nil } -// UpdatePracticeTest is the resolver for the updatePracticeTest field. -func (r *mutationResolver) UpdatePracticeTest(ctx context.Context, id string, input model.PracticeTestInput) (*model.PracticeTest, error) { +// UpdatePracticeTestQuestion is the resolver for the updatePracticeTestQuestion field. +func (r *mutationResolver) UpdatePracticeTestQuestion(ctx context.Context, id string, correct bool, userMarkedCorrect *bool) (*model.Question, error) { authedUser := auth.AuthedUserContext(ctx) if authedUser == nil { return nil, fmt.Errorf("not authenticated") @@ -944,153 +900,90 @@ func (r *mutationResolver) UpdatePracticeTest(ctx context.Context, id string, in } defer tx.Rollback(ctx) - var questionsCorrect int32 = 0 - var questionsTotal int32 = int32(len(input.Questions)) - var questionTermIDs []string - var distractorTermIDs []string - - for _, q := range input.Questions { - if q == nil { - continue - } - correct := false - if q.Mcq != nil { - correct = q.Mcq.Correct - if q.Mcq.Term != nil { - questionTermIDs = append(questionTermIDs, q.Mcq.Term.ID) - } - for _, d := range q.Mcq.Distractors { - if d != nil { - distractorTermIDs = append(distractorTermIDs, d.ID) - } - } - } else if q.Tfq != nil { - correct = q.Tfq.Correct - if q.Tfq.Term != nil { - questionTermIDs = append(questionTermIDs, q.Tfq.Term.ID) - } - if q.Tfq.Distractor != nil { - distractorTermIDs = append(distractorTermIDs, q.Tfq.Distractor.ID) - } - } else if q.Frq != nil { - correct = q.Frq.Correct - if q.Frq.UserMarkedCorrect != nil && *q.Frq.UserMarkedCorrect { - correct = true - } - if q.Frq.Term != nil { - questionTermIDs = append(questionTermIDs, q.Frq.Term.ID) - } - } - if correct { - questionsCorrect++ - } + var row model.QuestionRow + err = pgxscan.Get(ctx, tx, &row, ` + SELECT q.id, q.practice_test_id, q.term_id, q.term_snapshot, q.def_snapshot, q.type, q.answer_with, q.correct, q.position, q.data + FROM practice_test_questions q + JOIN practice_tests pt ON q.practice_test_id = pt.id + WHERE q.id = $1 AND pt.user_id = $2 + `, id, authedUser.ID) + if err != nil { + return nil, fmt.Errorf("question not found or not owned by user: %w", err) } - allTermIDs := append(questionTermIDs, distractorTermIDs...) - var studysetIDs []string - if len(allTermIDs) > 0 { - var studysets []struct { - ID string `db:"id"` - Private bool `db:"private"` - UserID string `db:"user_id"` - Draft bool `db:"draft"` - } - err = pgxscan.Select(ctx, tx, &studysets, ` - SELECT id, private, user_id, draft - FROM studysets - WHERE id IN (SELECT DISTINCT studyset_id FROM terms WHERE id = ANY($1)) - `, allTermIDs) - if err != nil { - return nil, fmt.Errorf("database error checking studysets: %w", err) - } + oldCorrect := row.Correct + newCorrect := correct - for _, s := range studysets { - if s.Draft || (s.Private && (authedUser.ID == nil || s.UserID != *authedUser.ID)) { - return nil, fmt.Errorf("studyset not found or not accessible") - } - studysetIDs = append(studysetIDs, s.ID) - } + if row.Type != "FRQ" { + return nil, fmt.Errorf("only FRQ questions can be updated via this mutation") } - var practiceTest model.PracticeTest - err = pgxscan.Get( - ctx, - tx, - &practiceTest, - `UPDATE practice_tests SET questions_correct = $3, questions_total = $4, questions = $5 -WHERE user_id = $1 AND id = $2 -RETURNING - id, - to_char(timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as timestamp, - questions_correct, - questions_total, - questions`, - authedUser.ID, - id, - questionsCorrect, - questionsTotal, - input.Questions, - ) - if err != nil { - if pgxscan.NotFound(err) { - return nil, fmt.Errorf("practice test not found") - } - return nil, fmt.Errorf("database error in updatePracticeTest: %w", err) + var data map[string]interface{} + if err := json.Unmarshal(row.Data, &data); err != nil { + return nil, fmt.Errorf("failed to unmarshal question data: %w", err) } - // Update mappings - _, err = tx.Exec(ctx, "DELETE FROM practice_test_question_terms WHERE practice_test_id = $1", id) - if err != nil { - return nil, fmt.Errorf("failed to delete old question terms: %w", err) + if userMarkedCorrect != nil { + data["userMarkedCorrect"] = *userMarkedCorrect } - _, err = tx.Exec(ctx, "DELETE FROM practice_test_distractor_terms WHERE practice_test_id = $1", id) + + newDataBytes, err := json.Marshal(data) if err != nil { - return nil, fmt.Errorf("failed to delete old distractor terms: %w", err) + return nil, fmt.Errorf("failed to marshal new question data: %w", err) } - _, err = tx.Exec(ctx, "DELETE FROM practice_test_studysets WHERE practice_test_id = $1", id) + + _, err = tx.Exec(ctx, ` + UPDATE practice_test_questions + SET correct = $2, data = $3 + WHERE id = $1 + `, id, newCorrect, newDataBytes) if err != nil { - return nil, fmt.Errorf("failed to delete old practice test studysets: %w", err) + return nil, fmt.Errorf("failed to update question: %w", err) } - if len(questionTermIDs) > 0 { - placeholders := make([]string, len(questionTermIDs)) - args := make([]interface{}, len(questionTermIDs)+1) - args[0] = id - for i, termID := range questionTermIDs { - placeholders[i] = fmt.Sprintf("($1, $%d)", i+2) - args[i+1] = termID + if oldCorrect != newCorrect { + diff := int32(1) + if oldCorrect { + diff = -1 } - sql := fmt.Sprintf("INSERT INTO practice_test_question_terms (practice_test_id, term_id) VALUES %s", strings.Join(placeholders, ",")) - if _, err := tx.Exec(ctx, sql, args...); err != nil { - return nil, fmt.Errorf("failed to insert question terms: %w", err) + _, err = tx.Exec(ctx, ` + UPDATE practice_tests + SET questions_correct = questions_correct + $1 + WHERE id = $2 + `, diff, row.PracticeTestID) + if err != nil { + return nil, fmt.Errorf("failed to update practice test correct count: %w", err) } - } - if len(distractorTermIDs) > 0 { - placeholders := make([]string, len(distractorTermIDs)) - args := make([]interface{}, len(distractorTermIDs)+1) - args[0] = id - for i, termID := range distractorTermIDs { - placeholders[i] = fmt.Sprintf("($1, $%d)", i+2) - args[i+1] = termID - } - sql := fmt.Sprintf("INSERT INTO practice_test_distractor_terms (practice_test_id, term_id) VALUES %s", strings.Join(placeholders, ",")) - if _, err := tx.Exec(ctx, sql, args...); err != nil { - return nil, fmt.Errorf("failed to insert distractor terms: %w", err) + // Update term progress + var correctInc, incorrectInc int32 + if newCorrect { + correctInc = 1 + incorrectInc = -1 + } else { + correctInc = -1 + incorrectInc = 1 } - } - if len(studysetIDs) > 0 { - placeholders := make([]string, len(studysetIDs)) - args := make([]interface{}, len(studysetIDs)+1) - args[0] = id - for i, studysetID := range studysetIDs { - placeholders[i] = fmt.Sprintf("($1, $%d)", i+2) - args[i+1] = studysetID + var termCorrectInc, termIncorrectInc, defCorrectInc, defIncorrectInc int32 + if row.AnswerWith == model.AnswerWithDef { + defCorrectInc = correctInc + defIncorrectInc = incorrectInc + } else { + termCorrectInc = correctInc + termIncorrectInc = incorrectInc } - sql := fmt.Sprintf("INSERT INTO practice_test_studysets (practice_test_id, studyset_id) VALUES %s", strings.Join(placeholders, ",")) - if _, err := tx.Exec(ctx, sql, args...); err != nil { - return nil, fmt.Errorf("failed to insert practice test studysets: %w", err) + + _, err = tx.Exec(ctx, ` + UPDATE term_progress + SET term_correct_count = term_correct_count + $3, + term_incorrect_count = term_incorrect_count + $4, + def_correct_count = def_correct_count + $5, + def_incorrect_count = def_incorrect_count + $6 + WHERE term_id = $1 AND user_id = $2 + `, row.TermID, authedUser.ID, termCorrectInc, termIncorrectInc, defCorrectInc, defIncorrectInc) + if err != nil { + return nil, fmt.Errorf("failed to update term progress: %w", err) } } @@ -1098,7 +991,30 @@ RETURNING return nil, fmt.Errorf("failed to commit transaction: %w", err) } - return &practiceTest, nil + // Reconstruct and return the question + q := &model.Question{} + termATP := &model.TermAtp{ + ID: row.TermID, + TermSnapshot: row.TermSnapshot, + DefSnapshot: row.DefSnapshot, + } + umc := false + if userMarkedCorrect != nil { + umc = *userMarkedCorrect + } else if val, ok := data["userMarkedCorrect"].(bool); ok { + umc = val + } + + q.ID = row.ID + q.Frq = &model.Frq{ + Term: termATP, + AnswerWith: row.AnswerWith, + Correct: newCorrect, + UserMarkedCorrect: &umc, + AnsweredString: data["answeredString"].(string), + } + + return q, nil } // CreateFolder is the resolver for the createFolder field. diff --git a/graph/resolver/query.resolvers.go b/graph/resolver/query.resolvers.go index 682853b..e4d7493 100644 --- a/graph/resolver/query.resolvers.go +++ b/graph/resolver/query.resolvers.go @@ -6,6 +6,7 @@ package resolver import ( "context" + "encoding/json" "fmt" "quizfreely/api/auth" "quizfreely/api/graph" @@ -968,8 +969,7 @@ func (r *queryResolver) PracticeTest(ctx context.Context, id string) (*model.Pra `SELECT id, to_char(timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.MSTZH:TZM') as timestamp, questions_correct, - questions_total, - questions + questions_total FROM practice_tests WHERE id = $1 AND user_id = $2`, id, @@ -1224,6 +1224,87 @@ func (r *queryResolver) MySavedStudysetCount(ctx context.Context) (int32, error) return count, nil } +// Questions is the resolver for the questions field. +func (r *practiceTestResolver) Questions(ctx context.Context, obj *model.PracticeTest) ([]*model.Question, error) { + if obj == nil || obj.ID == nil { + return nil, nil + } + + var rows []*model.QuestionRow + sql := `SELECT id, practice_test_id, term_id, term_snapshot, def_snapshot, type, answer_with, correct, position, data + FROM practice_test_questions WHERE practice_test_id = $1 ORDER BY position ASC` + err := pgxscan.Select(ctx, r.DB, &rows, sql, *obj.ID) + if err != nil { + return nil, fmt.Errorf("failed to fetch questions for practice test: %w", err) + } + + questions := make([]*model.Question, len(rows)) + for i, row := range rows { + q := &model.Question{} + termATP := &model.TermAtp{ + ID: row.TermID, + TermSnapshot: row.TermSnapshot, + DefSnapshot: row.DefSnapshot, + } + + switch row.Type { + case "MCQ": + var data struct { + Distractors []*model.TermAtp `json:"distractors"` + CorrectChoiceIndex int32 `json:"correctChoiceIndex"` + AnsweredIndex int32 `json:"answeredIndex"` + } + if err := json.Unmarshal(row.Data, &data); err != nil { + return nil, fmt.Errorf("failed to unmarshal MCQ data: %w", err) + } + q.ID = row.ID + q.Mcq = &model.Mcq{ + Term: termATP, + AnswerWith: row.AnswerWith, + Correct: row.Correct, + CorrectChoiceIndex: data.CorrectChoiceIndex, + AnsweredIndex: data.AnsweredIndex, + Distractors: data.Distractors, + } + case "TFQ": + var data struct { + AnsweredBool bool `json:"answeredBool"` + Distractor *model.TermAtp `json:"distractor"` + } + if err := json.Unmarshal(row.Data, &data); err != nil { + return nil, fmt.Errorf("failed to unmarshal TFQ data: %w", err) + } + q.ID = row.ID + q.Tfq = &model.Tfq{ + Term: termATP, + AnswerWith: row.AnswerWith, + Correct: row.Correct, + AnsweredBool: data.AnsweredBool, + Distractor: data.Distractor, + } + case "FRQ": + var data struct { + AnsweredString string `json:"answeredString"` + UserMarkedCorrect bool `json:"userMarkedCorrect"` + } + if err := json.Unmarshal(row.Data, &data); err != nil { + return nil, fmt.Errorf("failed to unmarshal FRQ data: %w", err) + } + q.ID = row.ID + q.Frq = &model.Frq{ + Term: termATP, + AnswerWith: row.AnswerWith, + Correct: row.Correct, + UserMarkedCorrect: &data.UserMarkedCorrect, + AnsweredString: data.AnsweredString, + } + } + questions[i] = q + } + + return questions, nil +} + // PracticeTest returns graph.PracticeTestResolver implementation. func (r *Resolver) PracticeTest() graph.PracticeTestResolver { return &practiceTestResolver{r} } diff --git a/graph/resolver/term.resolvers.go b/graph/resolver/term.resolvers.go index 2558ff7..dd5054d 100644 --- a/graph/resolver/term.resolvers.go +++ b/graph/resolver/term.resolvers.go @@ -22,16 +22,6 @@ func (r *termResolver) Progress(ctx context.Context, obj *model.Term) (*model.Te return loader.GetTermProgress(ctx, *obj.ID) } -// ProgressHistory is the resolver for the progressHistory field. -func (r *termResolver) ProgressHistory(ctx context.Context, obj *model.Term) ([]*model.TermProgressHistory, error) { - authedUser := auth.AuthedUserContext(ctx) - if authedUser == nil || authedUser.ID == nil || obj.ID == nil { - return nil, nil - } - - return loader.GetTermProgressHistory(ctx, *obj.ID) -} - // TopConfusionPairs is the resolver for the top_confusion_pairs field. func (r *termResolver) TopConfusionPairs(ctx context.Context, obj *model.Term) ([]*model.TermConfusionPair, error) { authedUser := auth.AuthedUserContext(ctx) diff --git a/graph/term.graphqls b/graph/term.graphqls index a72a10e..d3d3adf 100644 --- a/graph/term.graphqls +++ b/graph/term.graphqls @@ -6,7 +6,6 @@ type Term { defImageUrl: String sortOrder: Int! progress: TermProgress - progressHistory: [TermProgressHistory] topConfusionPairs: [TermConfusionPair] topReverseConfusionPairs: [TermConfusionPair] fsrsCard: FSRSCard @@ -38,23 +37,11 @@ type TermProgress { termIncorrectCount: Int! defCorrectCount: Int! defIncorrectCount: Int! - termLeitnerSystemBox: Int - defLeitnerSystemBox: Int -} -type TermProgressHistory { - id: ID! - timestamp: String! - termCorrectCount: Int - termIncorrectCount: Int - defCorrectCount: Int - defIncorrectCount: Int } input TermProgressInput { termId: ID! termReviewedAt: String defReviewedAt: String - termLeitnerSystemBox: Int - defLeitnerSystemBox: Int termCorrectIncrease: Int termIncorrectIncrease: Int defCorrectIncrease: Int diff --git a/tests/practice_test_test.go b/tests/practice_test_test.go index 2184276..85fa05c 100644 --- a/tests/practice_test_test.go +++ b/tests/practice_test_test.go @@ -58,23 +58,24 @@ func TestPracticeTestLifecycle(t *testing.T) { id questionsCorrect questionsTotal + questions { + id + } } }`, "variables": map[string]interface{}{ "input": map[string]interface{}{ "questions": []interface{}{ map[string]interface{}{ - "mcq": map[string]interface{}{ + "frq": map[string]interface{}{ "term": map[string]interface{}{ - "id": term1ID, - "term": term1Text, - "def": term1Def, + "id": term1ID, + "termSnapshot": term1Text, + "defSnapshot": term1Def, }, - "answerWith": "DEF", - "correct": true, - "correctChoiceIndex": 0, - "answeredIndex": 0, - "distractors": []interface{}{}, + "answerWith": "DEF", + "correct": true, + "answeredString": term1Def, }, }, }, @@ -88,55 +89,39 @@ func TestPracticeTestLifecycle(t *testing.T) { var recordResult map[string]interface{} json.NewDecoder(resp.Body).Decode(&recordResult) require.Nil(t, recordResult["errors"], "user2 should be able to record PT for public set") - ptID := getNested(recordResult, "data", "recordPracticeTest", "id").(string) + q1ID := getNested(recordResult, "data", "recordPracticeTest", "questions", 0, "id").(string) - // 4. user2 updates their own practice test - updatePTBody := map[string]interface{}{ - "query": `mutation UpdatePT($id: ID!, $input: PracticeTestInput!) { - updatePracticeTest(id: $id, input: $input) { - id - questionsCorrect + // 4. user2 updates their own practice test question + updatePTQBody := map[string]interface{}{ + "query": `mutation UpdatePTQ($id: ID!, $correct: Boolean!) { + updatePracticeTestQuestion(id: $id, correct: $correct) { + frq { + correct + } } }`, "variables": map[string]interface{}{ - "id": ptID, - "input": map[string]interface{}{ - "questions": []interface{}{ - map[string]interface{}{ - "mcq": map[string]interface{}{ - "term": map[string]interface{}{ - "id": term1ID, - "term": term1Text, - "def": term1Def, - }, - "answerWith": "DEF", - "correct": false, - "correctChoiceIndex": 0, - "answeredIndex": 0, - "distractors": []interface{}{}, - }, - }, - }, - }, + "id": q1ID, + "correct": false, }, } - req, _ = http.NewRequest(http.MethodPost, testServer.URL+"/graphql", marshal(updatePTBody)) + req, _ = http.NewRequest(http.MethodPost, testServer.URL+"/graphql", marshal(updatePTQBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+user2Token) resp, _ = http.DefaultClient.Do(req) var updateResult map[string]interface{} json.NewDecoder(resp.Body).Decode(&updateResult) - require.Nil(t, updateResult["errors"], "user2 should be able to update their own PT") - require.Equal(t, float64(0), getNested(updateResult, "data", "updatePracticeTest", "questionsCorrect")) + require.Nil(t, updateResult["errors"], "user2 should be able to update their own PTQ") + require.False(t, getNested(updateResult, "data", "updatePracticeTestQuestion", "frq", "correct").(bool)) - // 5. Invalid Authz: user1 tries to update user2's practice test - req, _ = http.NewRequest(http.MethodPost, testServer.URL+"/graphql", marshal(updatePTBody)) + // 5. Invalid Authz: user1 tries to update user2's practice test question + req, _ = http.NewRequest(http.MethodPost, testServer.URL+"/graphql", marshal(updatePTQBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+user1Token) resp, _ = http.DefaultClient.Do(req) var unauthorizedResult map[string]interface{} json.NewDecoder(resp.Body).Decode(&unauthorizedResult) - require.NotNil(t, unauthorizedResult["errors"], "user1 should NOT be able to update user2's PT") + require.NotNil(t, unauthorizedResult["errors"], "user1 should NOT be able to update user2's PTQ") // 6. Private Set Security (Implicit): user2 tries to record PT for a term in a private studyset @@ -167,9 +152,9 @@ func TestPracticeTestLifecycle(t *testing.T) { map[string]interface{}{ "mcq": map[string]interface{}{ "term": map[string]interface{}{ - "id": privateTermID, - "term": "X", - "def": "Y", + "id": privateTermID, + "termSnapshot": "X", + "defSnapshot": "Y", }, "answerWith": "DEF", "correct": true, diff --git a/tests/term_progress_test.go b/tests/term_progress_test.go index 043bbbb..c35fc72 100644 --- a/tests/term_progress_test.go +++ b/tests/term_progress_test.go @@ -47,16 +47,14 @@ func TestTermProgressLifecycle(t *testing.T) { updateProgressBody := map[string]interface{}{ "query": `mutation UpdateProgress($input: [TermProgressInput!]!) { updateTermProgress(termProgress: $input) { - termLeitnerSystemBox termCorrectCount } }`, "variables": map[string]interface{}{ "input": []map[string]interface{}{ { - "termId": termID, - "termCorrectIncrease": 1, - "termLeitnerSystemBox": 1, + "termId": termID, + "termCorrectIncrease": 1, }, }, },