From a902dda718be0f3116139fd518f7d549561214d5 Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Fri, 18 Jul 2025 21:14:57 -0400 Subject: [PATCH 1/7] feat: add rating mutation and query for has rating - Added mutation for a user to upsert a mutation on a revision - Added field to revision for if the currently logged in user has rated it --- api/db/has_user_rated_revision.sql.go | 35 ++ api/db/querier.go | 1 + api/graph/generated.go | 304 ++++++++++++++++++ api/graph/model/models_gen.go | 7 + api/graph/mutation.resolvers.go | 10 + api/graph/recipe.resolvers.go | 15 +- api/graph/schema/mutation.graphql | 8 + api/graph/schema/recipe.graphql | 1 + api/services/recipe/recipe.go | 42 ++- api/services/user/user.go | 3 + .../revision/has_user_rated_revision.sql | 9 + 11 files changed, 433 insertions(+), 2 deletions(-) create mode 100644 api/db/has_user_rated_revision.sql.go create mode 100644 db/queries/revision/has_user_rated_revision.sql diff --git a/api/db/has_user_rated_revision.sql.go b/api/db/has_user_rated_revision.sql.go new file mode 100644 index 0000000..e75a0be --- /dev/null +++ b/api/db/has_user_rated_revision.sql.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: has_user_rated_revision.sql + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const hasUserRatedRevision = `-- name: HasUserRatedRevision :one +SELECT EXISTS( + SELECT star_value + FROM ratings + WHERE + revision_id = $1::uuid + AND + user_id = $2::uuid +) +` + +type HasUserRatedRevisionParams struct { + RevisionID pgtype.UUID + UserID pgtype.UUID +} + +func (q *Queries) HasUserRatedRevision(ctx context.Context, arg HasUserRatedRevisionParams) (bool, error) { + row := q.db.QueryRow(ctx, hasUserRatedRevision, arg.RevisionID, arg.UserID) + var exists bool + err := row.Scan(&exists) + return exists, err +} diff --git a/api/db/querier.go b/api/db/querier.go index 1ee7432..7304a01 100644 --- a/api/db/querier.go +++ b/api/db/querier.go @@ -32,6 +32,7 @@ type Querier interface { GetUserByEmail(ctx context.Context, email string) (User, error) GetUserById(ctx context.Context, id pgtype.UUID) (User, error) GetUserBySessionId(ctx context.Context, id pgtype.UUID) (GetUserBySessionIdRow, error) + HasUserRatedRevision(ctx context.Context, arg HasUserRatedRevisionParams) (bool, error) ListIngredientsByRecipeRevisionID(ctx context.Context, id pgtype.UUID) ([]RecipeIngredient, error) ListRecipes(ctx context.Context, arg ListRecipesParams) ([]Recipe, error) //Used when there is not a featured recipe diff --git a/api/graph/generated.go b/api/graph/generated.go index 6ed7c08..ad69bdc 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -122,6 +122,7 @@ type ComplexityRoot struct { } RecipeMutation struct { + AddRating func(childComplexity int, input model.AddRatingInput) int AddRevision func(childComplexity int, input model.AddRevisionInput) int Create func(childComplexity int, input model.CreateRecipeInput) int } @@ -134,6 +135,7 @@ type ComplexityRoot struct { RecipeRevision struct { ChangeComment func(childComplexity int) int + HasRated func(childComplexity int) int ID func(childComplexity int) int Ingredients func(childComplexity int) int Parent func(childComplexity int) int @@ -211,6 +213,7 @@ type RecipeIngredientResolver interface { type RecipeMutationResolver interface { Create(ctx context.Context, obj *model.RecipeMutation, input model.CreateRecipeInput) (*model.Recipe, error) AddRevision(ctx context.Context, obj *model.RecipeMutation, input model.AddRevisionInput) (*model.RecipeRevision, error) + AddRating(ctx context.Context, obj *model.RecipeMutation, input model.AddRatingInput) (bool, error) } type RecipeQueryResolver interface { ByID(ctx context.Context, obj *model.RecipeQuery, id uuid.UUID) (*model.Recipe, error) @@ -225,6 +228,8 @@ type RecipeRevisionResolver interface { Ingredients(ctx context.Context, obj *model.RecipeRevision) ([]*model.RecipeIngredient, error) Steps(ctx context.Context, obj *model.RecipeRevision) ([]*model.RecipeStep, error) Rating(ctx context.Context, obj *model.RecipeRevision) (*float64, error) + + HasRated(ctx context.Context, obj *model.RecipeRevision) (*bool, error) } type RecipeStepResolver interface { Revision(ctx context.Context, obj *model.RecipeStep) (*model.RecipeRevision, error) @@ -494,6 +499,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.RecipeIngredient.Unit(childComplexity), true + case "RecipeMutation.addRating": + if e.complexity.RecipeMutation.AddRating == nil { + break + } + + args, err := ec.field_RecipeMutation_addRating_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.RecipeMutation.AddRating(childComplexity, args["input"].(model.AddRatingInput)), true + case "RecipeMutation.addRevision": if e.complexity.RecipeMutation.AddRevision == nil { break @@ -561,6 +578,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.RecipeRevision.ChangeComment(childComplexity), true + case "RecipeRevision.hasRated": + if e.complexity.RecipeRevision.HasRated == nil { + break + } + + return e.complexity.RecipeRevision.HasRated(childComplexity), true + case "RecipeRevision.id": if e.complexity.RecipeRevision.ID == nil { break @@ -854,6 +878,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { rc := graphql.GetOperationContext(ctx) ec := executionContext{rc, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( + ec.unmarshalInputAddRatingInput, ec.unmarshalInputAddRevisionInput, ec.unmarshalInputCreateRecipeInput, ec.unmarshalInputCreateRecipeRevisionIngredient, @@ -1016,6 +1041,21 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs return args, nil } +func (ec *executionContext) field_RecipeMutation_addRating_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.AddRatingInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNAddRatingInput2forkdᚋgraphᚋmodelᚐAddRatingInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_RecipeMutation_addRevision_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1746,6 +1786,8 @@ func (ec *executionContext) fieldContext_Mutation_recipe(_ context.Context, fiel return ec.fieldContext_RecipeMutation_create(ctx, field) case "addRevision": return ec.fieldContext_RecipeMutation_addRevision(ctx, field) + case "addRating": + return ec.fieldContext_RecipeMutation_addRating(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RecipeMutation", field.Name) }, @@ -1814,6 +1856,8 @@ func (ec *executionContext) fieldContext_PaginatedRecipeRevisions_items(_ contex return ec.fieldContext_RecipeRevision_rating(ctx, field) case "photo": return ec.fieldContext_RecipeRevision_photo(ctx, field) + case "hasRated": + return ec.fieldContext_RecipeRevision_hasRated(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RecipeRevision", field.Name) }, @@ -2547,6 +2591,8 @@ func (ec *executionContext) fieldContext_Recipe_forkedFrom(_ context.Context, fi return ec.fieldContext_RecipeRevision_rating(ctx, field) case "photo": return ec.fieldContext_RecipeRevision_photo(ctx, field) + case "hasRated": + return ec.fieldContext_RecipeRevision_hasRated(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RecipeRevision", field.Name) }, @@ -2717,6 +2763,8 @@ func (ec *executionContext) fieldContext_Recipe_featuredRevision(_ context.Conte return ec.fieldContext_RecipeRevision_rating(ctx, field) case "photo": return ec.fieldContext_RecipeRevision_photo(ctx, field) + case "hasRated": + return ec.fieldContext_RecipeRevision_hasRated(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RecipeRevision", field.Name) }, @@ -2829,6 +2877,8 @@ func (ec *executionContext) fieldContext_RecipeIngredient_revision(_ context.Con return ec.fieldContext_RecipeRevision_rating(ctx, field) case "photo": return ec.fieldContext_RecipeRevision_photo(ctx, field) + case "hasRated": + return ec.fieldContext_RecipeRevision_hasRated(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RecipeRevision", field.Name) }, @@ -3207,6 +3257,8 @@ func (ec *executionContext) fieldContext_RecipeMutation_addRevision(ctx context. return ec.fieldContext_RecipeRevision_rating(ctx, field) case "photo": return ec.fieldContext_RecipeRevision_photo(ctx, field) + case "hasRated": + return ec.fieldContext_RecipeRevision_hasRated(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RecipeRevision", field.Name) }, @@ -3225,6 +3277,85 @@ func (ec *executionContext) fieldContext_RecipeMutation_addRevision(ctx context. return fc, nil } +func (ec *executionContext) _RecipeMutation_addRating(ctx context.Context, field graphql.CollectedField, obj *model.RecipeMutation) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_RecipeMutation_addRating(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) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.RecipeMutation().AddRating(rctx, obj, fc.Args["input"].(model.AddRatingInput)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + required, err := ec.unmarshalOBoolean2ᚖbool(ctx, true) + if err != nil { + return nil, err + } + if ec.directives.Auth == nil { + return nil, errors.New("directive auth is not implemented") + } + return ec.directives.Auth(ctx, obj, directive0, required) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(bool); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be bool`, tmp) + }) + 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.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_RecipeMutation_addRating(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "RecipeMutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + 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_RecipeMutation_addRating_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _RecipeQuery_byId(ctx context.Context, field graphql.CollectedField, obj *model.RecipeQuery) (ret graphql.Marshaler) { fc, err := ec.fieldContext_RecipeQuery_byId(ctx, field) if err != nil { @@ -3716,6 +3847,8 @@ func (ec *executionContext) fieldContext_RecipeRevision_parent(_ context.Context return ec.fieldContext_RecipeRevision_rating(ctx, field) case "photo": return ec.fieldContext_RecipeRevision_photo(ctx, field) + case "hasRated": + return ec.fieldContext_RecipeRevision_hasRated(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RecipeRevision", field.Name) }, @@ -3963,6 +4096,67 @@ func (ec *executionContext) fieldContext_RecipeRevision_photo(_ context.Context, return fc, nil } +func (ec *executionContext) _RecipeRevision_hasRated(ctx context.Context, field graphql.CollectedField, obj *model.RecipeRevision) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_RecipeRevision_hasRated(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) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.RecipeRevision().HasRated(rctx, obj) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.Auth == nil { + return nil, errors.New("directive auth is not implemented") + } + return ec.directives.Auth(ctx, obj, directive0, nil) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*bool); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *bool`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*bool) + fc.Result = res + return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_RecipeRevision_hasRated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "RecipeRevision", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _RecipeStep_id(ctx context.Context, field graphql.CollectedField, obj *model.RecipeStep) (ret graphql.Marshaler) { fc, err := ec.fieldContext_RecipeStep_id(ctx, field) if err != nil { @@ -4068,6 +4262,8 @@ func (ec *executionContext) fieldContext_RecipeStep_revision(_ context.Context, return ec.fieldContext_RecipeRevision_rating(ctx, field) case "photo": return ec.fieldContext_RecipeRevision_photo(ctx, field) + case "hasRated": + return ec.fieldContext_RecipeRevision_hasRated(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RecipeRevision", field.Name) }, @@ -7085,6 +7281,40 @@ func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputAddRatingInput(ctx context.Context, obj interface{}) (model.AddRatingInput, error) { + var it model.AddRatingInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"revisionId", "starValue"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "revisionId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("revisionId")) + data, err := ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + it.RevisionID = data + case "starValue": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("starValue")) + data, err := ec.unmarshalNInt2int(ctx, v) + if err != nil { + return it, err + } + it.StarValue = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputAddRevisionInput(ctx context.Context, obj interface{}) (model.AddRevisionInput, error) { var it model.AddRevisionInput asMap := map[string]interface{}{} @@ -8407,6 +8637,42 @@ func (ec *executionContext) _RecipeMutation(ctx context.Context, sel ast.Selecti continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "addRating": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._RecipeMutation_addRating(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + 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) }) default: panic("unknown field " + strconv.Quote(field.Name)) @@ -8773,6 +9039,39 @@ func (ec *executionContext) _RecipeRevision(ctx context.Context, sel ast.Selecti out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "photo": out.Values[i] = ec._RecipeRevision_photo(ctx, field, obj) + case "hasRated": + 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._RecipeRevision_hasRated(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) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9731,6 +10030,11 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) unmarshalNAddRatingInput2forkdᚋgraphᚋmodelᚐAddRatingInput(ctx context.Context, v interface{}) (model.AddRatingInput, error) { + res, err := ec.unmarshalInputAddRatingInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNAddRevisionInput2forkdᚋgraphᚋmodelᚐAddRevisionInput(ctx context.Context, v interface{}) (model.AddRevisionInput, error) { res, err := ec.unmarshalInputAddRevisionInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go index b7e7e62..d78162c 100644 --- a/api/graph/model/models_gen.go +++ b/api/graph/model/models_gen.go @@ -16,6 +16,11 @@ type PaginatedResult interface { GetPagination() *PaginationInfo } +type AddRatingInput struct { + RevisionID uuid.UUID `json:"revisionId"` + StarValue int `json:"starValue"` +} + type AddRevisionInput struct { ID uuid.UUID `json:"id"` Parent uuid.UUID `json:"parent"` @@ -142,6 +147,7 @@ type RecipeIngredient struct { type RecipeMutation struct { Create *Recipe `json:"create"` AddRevision *RecipeRevision `json:"addRevision"` + AddRating bool `json:"addRating"` } type RecipeQuery struct { @@ -162,6 +168,7 @@ type RecipeRevision struct { Steps []*RecipeStep `json:"steps"` Rating *float64 `json:"rating,omitempty"` Photo *string `json:"photo,omitempty"` + HasRated *bool `json:"hasRated,omitempty"` } type RecipeStep struct { diff --git a/api/graph/mutation.resolvers.go b/api/graph/mutation.resolvers.go index 62fe0cb..c0b1f8c 100644 --- a/api/graph/mutation.resolvers.go +++ b/api/graph/mutation.resolvers.go @@ -6,6 +6,7 @@ package graph import ( "context" + "fmt" "forkd/graph/model" ) @@ -29,6 +30,15 @@ func (r *recipeMutationResolver) AddRevision(ctx context.Context, obj *model.Rec return r.RecipeService.AddRecipeRevision(ctx, input) } +// AddRating is the resolver for the addRating field. +func (r *recipeMutationResolver) AddRating(ctx context.Context, obj *model.RecipeMutation, input model.AddRatingInput) (bool, error) { + user, _ := r.AuthService.GetUserSessionFromCtx(ctx) + if user == nil { + return false, fmt.Errorf("no user on context") + } + return r.RecipeService.AddRevisionRating(ctx, user.ID.Bytes, input.RevisionID, input.StarValue) +} + // RequestMagicLink is the resolver for the requestMagicLink field. func (r *userMutationResolver) RequestMagicLink(ctx context.Context, obj *model.UserMutation, email string) (*string, error) { return r.AuthService.RequestMagicLink(ctx, email) diff --git a/api/graph/recipe.resolvers.go b/api/graph/recipe.resolvers.go index 5b084df..c58da43 100644 --- a/api/graph/recipe.resolvers.go +++ b/api/graph/recipe.resolvers.go @@ -75,11 +75,24 @@ func (r *recipeRevisionResolver) Steps(ctx context.Context, obj *model.RecipeRev // Rating is the resolver for the rating field. func (r *recipeRevisionResolver) Rating(ctx context.Context, obj *model.RecipeRevision) (*float64, error) { - // TODO: Implement logic for computing the rating. This might be best done as a computed field inside the db, but might also be good to have a dedicated resolver for rating, err := r.RecipeService.GetRevisionRating(ctx, obj.ID) return &rating, err } +// HasRated is the resolver for the hasRated field. +func (r *recipeRevisionResolver) HasRated(ctx context.Context, obj *model.RecipeRevision) (*bool, error) { + user, err := r.UserService.GetCurrent(ctx) + if err != nil { + return nil, err + } + + if user == nil || obj == nil { + return nil, nil + } + result, err := r.RecipeService.HasUserRatedRevision(ctx, user.ID, obj.ID) + return &result, err +} + // Revision is the resolver for the revision field. func (r *recipeStepResolver) Revision(ctx context.Context, obj *model.RecipeStep) (*model.RecipeRevision, error) { return r.RecipeService.GetRecipeRevisionById(ctx, obj.Revision.ID) diff --git a/api/graph/schema/mutation.graphql b/api/graph/schema/mutation.graphql index eac4731..46a812e 100644 --- a/api/graph/schema/mutation.graphql +++ b/api/graph/schema/mutation.graphql @@ -22,6 +22,9 @@ type RecipeMutation { addRevision(input: AddRevisionInput!): RecipeRevision! @auth(required: true) @goField(forceResolver: true) + addRating(input: AddRatingInput!): Boolean! + @auth(required: true) + @goField(forceResolver: true) } type LoginResponse { @@ -70,3 +73,8 @@ input CreateRecipeRevisionStep { step: Int! photo: String } + +input AddRatingInput { + revisionId: UUID! + starValue: Int! +} diff --git a/api/graph/schema/recipe.graphql b/api/graph/schema/recipe.graphql index 550e0a6..5f22509 100644 --- a/api/graph/schema/recipe.graphql +++ b/api/graph/schema/recipe.graphql @@ -22,6 +22,7 @@ type RecipeRevision { steps: [RecipeStep!]! @goField(forceResolver: true) rating: Float @goField(forceResolver: true) photo: String + hasRated: Boolean @auth @goField(forceResolver: true) } type RecipeStep { diff --git a/api/services/recipe/recipe.go b/api/services/recipe/recipe.go index 52d77b2..49391ac 100644 --- a/api/services/recipe/recipe.go +++ b/api/services/recipe/recipe.go @@ -37,7 +37,9 @@ type RecipeService interface { ListRecipeSteps(ctx context.Context, id uuid.UUID) ([]*model.RecipeStep, error) CreateRecipe(ctx context.Context, input model.CreateRecipeInput) (*model.Recipe, error) AddRecipeRevision(ctx context.Context, input model.AddRevisionInput) (*model.RecipeRevision, error) - GetRevisionRating(ctx context.Context, revision_id uuid.UUID) (float64, error) + GetRevisionRating(ctx context.Context, revisionId uuid.UUID) (float64, error) + AddRevisionRating(ctx context.Context, userId uuid.UUID, revisionId uuid.UUID, starValue int) (bool, error) + HasUserRatedRevision(ctx context.Context, userId uuid.UUID, revisionId uuid.UUID) (bool, error) } type recipeService struct { @@ -47,6 +49,44 @@ type recipeService struct { storageService object_storage.ObjectStorageService } +// HasUserRatedRevision implements RecipeService. +func (r recipeService) HasUserRatedRevision(ctx context.Context, userId uuid.UUID, revisionId uuid.UUID) (bool, error) { + arg := db.HasUserRatedRevisionParams{ + RevisionID: pgtype.UUID{ + Bytes: revisionId, + Valid: true, + }, + UserID: pgtype.UUID{ + Bytes: userId, + Valid: true, + }, + } + return r.queries.HasUserRatedRevision(ctx, arg) +} + +// AddRevisionRating implements RecipeService. +func (r recipeService) AddRevisionRating(ctx context.Context, userId uuid.UUID, revisionId uuid.UUID, starValue int) (bool, error) { + if starValue < 0 || starValue > 5 { + return false, fmt.Errorf("invalid star value: %d. Must be an integer between 1 and 5 inclusive", starValue) + } + arg := db.UpsertRatingParams{ + RevisionID: pgtype.UUID{ + Bytes: revisionId, + Valid: true, + }, + UserID: pgtype.UUID{ + Bytes: userId, + Valid: true, + }, + StarValue: int16(starValue), + } + _, err := r.queries.UpsertRating(ctx, arg) + if err != nil { + return false, err + } + return true, nil +} + // GetRevisionRating implements RecipeService. func (r recipeService) GetRevisionRating(ctx context.Context, revisionId uuid.UUID) (float64, error) { pgId := pgtype.UUID{ diff --git a/api/services/user/user.go b/api/services/user/user.go index e5fe1f2..3624b3e 100644 --- a/api/services/user/user.go +++ b/api/services/user/user.go @@ -39,6 +39,9 @@ func (u userService) Create(ctx context.Context, email string, displayName strin // GetCurrent implements UserService. func (u userService) GetCurrent(ctx context.Context) (*model.User, error) { user, _ := u.authService.GetUserSessionFromCtx(ctx) + if user == nil { + return nil, nil + } return model.UserFromDBType(*user), nil } diff --git a/db/queries/revision/has_user_rated_revision.sql b/db/queries/revision/has_user_rated_revision.sql new file mode 100644 index 0000000..3dd3de8 --- /dev/null +++ b/db/queries/revision/has_user_rated_revision.sql @@ -0,0 +1,9 @@ +-- name: HasUserRatedRevision :one +SELECT EXISTS( + SELECT star_value + FROM ratings + WHERE + revision_id = sqlc.arg('revision_id')::uuid + AND + user_id = sqlc.arg('user_id')::uuid +); From 10b4f40a9e8ba69aa5e506477e43478a2da38e1a Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Fri, 18 Jul 2025 21:41:25 -0400 Subject: [PATCH 2/7] refactor: running sqlfluff, need to add task for it --- .../get_user_and_session_by_session_id.sql.go | 4 +- ...t_ingredients_by_recipe_revision_id.sql.go | 4 +- .../list_steps_by_recipe_revision_id.sql.go | 4 +- api/db/querier.go | 6 +- api/db/update_user.sql.go | 8 +- db/queries/auth/create_magic_link.sql | 14 +- db/queries/auth/create_session.sql | 27 +-- db/queries/auth/delete_magic_link.sql | 4 +- db/queries/auth/delete_session.sql | 4 +- db/queries/auth/get_magic_link.sql | 12 +- .../get_user_and_session_by_session_id.sql | 12 +- .../ingredient/get_ingredient_by_id.sql | 10 +- db/queries/ingredient/upsert_ingredient.sql | 31 ++-- .../get_measurement_unit_by_id.sql | 10 +- .../upsert_measurement_unit.sql | 31 ++-- db/queries/recipe/create_recipe.sql | 30 ++-- db/queries/recipe/get_recipe_by_id.sql | 18 +- db/queries/recipe/get_recipe_by_slug.sql | 26 +-- db/queries/recipe/list_recipes.sql | 159 ++++++++++++------ db/queries/recipe/seed_recipe.sql | 34 ++-- db/queries/recipe/update_recipe.sql | 23 +-- .../revision/create_recipe_ingredient.sql | 24 +-- db/queries/revision/create_recipe_step.sql | 22 +-- db/queries/revision/create_revision.sql | 40 ++--- ...et_latest_recipe_revision_by_recipe_id.sql | 22 +-- .../revision/get_recipe_revision_by_id.sql | 20 +-- ...list_ingredients_by_recipe_revision_id.sql | 20 +-- db/queries/revision/list_revisions.sql | 106 +++++++----- .../list_steps_by_recipe_revision_id.sql | 20 +-- db/queries/revision/seed_revision.sql | 44 ++--- db/queries/revision/upsert_rating.sql | 20 +-- db/queries/user/create_user.sql | 20 +-- db/queries/user/get_user_by_display_name.sql | 11 +- db/queries/user/get_user_by_email.sql | 11 +- db/queries/user/get_user_by_id.sql | 11 +- db/queries/user/seed_user.sql | 22 +-- db/queries/user/update_user.sql | 19 ++- 37 files changed, 507 insertions(+), 396 deletions(-) diff --git a/api/db/get_user_and_session_by_session_id.sql.go b/api/db/get_user_and_session_by_session_id.sql.go index 792caf1..acf69aa 100644 --- a/api/db/get_user_and_session_by_session_id.sql.go +++ b/api/db/get_user_and_session_by_session_id.sql.go @@ -29,8 +29,8 @@ type GetUserBySessionIdRow struct { Session Session } -func (q *Queries) GetUserBySessionId(ctx context.Context, id pgtype.UUID) (GetUserBySessionIdRow, error) { - row := q.db.QueryRow(ctx, getUserBySessionId, id) +func (q *Queries) GetUserBySessionId(ctx context.Context, sessionID pgtype.UUID) (GetUserBySessionIdRow, error) { + row := q.db.QueryRow(ctx, getUserBySessionId, sessionID) var i GetUserBySessionIdRow err := row.Scan( &i.User.ID, diff --git a/api/db/list_ingredients_by_recipe_revision_id.sql.go b/api/db/list_ingredients_by_recipe_revision_id.sql.go index a775fb4..a620d61 100644 --- a/api/db/list_ingredients_by_recipe_revision_id.sql.go +++ b/api/db/list_ingredients_by_recipe_revision_id.sql.go @@ -28,8 +28,8 @@ WHERE ORDER BY recipe_ingredients.id ` -func (q *Queries) ListIngredientsByRecipeRevisionID(ctx context.Context, id pgtype.UUID) ([]RecipeIngredient, error) { - rows, err := q.db.Query(ctx, listIngredientsByRecipeRevisionID, id) +func (q *Queries) ListIngredientsByRecipeRevisionID(ctx context.Context, revisionID pgtype.UUID) ([]RecipeIngredient, error) { + rows, err := q.db.Query(ctx, listIngredientsByRecipeRevisionID, revisionID) if err != nil { return nil, err } diff --git a/api/db/list_steps_by_recipe_revision_id.sql.go b/api/db/list_steps_by_recipe_revision_id.sql.go index 3ec9fc3..5c34590 100644 --- a/api/db/list_steps_by_recipe_revision_id.sql.go +++ b/api/db/list_steps_by_recipe_revision_id.sql.go @@ -28,8 +28,8 @@ ORDER BY recipe_steps.index ` -func (q *Queries) ListStepsByRecipeRevisionID(ctx context.Context, id pgtype.UUID) ([]RecipeStep, error) { - rows, err := q.db.Query(ctx, listStepsByRecipeRevisionID, id) +func (q *Queries) ListStepsByRecipeRevisionID(ctx context.Context, revisionID pgtype.UUID) ([]RecipeStep, error) { + rows, err := q.db.Query(ctx, listStepsByRecipeRevisionID, revisionID) if err != nil { return nil, err } diff --git a/api/db/querier.go b/api/db/querier.go index 7304a01..d95c409 100644 --- a/api/db/querier.go +++ b/api/db/querier.go @@ -31,16 +31,16 @@ type Querier interface { GetUserByDisplayName(ctx context.Context, displayName string) (User, error) GetUserByEmail(ctx context.Context, email string) (User, error) GetUserById(ctx context.Context, id pgtype.UUID) (User, error) - GetUserBySessionId(ctx context.Context, id pgtype.UUID) (GetUserBySessionIdRow, error) + GetUserBySessionId(ctx context.Context, sessionID pgtype.UUID) (GetUserBySessionIdRow, error) HasUserRatedRevision(ctx context.Context, arg HasUserRatedRevisionParams) (bool, error) - ListIngredientsByRecipeRevisionID(ctx context.Context, id pgtype.UUID) ([]RecipeIngredient, error) + ListIngredientsByRecipeRevisionID(ctx context.Context, revisionID pgtype.UUID) ([]RecipeIngredient, error) ListRecipes(ctx context.Context, arg ListRecipesParams) ([]Recipe, error) //Used when there is not a featured recipe //and recipe_id = 'ae1f8b91-3659-4f7e-b484-dd54e7f3d2b3' //Used when there is a featured recipe ListRecipesWithQuery(ctx context.Context, arg ListRecipesWithQueryParams) ([]ListRecipesWithQueryRow, error) ListRevisions(ctx context.Context, arg ListRevisionsParams) ([]RecipeRevision, error) - ListStepsByRecipeRevisionID(ctx context.Context, id pgtype.UUID) ([]RecipeStep, error) + ListStepsByRecipeRevisionID(ctx context.Context, revisionID pgtype.UUID) ([]RecipeStep, error) SeedRecipe(ctx context.Context, arg SeedRecipeParams) (Recipe, error) SeedRevision(ctx context.Context, arg SeedRevisionParams) (RecipeRevision, error) SeedUser(ctx context.Context, arg SeedUserParams) (User, error) diff --git a/api/db/update_user.sql.go b/api/db/update_user.sql.go index 3b59551..6ee724b 100644 --- a/api/db/update_user.sql.go +++ b/api/db/update_user.sql.go @@ -13,8 +13,8 @@ import ( const updateUser = `-- name: UpdateUser :one UPDATE users -SET display_name = $2, email = $3, photo = $4 -WHERE id = $1 +SET display_name = $1, email = $2, photo = $3 +WHERE id = $4 RETURNING id, display_name, @@ -25,18 +25,18 @@ RETURNING ` type UpdateUserParams struct { - ID pgtype.UUID DisplayName string Email string Photo pgtype.Text + ID pgtype.UUID } func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) { row := q.db.QueryRow(ctx, updateUser, - arg.ID, arg.DisplayName, arg.Email, arg.Photo, + arg.ID, ) var i User err := row.Scan( diff --git a/db/queries/auth/create_magic_link.sql b/db/queries/auth/create_magic_link.sql index 23ed5ec..30a0a42 100644 --- a/db/queries/auth/create_magic_link.sql +++ b/db/queries/auth/create_magic_link.sql @@ -1,15 +1,15 @@ -- name: CreateMagicLink :one INSERT INTO - magic_links ( +magic_links ( user_id, token, expiry - ) +) VALUES ( - $1, - $2, - $3 + sqlc.arg('user_id'), + sqlc.arg('token'), + sqlc.arg('expiry') ) RETURNING - magic_links.id, - magic_links.token; + magic_links.id, + magic_links.token; diff --git a/db/queries/auth/create_session.sql b/db/queries/auth/create_session.sql index 74dad78..187c717 100644 --- a/db/queries/auth/create_session.sql +++ b/db/queries/auth/create_session.sql @@ -1,16 +1,21 @@ -- name: CreateSession :one WITH sesh AS ( - INSERT INTO + INSERT INTO sessions ( - user_id, - expiry + user_id, + expiry ) - VALUES ( - $1, - $2 - ) - RETURNING - sessions.id, - sessions.user_id + VALUES ( + sqlc.arg('user_id'), + sqlc.arg('expiry') + ) + RETURNING + sessions.id, + sessions.user_id ) -SELECT sqlc.embed(users), sesh.id FROM sesh INNER JOIN users ON sesh.user_id = users.id; + +SELECT + sesh.id, + sqlc.embed(users) AS user -- noqa: RF02,RF04 +FROM sesh +INNER JOIN users ON sesh.user_id = users.id; diff --git a/db/queries/auth/delete_magic_link.sql b/db/queries/auth/delete_magic_link.sql index 93f805c..9221296 100644 --- a/db/queries/auth/delete_magic_link.sql +++ b/db/queries/auth/delete_magic_link.sql @@ -1,4 +1,4 @@ -- name: DeleteMagicLinkById :exec DELETE FROM - magic_links -WHERE magic_links.id = $1; +magic_links +WHERE magic_links.id = sqlc.arg('id'); diff --git a/db/queries/auth/delete_session.sql b/db/queries/auth/delete_session.sql index ac5fad7..5634ed7 100644 --- a/db/queries/auth/delete_session.sql +++ b/db/queries/auth/delete_session.sql @@ -1,4 +1,4 @@ -- name: DeleteSession :exec DELETE FROM - sessions -WHERE sessions.id = $1; +sessions +WHERE sessions.id = sqlc.arg('id'); diff --git a/db/queries/auth/get_magic_link.sql b/db/queries/auth/get_magic_link.sql index 7b70dd3..f0df039 100644 --- a/db/queries/auth/get_magic_link.sql +++ b/db/queries/auth/get_magic_link.sql @@ -1,10 +1,10 @@ -- name: GetMagicLink :one SELECT - magic_links.id, - magic_links.token, - magic_links.user_id, - magic_links.expiry + magic_links.id, + magic_links.token, + magic_links.user_id, + magic_links.expiry FROM - magic_links + magic_links WHERE - magic_links.id = $1 AND magic_links.token = $2; + magic_links.id = sqlc.arg('id') AND magic_links.token = sqlc.arg('token'); diff --git a/db/queries/auth/get_user_and_session_by_session_id.sql b/db/queries/auth/get_user_and_session_by_session_id.sql index 786d9eb..52165c3 100644 --- a/db/queries/auth/get_user_and_session_by_session_id.sql +++ b/db/queries/auth/get_user_and_session_by_session_id.sql @@ -1,11 +1,11 @@ -- name: GetUserBySessionId :one SELECT - sqlc.embed(users), - sqlc.embed(sessions) + sqlc.embed(users) AS user, -- noqa: RF02,RF04 + sqlc.embed(sessions) AS session -- noqa: RF02,RF04 FROM - sessions -JOIN - users ON users.id = sessions.user_id + sessions +INNER JOIN + users ON sessions.user_id = users.id WHERE - sessions.id = $1 + sessions.id = sqlc.arg('session_id') LIMIT 1; diff --git a/db/queries/ingredient/get_ingredient_by_id.sql b/db/queries/ingredient/get_ingredient_by_id.sql index 580f53a..7773150 100644 --- a/db/queries/ingredient/get_ingredient_by_id.sql +++ b/db/queries/ingredient/get_ingredient_by_id.sql @@ -1,10 +1,10 @@ -- name: GetIngredientById :one SELECT - ingredients.id, - ingredients.name, - ingredients.description + ingredients.id, + ingredients.name, + ingredients.description FROM - ingredients + ingredients WHERE - ingredients.id = $1 + ingredients.id = sqlc.arg('id') LIMIT 1; diff --git a/db/queries/ingredient/upsert_ingredient.sql b/db/queries/ingredient/upsert_ingredient.sql index 466a6f1..7a8de7e 100644 --- a/db/queries/ingredient/upsert_ingredient.sql +++ b/db/queries/ingredient/upsert_ingredient.sql @@ -1,28 +1,29 @@ -- name: UpsertIngredient :one WITH upsert AS ( - INSERT INTO + INSERT INTO ingredients ( - name + name ) - VALUES ( - $1 - ) - ON CONFLICT (name) - DO NOTHING - RETURNING - ingredients.id, - ingredients.name + VALUES ( + sqlc.arg('name') + ) + ON CONFLICT (name) + DO NOTHING + RETURNING + ingredients.id, + ingredients.name ) + SELECT - upsert.id, - upsert.name + upsert.id, + upsert.name FROM - upsert + upsert UNION SELECT ingredients.id, ingredients.name FROM - ingredients + ingredients WHERE - ingredients.name = $1; + ingredients.name = sqlc.arg('name'); diff --git a/db/queries/measurement_unit/get_measurement_unit_by_id.sql b/db/queries/measurement_unit/get_measurement_unit_by_id.sql index 6e5d16c..ce6d007 100644 --- a/db/queries/measurement_unit/get_measurement_unit_by_id.sql +++ b/db/queries/measurement_unit/get_measurement_unit_by_id.sql @@ -1,10 +1,10 @@ -- name: GetMeasurementUnitById :one SELECT - measurement_units.id, - measurement_units.name, - measurement_units.description + measurement_units.id, + measurement_units.name, + measurement_units.description FROM - measurement_units + measurement_units WHERE - measurement_units.id = $1 + measurement_units.id = sqlc.arg('id') LIMIT 1; diff --git a/db/queries/measurement_unit/upsert_measurement_unit.sql b/db/queries/measurement_unit/upsert_measurement_unit.sql index b5d7803..ecf919f 100644 --- a/db/queries/measurement_unit/upsert_measurement_unit.sql +++ b/db/queries/measurement_unit/upsert_measurement_unit.sql @@ -1,28 +1,29 @@ -- name: UpsertMeasurementUnit :one WITH upsert AS ( - INSERT INTO + INSERT INTO measurement_units ( - name + name ) - VALUES ( - $1 - ) - ON CONFLICT (name) - DO NOTHING - RETURNING - measurement_units.id, - measurement_units.name + VALUES ( + sqlc.arg('name') + ) + ON CONFLICT (name) + DO NOTHING + RETURNING + measurement_units.id, + measurement_units.name ) + SELECT - upsert.id, - upsert.name + upsert.id, + upsert.name FROM - upsert + upsert UNION SELECT measurement_units.id, measurement_units.name FROM - measurement_units + measurement_units WHERE - measurement_units.name = $1; + measurement_units.name = sqlc.arg('name'); diff --git a/db/queries/recipe/create_recipe.sql b/db/queries/recipe/create_recipe.sql index edbe15f..d4ec46f 100644 --- a/db/queries/recipe/create_recipe.sql +++ b/db/queries/recipe/create_recipe.sql @@ -1,19 +1,19 @@ -- name: CreateRecipe :one INSERT INTO recipes ( - author_id, - forked_from, - slug, - private + author_id, + forked_from, + slug, + private ) VALUES ( - $1, - $2, - $3, - $4 + sqlc.arg('author_id'), + sqlc.arg('forked_from'), + sqlc.arg('slug'), + sqlc.arg('private') ) RETURNING - id, - author_id, - slug, - private, - initial_publish_date, - forked_from, - featured_revision; + id, + author_id, + slug, + private, + initial_publish_date, + forked_from, + featured_revision; diff --git a/db/queries/recipe/get_recipe_by_id.sql b/db/queries/recipe/get_recipe_by_id.sql index ed4953d..9575f47 100644 --- a/db/queries/recipe/get_recipe_by_id.sql +++ b/db/queries/recipe/get_recipe_by_id.sql @@ -1,14 +1,14 @@ -- name: GetRecipeById :one SELECT - id, - author_id, - slug, - private, - initial_publish_date, - forked_from, - featured_revision + id, + author_id, + slug, + private, + initial_publish_date, + forked_from, + featured_revision FROM - recipes + recipes WHERE - id = $1 + id = sqlc.arg('id') LIMIT 1; diff --git a/db/queries/recipe/get_recipe_by_slug.sql b/db/queries/recipe/get_recipe_by_slug.sql index 5a450cb..eec4954 100644 --- a/db/queries/recipe/get_recipe_by_slug.sql +++ b/db/queries/recipe/get_recipe_by_slug.sql @@ -1,18 +1,18 @@ -- name: GetRecipeBySlug :one SELECT - recipes.id, - recipes.author_id, - recipes.slug, - recipes.private, - recipes.initial_publish_date, - recipes.forked_from, - recipes.featured_revision + recipes.id, + recipes.author_id, + recipes.slug, + recipes.private, + recipes.initial_publish_date, + recipes.forked_from, + recipes.featured_revision FROM - recipes -JOIN - users ON users.id = recipes.author_id + recipes +INNER JOIN + users ON recipes.author_id = users.id WHERE - lower(recipes.slug) = lower(sqlc.arg(slug)::text) - AND - lower(users.display_name) = lower(sqlc.arg(display_name)::text) + LOWER(recipes.slug) = LOWER(sqlc.arg('slug')::text) + AND + LOWER(users.display_name) = LOWER(sqlc.arg('display_name')::text) LIMIT 1; diff --git a/db/queries/recipe/list_recipes.sql b/db/queries/recipe/list_recipes.sql index 55c4ee8..c66f96e 100644 --- a/db/queries/recipe/list_recipes.sql +++ b/db/queries/recipe/list_recipes.sql @@ -1,62 +1,109 @@ -- name: ListRecipes :many SELECT - id, - author_id, - slug, - private, - initial_publish_date, - forked_from, - featured_revision + id, + author_id, + slug, + private, + initial_publish_date, + forked_from, + featured_revision FROM - recipes + recipes WHERE - CASE - WHEN sqlc.narg('author_id')::uuid IS NOT NULL THEN author_id = sqlc.narg('author_id')::uuid - ELSE true - END - AND - CASE - WHEN sqlc.narg('forked_from')::uuid IS NOT NULL THEN forked_from = sqlc.narg('forked_from')::uuid - ELSE true - END - AND - CASE - WHEN sqlc.narg('current_user')::uuid IS NOT NULL AND sqlc.narg('private')::bool IS NOT NULL AND sqlc.narg('private')::bool THEN author_id = sqlc.narg('current_user')::uuid AND private = true - ELSE private = false OR private IS NULL - END - AND - CASE - WHEN sqlc.narg('publish_start')::timestamp IS NOT NULL THEN initial_publish_date >= sqlc.narg('publish_start')::timestamp - ELSE true - END - AND - CASE - WHEN sqlc.narg('publish_end')::timestamp IS NOT NULL THEN initial_publish_date <= sqlc.narg('publish_end')::timestamp - ELSE true - END - AND - CASE - WHEN sqlc.arg('sort_col')::text = 'publish_date' AND sqlc.arg('sort_dir')::bool AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL THEN sqlc.narg('publish_cursor')::timestamp > initial_publish_date - ELSE true - END - AND - CASE - WHEN NOT sqlc.arg('sort_dir')::bool AND sqlc.arg('sort_col')::text = 'publish_date' AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL THEN sqlc.narg('publish_cursor')::timestamp < initial_publish_date - ELSE true - END - AND - CASE - WHEN sqlc.arg('sort_col')::text = 'slug' AND sqlc.arg('sort_dir')::bool AND sqlc.narg('slug_cursor')::text IS NOT NULL THEN sqlc.narg('slug_cursor')::text > slug - ELSE true - END - AND - CASE - WHEN NOT sqlc.arg('sort_dir')::bool AND sqlc.arg('sort_col')::text = 'slug' AND sqlc.narg('slug_cursor')::text IS NOT NULL THEN sqlc.narg('slug_cursor')::text < slug - ELSE true - END + CASE + WHEN + sqlc.narg('author_id')::uuid IS NOT NULL + THEN author_id = sqlc.narg('author_id')::uuid + ELSE TRUE + END + AND + CASE + WHEN + sqlc.narg('forked_from')::uuid IS NOT NULL + THEN forked_from = sqlc.narg('forked_from')::uuid + ELSE TRUE + END + AND + CASE + WHEN + sqlc.narg('current_user')::uuid IS NOT NULL + AND sqlc.narg('private')::bool IS NOT NULL + AND sqlc.narg('private')::bool + THEN author_id = sqlc.narg('current_user')::uuid AND private = TRUE + ELSE private = FALSE OR private IS NULL + END + AND + CASE + WHEN + sqlc.narg('publish_start')::timestamp IS NOT NULL + THEN initial_publish_date >= sqlc.narg('publish_start')::timestamp + ELSE TRUE + END + AND + CASE + WHEN + sqlc.narg('publish_end')::timestamp IS NOT NULL + THEN initial_publish_date <= sqlc.narg('publish_end')::timestamp + ELSE TRUE + END + AND + CASE + WHEN + sqlc.arg('sort_col')::text = 'publish_date' + AND sqlc.arg('sort_dir')::bool + AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL + THEN sqlc.narg('publish_cursor')::timestamp > initial_publish_date + ELSE TRUE + END + AND + CASE + WHEN + NOT sqlc.arg('sort_dir')::bool + AND sqlc.arg('sort_col')::text = 'publish_date' + AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL + THEN sqlc.narg('publish_cursor')::timestamp < initial_publish_date + ELSE TRUE + END + AND + CASE + WHEN + sqlc.arg('sort_col')::text = 'slug' + AND sqlc.arg('sort_dir')::bool + AND sqlc.narg('slug_cursor')::text IS NOT NULL + THEN sqlc.narg('slug_cursor')::text > slug + ELSE TRUE + END + AND + CASE + WHEN + NOT sqlc.arg('sort_dir')::bool + AND sqlc.arg('sort_col')::text = 'slug' + AND sqlc.narg('slug_cursor')::text IS NOT NULL + THEN sqlc.narg('slug_cursor')::text < slug + ELSE TRUE + END ORDER BY - CASE WHEN sqlc.arg('sort_col')::text = 'publish_date' AND sqlc.arg('sort_dir')::bool THEN initial_publish_date END DESC, - CASE WHEN sqlc.arg('sort_col')::text = 'publish_date' AND NOT sqlc.arg('sort_dir')::bool THEN initial_publish_date END ASC, - CASE WHEN sqlc.arg('sort_col')::text = 'slug' AND sqlc.arg('sort_dir')::bool THEN slug END DESC, - CASE WHEN sqlc.arg('sort_col')::text = 'slug' AND NOT sqlc.arg('sort_dir')::bool THEN slug END ASC + CASE + WHEN + sqlc.arg('sort_col')::text = 'publish_date' + AND sqlc.arg('sort_dir')::bool + THEN initial_publish_date + END DESC, + CASE + WHEN + sqlc.arg('sort_col')::text = 'publish_date' + AND NOT sqlc.arg('sort_dir')::bool + THEN initial_publish_date + END ASC, + CASE + WHEN + sqlc.arg('sort_col')::text = 'slug' AND sqlc.arg('sort_dir')::bool + THEN slug + END DESC, + CASE + WHEN + sqlc.arg('sort_col')::text = 'slug' + AND NOT sqlc.arg('sort_dir')::bool + THEN slug + END ASC LIMIT sqlc.arg('limit'); diff --git a/db/queries/recipe/seed_recipe.sql b/db/queries/recipe/seed_recipe.sql index 89c2e37..c15a196 100644 --- a/db/queries/recipe/seed_recipe.sql +++ b/db/queries/recipe/seed_recipe.sql @@ -1,21 +1,21 @@ -- name: SeedRecipe :one INSERT INTO recipes ( - author_id, - forked_from, - slug, - private, - initial_publish_date + author_id, + forked_from, + slug, + private, + initial_publish_date ) VALUES ( - $1, - $2, - $3, - $4, - $5 + sqlc.arg('author_id'), + sqlc.arg('forked_from'), + sqlc.arg('slug'), + sqlc.arg('private'), + sqlc.arg('initial_publish_date') ) RETURNING - id, - author_id, - slug, - private, - initial_publish_date, - forked_from, - featured_revision; + id, + author_id, + slug, + private, + initial_publish_date, + forked_from, + featured_revision; diff --git a/db/queries/recipe/update_recipe.sql b/db/queries/recipe/update_recipe.sql index d0f5af5..9e56a8d 100644 --- a/db/queries/recipe/update_recipe.sql +++ b/db/queries/recipe/update_recipe.sql @@ -1,15 +1,16 @@ -- name: UpdateRecipe :one UPDATE recipes SET - slug = coalesce($1, slug), - private = coalesce($2, private), - featured_revision = coalesce($3, featured_revision) -WHERE id = $4 + slug = coalesce(sqlc.arg('slug'), slug), + private = coalesce(sqlc.arg('private'), private), + featured_revision + = coalesce(sqlc.arg('featured_revision'), featured_revision) +WHERE id = sqlc.arg('id') RETURNING - id, - author_id, - slug, - private, - initial_publish_date, - forked_from, - featured_revision; + id, + author_id, + slug, + private, + initial_publish_date, + forked_from, + featured_revision; diff --git a/db/queries/revision/create_recipe_ingredient.sql b/db/queries/revision/create_recipe_ingredient.sql index d1c96ab..319c9e5 100644 --- a/db/queries/revision/create_recipe_ingredient.sql +++ b/db/queries/revision/create_recipe_ingredient.sql @@ -1,6 +1,6 @@ -- name: CreateRecipeIngredient :one INSERT INTO - recipe_ingredients ( +recipe_ingredients ( revision_id, ingredient_id, measurement_unit_id, @@ -8,16 +8,16 @@ INSERT INTO comment ) VALUES ( - $1, - $2, - $3, - $4, - $5 + sqlc.arg('revision_id'), + sqlc.arg('ingredient_id'), + sqlc.arg('measurement_unit_id'), + sqlc.arg('quantity'), + sqlc.arg('comment') ) RETURNING - recipe_ingredients.id, - recipe_ingredients.revision_id, - recipe_ingredients.ingredient_id, - recipe_ingredients.quantity, - recipe_ingredients.measurement_unit_id, - recipe_ingredients.comment; + recipe_ingredients.id, + recipe_ingredients.revision_id, + recipe_ingredients.ingredient_id, + recipe_ingredients.quantity, + recipe_ingredients.measurement_unit_id, + recipe_ingredients.comment; diff --git a/db/queries/revision/create_recipe_step.sql b/db/queries/revision/create_recipe_step.sql index 5989d93..65883e3 100644 --- a/db/queries/revision/create_recipe_step.sql +++ b/db/queries/revision/create_recipe_step.sql @@ -1,20 +1,20 @@ -- name: CreateRecipeStep :one INSERT INTO - recipe_steps ( +recipe_steps ( revision_id, content, index, photo - ) +) VALUES ( - $1, - $2, - $3, - $4 + sqlc.arg('revision_id'), + sqlc.arg('content'), + sqlc.arg('index'), + sqlc.arg('photo') ) RETURNING - id, - revision_id, - content, - index, - photo; + id, + revision_id, + content, + index, + photo; diff --git a/db/queries/revision/create_revision.sql b/db/queries/revision/create_revision.sql index 1b0d661..6a0d5cd 100644 --- a/db/queries/revision/create_revision.sql +++ b/db/queries/revision/create_revision.sql @@ -1,26 +1,26 @@ -- name: CreateRevision :one INSERT INTO recipe_revisions ( - recipe_id, - parent_id, - recipe_description, - change_comment, - title, - photo + recipe_id, + parent_id, + recipe_description, + change_comment, + title, + photo ) VALUES ( - $1, - $2, - $3, - $4, - $5, - $6 + sqlc.arg('recipe_id'), + sqlc.arg('parent_id'), + sqlc.arg('recipe_description'), + sqlc.arg('change_comment'), + sqlc.arg('title'), + sqlc.arg('photo') ) RETURNING - id, - recipe_id, - parent_id, - recipe_description, - change_comment, - title, - publish_date, - photo; + id, + recipe_id, + parent_id, + recipe_description, + change_comment, + title, + publish_date, + photo; diff --git a/db/queries/revision/get_latest_recipe_revision_by_recipe_id.sql b/db/queries/revision/get_latest_recipe_revision_by_recipe_id.sql index e169ec3..111744d 100644 --- a/db/queries/revision/get_latest_recipe_revision_by_recipe_id.sql +++ b/db/queries/revision/get_latest_recipe_revision_by_recipe_id.sql @@ -1,17 +1,17 @@ -- name: GetLatestRecipeRevisionByRecipeId :one SELECT - id, - recipe_id, - parent_id, - recipe_description, - change_comment, - title, - publish_date, - photo + id, + recipe_id, + parent_id, + recipe_description, + change_comment, + title, + publish_date, + photo FROM - recipe_revisions + recipe_revisions WHERE - recipe_id = $1 + recipe_id = sqlc.arg('recipe_id') ORDER BY - publish_date DESC + publish_date DESC LIMIT 1; diff --git a/db/queries/revision/get_recipe_revision_by_id.sql b/db/queries/revision/get_recipe_revision_by_id.sql index 42bab44..651a592 100644 --- a/db/queries/revision/get_recipe_revision_by_id.sql +++ b/db/queries/revision/get_recipe_revision_by_id.sql @@ -1,15 +1,15 @@ -- name: GetRecipeRevisionById :one SELECT - id, - recipe_id, - parent_id, - recipe_description, - change_comment, - title, - publish_date, - photo + id, + recipe_id, + parent_id, + recipe_description, + change_comment, + title, + publish_date, + photo FROM - recipe_revisions + recipe_revisions WHERE - id = $1 + id = sqlc.arg('id') LIMIT 1; diff --git a/db/queries/revision/list_ingredients_by_recipe_revision_id.sql b/db/queries/revision/list_ingredients_by_recipe_revision_id.sql index aa695a4..33ff0b2 100644 --- a/db/queries/revision/list_ingredients_by_recipe_revision_id.sql +++ b/db/queries/revision/list_ingredients_by_recipe_revision_id.sql @@ -1,15 +1,15 @@ -- name: ListIngredientsByRecipeRevisionID :many SELECT - recipe_ingredients.id, - recipe_ingredients.revision_id, - recipe_ingredients.ingredient_id, - recipe_ingredients.quantity, - recipe_ingredients.measurement_unit_id, - recipe_ingredients.comment + recipe_ingredients.id, + recipe_ingredients.revision_id, + recipe_ingredients.ingredient_id, + recipe_ingredients.quantity, + recipe_ingredients.measurement_unit_id, + recipe_ingredients.comment FROM - recipe_revisions -JOIN - recipe_ingredients ON recipe_revisions.id = recipe_ingredients.revision_id + recipe_revisions +INNER JOIN + recipe_ingredients ON recipe_revisions.id = recipe_ingredients.revision_id WHERE - recipe_revisions.id = $1 + recipe_revisions.id = sqlc.arg('revision_id') ORDER BY recipe_ingredients.id; diff --git a/db/queries/revision/list_revisions.sql b/db/queries/revision/list_revisions.sql index d21511d..5617e3b 100644 --- a/db/queries/revision/list_revisions.sql +++ b/db/queries/revision/list_revisions.sql @@ -1,46 +1,72 @@ -- name: ListRevisions :many SELECT - id, - recipe_id, - parent_id, - recipe_description, - change_comment, - title, - publish_date, - photo + id, + recipe_id, + parent_id, + recipe_description, + change_comment, + title, + publish_date, + photo FROM - recipe_revisions + recipe_revisions WHERE - CASE - WHEN sqlc.narg('recipe_id')::uuid IS NOT NULL THEN sqlc.narg('recipe_id')::uuid = recipe_id - ELSE true - END - AND - CASE - WHEN sqlc.narg('parent_id')::uuid IS NOT NULL THEN sqlc.narg('parent_id')::uuid = parent_id - ELSE true - END - AND - CASE - WHEN sqlc.narg('publish_start')::timestamp IS NOT NULL THEN publish_date >= sqlc.narg('publish_start')::timestamp - ELSE true - END - AND - CASE - WHEN sqlc.narg('publish_end')::timestamp IS NOT NULL THEN publish_date <= sqlc.narg('publish_end')::timestamp - ELSE true - END - AND - CASE - WHEN sqlc.arg('sort_col')::text = 'publish_date' AND sqlc.arg('sort_dir')::bool AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL THEN sqlc.narg('publish_cursor')::timestamp > publish_date - ELSE true - END - AND - CASE - WHEN NOT sqlc.arg('sort_dir')::bool AND sqlc.arg('sort_col')::text = 'publish_date' AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL THEN sqlc.narg('publish_cursor')::timestamp < publish_date - ELSE true - END + CASE + WHEN + sqlc.narg('recipe_id')::uuid IS NOT NULL + THEN sqlc.narg('recipe_id')::uuid = recipe_id + ELSE TRUE + END + AND + CASE + WHEN + sqlc.narg('parent_id')::uuid IS NOT NULL + THEN sqlc.narg('parent_id')::uuid = parent_id + ELSE TRUE + END + AND + CASE + WHEN + sqlc.narg('publish_start')::timestamp IS NOT NULL + THEN publish_date >= sqlc.narg('publish_start')::timestamp + ELSE TRUE + END + AND + CASE + WHEN + sqlc.narg('publish_end')::timestamp IS NOT NULL + THEN publish_date <= sqlc.narg('publish_end')::timestamp + ELSE TRUE + END + AND + CASE + WHEN + sqlc.arg('sort_col')::text = 'publish_date' + AND sqlc.arg('sort_dir')::bool + AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL + THEN sqlc.narg('publish_cursor')::timestamp > publish_date + ELSE TRUE + END + AND + CASE + WHEN + NOT sqlc.arg('sort_dir')::bool + AND sqlc.arg('sort_col')::text = 'publish_date' + AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL + THEN sqlc.narg('publish_cursor')::timestamp < publish_date + ELSE TRUE + END ORDER BY - CASE WHEN sqlc.arg('sort_col')::text = 'publish_date' AND sqlc.arg('sort_dir')::bool THEN publish_date END DESC, - CASE WHEN sqlc.arg('sort_col')::text = 'publish_date' AND NOT sqlc.arg('sort_dir')::bool THEN publish_date END ASC + CASE + WHEN + sqlc.arg('sort_col')::text = 'publish_date' + AND sqlc.arg('sort_dir')::bool + THEN publish_date + END DESC, + CASE + WHEN + sqlc.arg('sort_col')::text = 'publish_date' + AND NOT sqlc.arg('sort_dir')::bool + THEN publish_date + END ASC LIMIT sqlc.arg('limit'); -- Limit for pagination diff --git a/db/queries/revision/list_steps_by_recipe_revision_id.sql b/db/queries/revision/list_steps_by_recipe_revision_id.sql index 17de4d8..27fe6cf 100644 --- a/db/queries/revision/list_steps_by_recipe_revision_id.sql +++ b/db/queries/revision/list_steps_by_recipe_revision_id.sql @@ -1,15 +1,15 @@ -- name: ListStepsByRecipeRevisionID :many SELECT - recipe_steps.id, - recipe_steps.revision_id, - recipe_steps.content, - recipe_steps.index, - recipe_steps.photo + recipe_steps.id, + recipe_steps.revision_id, + recipe_steps.content, + recipe_steps.index, + recipe_steps.photo FROM - recipe_revisions -JOIN - recipe_steps ON recipe_revisions.id = recipe_steps.revision_id + recipe_revisions +INNER JOIN + recipe_steps ON recipe_revisions.id = recipe_steps.revision_id WHERE - recipe_revisions.id = $1 + recipe_revisions.id = sqlc.arg('revision_id') ORDER BY - recipe_steps.index; + recipe_steps.index; diff --git a/db/queries/revision/seed_revision.sql b/db/queries/revision/seed_revision.sql index b66aeae..2e886d5 100644 --- a/db/queries/revision/seed_revision.sql +++ b/db/queries/revision/seed_revision.sql @@ -1,28 +1,28 @@ -- name: SeedRevision :one INSERT INTO recipe_revisions ( - recipe_id, - parent_id, - recipe_description, - change_comment, - title, - photo, - publish_date + recipe_id, + parent_id, + recipe_description, + change_comment, + title, + photo, + publish_date ) VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7 + sqlc.arg('recipe_id'), + sqlc.arg('parent_id'), + sqlc.arg('recipe_description'), + sqlc.arg('change_comment'), + sqlc.arg('title'), + sqlc.arg('photo'), + sqlc.arg('publish_date') ) RETURNING - id, - recipe_id, - parent_id, - recipe_description, - change_comment, - title, - publish_date, - photo; + id, + recipe_id, + parent_id, + recipe_description, + change_comment, + title, + publish_date, + photo; diff --git a/db/queries/revision/upsert_rating.sql b/db/queries/revision/upsert_rating.sql index 08d1bbd..a960ed6 100644 --- a/db/queries/revision/upsert_rating.sql +++ b/db/queries/revision/upsert_rating.sql @@ -1,17 +1,17 @@ -- name: UpsertRating :one INSERT INTO ratings ( - revision_id, - user_id, - star_value + revision_id, + user_id, + star_value ) VALUES ( - sqlc.arg('revision_id')::uuid, - sqlc.arg('user_id')::uuid, - sqlc.arg('star_value')::smallint + sqlc.arg('revision_id')::uuid, + sqlc.arg('user_id')::uuid, + sqlc.arg('star_value')::smallint ) ON CONFLICT (revision_id, user_id) -DO UPDATE SET star_value = EXCLUDED.star_value +DO UPDATE SET star_value = excluded.star_value RETURNING - revision_id, - user_id, - star_value; + revision_id, + user_id, + star_value; diff --git a/db/queries/user/create_user.sql b/db/queries/user/create_user.sql index f62afa8..ccba813 100644 --- a/db/queries/user/create_user.sql +++ b/db/queries/user/create_user.sql @@ -1,17 +1,17 @@ -- name: CreateUser :one INSERT INTO - users ( +users ( email, display_name - ) +) VALUES ( - $1, - $2 + sqlc.arg('email'), + sqlc.arg('display_name') ) RETURNING - users.id, - users.display_name, - users.email, - users.join_date, - users.updated_at, - users.photo; + users.id, + users.display_name, + users.email, + users.join_date, + users.updated_at, + users.photo; diff --git a/db/queries/user/get_user_by_display_name.sql b/db/queries/user/get_user_by_display_name.sql index 862fa97..e704faa 100644 --- a/db/queries/user/get_user_by_display_name.sql +++ b/db/queries/user/get_user_by_display_name.sql @@ -1,2 +1,11 @@ -- name: GetUserByDisplayName :one -SELECT users.id, users.display_name, users.email, users.join_date, users.updated_at, users.photo FROM users WHERE users.display_name = $1 LIMIT 1; +SELECT + users.id, + users.display_name, + users.email, + users.join_date, + users.updated_at, + users.photo +FROM users +WHERE users.display_name = sqlc.arg('display_name') +LIMIT 1; diff --git a/db/queries/user/get_user_by_email.sql b/db/queries/user/get_user_by_email.sql index 9521277..5224a94 100644 --- a/db/queries/user/get_user_by_email.sql +++ b/db/queries/user/get_user_by_email.sql @@ -1,2 +1,11 @@ -- name: GetUserByEmail :one -SELECT users.id, users.display_name, users.email, users.join_date, users.updated_at, users.photo FROM users WHERE users.email = $1 LIMIT 1; +SELECT + users.id, + users.display_name, + users.email, + users.join_date, + users.updated_at, + users.photo +FROM users +WHERE users.email = sqlc.arg('email') +LIMIT 1; diff --git a/db/queries/user/get_user_by_id.sql b/db/queries/user/get_user_by_id.sql index 91e0708..51a28f7 100644 --- a/db/queries/user/get_user_by_id.sql +++ b/db/queries/user/get_user_by_id.sql @@ -1,2 +1,11 @@ -- name: GetUserById :one -SELECT users.id, users.display_name, users.email, users.join_date, users.updated_at, users.photo FROM users WHERE users.id = $1 LIMIT 1; +SELECT + users.id, + users.display_name, + users.email, + users.join_date, + users.updated_at, + users.photo +FROM users +WHERE users.id = sqlc.arg('id') +LIMIT 1; diff --git a/db/queries/user/seed_user.sql b/db/queries/user/seed_user.sql index 1ff0122..6b63706 100644 --- a/db/queries/user/seed_user.sql +++ b/db/queries/user/seed_user.sql @@ -1,19 +1,19 @@ -- name: SeedUser :one INSERT INTO - users ( +users ( email, display_name, join_date - ) +) VALUES ( - $1, - $2, - $3 + sqlc.arg('email'), + sqlc.arg('display_name'), + sqlc.arg('join_date') ) RETURNING - users.id, - users.display_name, - users.email, - users.join_date, - users.updated_at, - users.photo; + users.id, + users.display_name, + users.email, + users.join_date, + users.updated_at, + users.photo; diff --git a/db/queries/user/update_user.sql b/db/queries/user/update_user.sql index eb6348a..393f812 100644 --- a/db/queries/user/update_user.sql +++ b/db/queries/user/update_user.sql @@ -1,11 +1,14 @@ -- name: UpdateUser :one UPDATE users -SET display_name = $2, email = $3, photo = $4 -WHERE id = $1 +SET + display_name = sqlc.arg('display_name'), + email = sqlc.arg('email'), + photo = sqlc.arg('photo') +WHERE id = sqlc.arg('id') RETURNING - id, - display_name, - email, - join_date, - updated_at, - photo; + id, + display_name, + email, + join_date, + updated_at, + photo; From 74a8d4d804f498b615e49440feef0265f900b595 Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Fri, 18 Jul 2025 22:13:13 -0400 Subject: [PATCH 3/7] refactor: run sqlfluff on migrations --- db/migrations/1726615439_init.down.sql | 1 - db/migrations/1726615439_init.up.sql | 128 ++++---- ...50406021926_unique-slug-by-author.down.sql | 3 +- ...235521_recipe_revision_search_index.up.sql | 9 +- db/queries/recipe/list_recipes_with_query.sql | 288 ++++++++++-------- 5 files changed, 234 insertions(+), 195 deletions(-) diff --git a/db/migrations/1726615439_init.down.sql b/db/migrations/1726615439_init.down.sql index 13550b6..1d463b4 100644 --- a/db/migrations/1726615439_init.down.sql +++ b/db/migrations/1726615439_init.down.sql @@ -15,4 +15,3 @@ DROP TABLE IF EXISTS tags; DROP TABLE IF EXISTS linked_recipes; DROP TABLE IF EXISTS recipes; DROP TABLE IF EXISTS users; - diff --git a/db/migrations/1726615439_init.up.sql b/db/migrations/1726615439_init.up.sql index 8462c75..d5e3cae 100644 --- a/db/migrations/1726615439_init.up.sql +++ b/db/migrations/1726615439_init.up.sql @@ -1,101 +1,103 @@ -- Write your up sql migration here CREATE TABLE IF NOT EXISTS users ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - display_name varchar(50) NOT NULL UNIQUE, - email varchar(255) NOT NULL UNIQUE, - join_date timestamp NOT NULL DEFAULT now(), - updated_at timestamp NULL + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + display_name varchar(50) NOT NULL UNIQUE, + email varchar(255) NOT NULL UNIQUE, + join_date timestamp NOT NULL DEFAULT now(), + updated_at timestamp NULL ); CREATE TABLE IF NOT EXISTS recipes ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - author_id uuid NOT NULL REFERENCES users(id), - slug varchar(75) NOT NULL UNIQUE, - private boolean NOT NULL, - initial_publish_date timestamp NOT NULL DEFAULT now() + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + author_id uuid NOT NULL REFERENCES users (id), + slug varchar(75) NOT NULL UNIQUE, + private boolean NOT NULL, + initial_publish_date timestamp NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS recipe_revisions ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - recipe_id uuid NOT NULL REFERENCES recipes(id), - parent_id uuid REFERENCES recipe_revisions(id), - -- About section for the recipe. - recipe_description text, - --change description should be a note describing how description changed - change_comment text, - title text NOT NULL, - publish_date timestamp NOT NULL DEFAULT now() + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + recipe_id uuid NOT NULL REFERENCES recipes (id), + parent_id uuid REFERENCES recipe_revisions (id), + -- About section for the recipe. + recipe_description text, + --change description should be a note describing how description changed + change_comment text, + title text NOT NULL, + publish_date timestamp NOT NULL DEFAULT now() ); ALTER TABLE IF EXISTS recipes -ADD forked_from uuid REFERENCES recipe_revisions(id), -ADD featured_revision uuid REFERENCES recipe_revisions(id); +ADD forked_from uuid REFERENCES recipe_revisions (id), +ADD featured_revision uuid REFERENCES recipe_revisions (id); CREATE TABLE IF NOT EXISTS tags ( - id bigserial PRIMARY KEY, - name varchar(255) NOT NULL UNIQUE, - description text, - --True if by user , False if internal keyword - user_generated boolean NOT NULL + id bigserial PRIMARY KEY, + name varchar(255) NOT NULL UNIQUE, + description text, + --True if by user , False if internal keyword + user_generated boolean NOT NULL ); CREATE TABLE IF NOT EXISTS measurement_units ( - id bigserial PRIMARY KEY, - name varchar(255) NOT NULL UNIQUE, - description text + id bigserial PRIMARY KEY, + name varchar(255) NOT NULL UNIQUE, + description text ); CREATE TABLE IF NOT EXISTS measurement_units_tags ( - measurement_unit_id bigint NOT NULL REFERENCES measurement_units(id), - tag_id bigint NOT NULL REFERENCES tags(id), - PRIMARY KEY(measurement_unit_id, tag_id) + measurement_unit_id bigint NOT NULL REFERENCES measurement_units (id), + tag_id bigint NOT NULL REFERENCES tags (id), + PRIMARY KEY (measurement_unit_id, tag_id) ); CREATE TABLE IF NOT EXISTS ingredients ( - id bigserial PRIMARY KEY, - name varchar(255) NOT NULL UNIQUE , - description text + id bigserial PRIMARY KEY, + name varchar(255) NOT NULL UNIQUE, + description text ); CREATE TABLE IF NOT EXISTS ingredient_tags ( - ingredient_id bigint NOT NULL REFERENCES ingredients(id), - tag_id bigint NOT NULL REFERENCES tags(id), - PRIMARY KEY(ingredient_id, tag_id) + ingredient_id bigint NOT NULL REFERENCES ingredients (id), + tag_id bigint NOT NULL REFERENCES tags (id), + PRIMARY KEY (ingredient_id, tag_id) ); CREATE TABLE IF NOT EXISTS recipe_ingredients ( - id bigserial PRIMARY KEY, - revision_id uuid NOT NULL REFERENCES recipe_revisions(id), - ingredient_id bigint NOT NULL REFERENCES ingredients(id), - quantity real NOT NULL, - measurement_unit_id bigint NOT NULL REFERENCES measurement_units(id), - comment text + id bigserial PRIMARY KEY, + revision_id uuid NOT NULL REFERENCES recipe_revisions (id), + ingredient_id bigint NOT NULL REFERENCES ingredients (id), + quantity real NOT NULL, + measurement_unit_id bigint NOT NULL REFERENCES measurement_units (id), + comment text ); CREATE TABLE IF NOT EXISTS recipe_steps ( - id bigserial PRIMARY KEY, - revision_id uuid NOT NULL REFERENCES recipe_revisions(id), - content text NOT NULL, - index int NOT NULL + id bigserial PRIMARY KEY, + revision_id uuid NOT NULL REFERENCES recipe_revisions (id), + content text NOT NULL, + index int NOT NULL ); -CREATE TABLE IF NOT EXISTS ratings( - revision_id uuid NOT NULL REFERENCES recipe_revisions(id), - user_id uuid NOT NULL REFERENCES users(id), - --should be 1-5 - star_value smallint CONSTRAINT check_star_Number CHECK(star_value >= 1 AND star_value <= 5), - PRIMARY KEY(revision_id, user_id) +CREATE TABLE IF NOT EXISTS ratings ( + revision_id uuid NOT NULL REFERENCES recipe_revisions (id), + user_id uuid NOT NULL REFERENCES users (id), + --should be 1-5 + star_value smallint CONSTRAINT check_star_number CHECK ( + star_value >= 1 AND star_value <= 5 + ), + PRIMARY KEY (revision_id, user_id) ); -CREATE TABLE IF NOT EXISTS magic_links( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - user_id uuid NOT NULL REFERENCES users(id), - token uuid NOT NULL DEFAULT gen_random_uuid(), - expiry timestamp NOT NULL CHECK (expiry > now()) +CREATE TABLE IF NOT EXISTS magic_links ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users (id), + token uuid NOT NULL DEFAULT gen_random_uuid(), + expiry timestamp NOT NULL CHECK (expiry > now()) ); -CREATE TABLE IF NOT EXISTS sessions( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - user_id uuid NOT NULL REFERENCES users(id), - expiry timestamp NOT NULL CHECK (expiry > now()) +CREATE TABLE IF NOT EXISTS sessions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users (id), + expiry timestamp NOT NULL CHECK (expiry > now()) ); diff --git a/db/migrations/20250406021926_unique-slug-by-author.down.sql b/db/migrations/20250406021926_unique-slug-by-author.down.sql index b0c7797..6fb3e6c 100644 --- a/db/migrations/20250406021926_unique-slug-by-author.down.sql +++ b/db/migrations/20250406021926_unique-slug-by-author.down.sql @@ -1,5 +1,4 @@ ALTER TABLE recipes DROP CONSTRAINT recipes_slug_author_id_key; ALTER TABLE recipes -ADD CONSTRAINT recipes_slug_key UNIQUE (slug); - +ADD CONSTRAINT recipes_slug_key UNIQUE (slug); diff --git a/db/migrations/20250705235521_recipe_revision_search_index.up.sql b/db/migrations/20250705235521_recipe_revision_search_index.up.sql index 817f3c1..490c467 100644 --- a/db/migrations/20250705235521_recipe_revision_search_index.up.sql +++ b/db/migrations/20250705235521_recipe_revision_search_index.up.sql @@ -1,3 +1,10 @@ CREATE INDEX recipe_revisions_search_text ON recipe_revisions -USING GIN ((setweight(to_tsvector('english', title), 'A') || setweight(to_tsvector('english', coalesce(recipe_description, '')), 'B'))) +USING gin ( + ( + setweight(to_tsvector('english', title), 'A') + || setweight( + to_tsvector('english', coalesce(recipe_description, '')), 'B' + ) + ) +) diff --git a/db/queries/recipe/list_recipes_with_query.sql b/db/queries/recipe/list_recipes_with_query.sql index fc17f11..27b7115 100644 --- a/db/queries/recipe/list_recipes_with_query.sql +++ b/db/queries/recipe/list_recipes_with_query.sql @@ -1,130 +1,162 @@ -- name: ListRecipesWithQuery :many ---RevisionWithRowNumbers takes the recipe revision and add row numbers to be used later ---This change is used to help us choose the most recent revision in the absence of a featured revision -WITH RevisionsWithRowNumbers -AS ( - SELECT ROW_NUMBER() OVER ( - PARTITION BY recipe_id ORDER BY publish_date DESC - ) AS row_num - ,recipe_id - ,recipe_description - ,title - ,id - FROM recipe_revisions - ) - --LatestRevision takes the RowNumbers partitioned above. Row_Num 1 is the most recent based on revisions ordered by date. - ,LatestRevision -AS ( - SELECT LDR.recipe_description - ,LDR.title - ,LDR.id AS revisionid - FROM recipes r - JOIN RevisionsWithRowNumbers LDR ON r.id = LDR.recipe_id - WHERE LDR.row_num = 1 - AND r.featured_revision IS NULL - - UNION - - --Used when there is a featured recipe - SELECT recipe_revisions.recipe_description - ,recipe_revisions.title - ,recipe_revisions.id AS revisionid - FROM recipes r - JOIN recipe_revisions ON r.featured_revision = recipe_revisions.id - ) - ,rankings -AS ( - SELECT r.* - ,ts_rank(setweight(to_tsvector('english', title), 'A') || setweight(to_tsvector('english', coalesce(recipe_description, '')), 'B'), websearch_to_tsquery('english', sqlc.arg('query'))) AS rank - FROM recipes r - JOIN LatestRevision ON r.id = LatestRevision.recipeid - ) -SELECT id - ,author_id - ,slug - ,private - ,initial_publish_date - ,forked_from - ,featured_revision - ,rank -FROM rankings -WHERE rank > 0 - AND CASE - WHEN sqlc.narg('author_id')::uuid IS NOT NULL - THEN author_id = sqlc.narg('author_id')::uuid - ELSE true - END - AND CASE - WHEN sqlc.narg('forked_from')::uuid IS NOT NULL - THEN forked_from = sqlc.narg('forked_from')::uuid - ELSE true - END - AND CASE - WHEN sqlc.narg('current_user')::uuid IS NOT NULL - AND sqlc.narg('private')::bool IS NOT NULL - AND sqlc.narg('private')::bool - THEN author_id = sqlc.narg('current_user')::uuid - AND private = true - ELSE private = false - OR private IS NULL - END - AND CASE - WHEN sqlc.narg('publish_start')::TIMESTAMP IS NOT NULL - THEN initial_publish_date >= sqlc.narg('publish_start')::TIMESTAMP - ELSE true - END - AND CASE - WHEN sqlc.narg('publish_end')::TIMESTAMP IS NOT NULL - THEN initial_publish_date <= sqlc.narg('publish_end')::TIMESTAMP - ELSE true - END - AND CASE - WHEN sqlc.arg('sort_col')::TEXT = 'publish_date' - AND sqlc.arg('sort_dir')::bool - AND sqlc.narg('publish_cursor')::TIMESTAMP IS NOT NULL - THEN sqlc.narg('publish_cursor')::TIMESTAMP > initial_publish_date - ELSE true - END - AND CASE - WHEN NOT sqlc.arg('sort_dir')::bool - AND sqlc.arg('sort_col')::TEXT = 'publish_date' - AND sqlc.narg('publish_cursor')::TIMESTAMP IS NOT NULL - THEN sqlc.narg('publish_cursor')::TIMESTAMP < initial_publish_date - ELSE true - END - AND CASE - WHEN sqlc.arg('sort_col')::TEXT = 'slug' - AND sqlc.arg('sort_dir')::bool - AND sqlc.narg('slug_cursor')::TEXT IS NOT NULL - THEN sqlc.narg('slug_cursor')::TEXT > slug - ELSE true - END - AND CASE - WHEN NOT sqlc.arg('sort_dir')::bool - AND sqlc.arg('sort_col')::TEXT = 'slug' - AND sqlc.narg('slug_cursor')::TEXT IS NOT NULL - THEN sqlc.narg('slug_cursor')::TEXT < slug - ELSE true - END -ORDER BY rank DESC - ,CASE - WHEN sqlc.arg('sort_col')::TEXT = 'publish_date' - AND sqlc.arg('sort_dir')::bool - THEN initial_publish_date - END DESC - ,CASE - WHEN sqlc.arg('sort_col')::TEXT = 'publish_date' - AND NOT sqlc.arg('sort_dir')::bool - THEN initial_publish_date - END ASC - ,CASE - WHEN sqlc.arg('sort_col')::TEXT = 'slug' - AND sqlc.arg('sort_dir')::bool - THEN slug - END DESC - ,CASE - WHEN sqlc.arg('sort_col')::TEXT = 'slug' - AND NOT sqlc.arg('sort_dir')::bool - THEN slug - END ASC LIMIT sqlc.arg('limit'); +--RevisionWithRowNumbers takes the recipe revision +--and add row numbers to be used later +--This change is used to help us choose the most recent revision +--in the absence of a featured revision +WITH REVISIONSWITHROWNUMBERS AS ( + SELECT + RECIPE_ID, + RECIPE_DESCRIPTION, + TITLE, + ID, + ROW_NUMBER() OVER ( + PARTITION BY RECIPE_ID ORDER BY PUBLISH_DATE DESC + ) AS ROW_NUM + FROM RECIPE_REVISIONS +), +--LatestRevision takes the RowNumbers partitioned above. +--Row_Num 1 is the most recent based on revisions ordered by date. +LATESTREVISION AS ( + SELECT + LDR.RECIPE_DESCRIPTION, + LDR.TITLE, + LDR.ID AS REVISIONID + FROM RECIPES AS R + INNER JOIN REVISIONSWITHROWNUMBERS AS LDR ON R.ID = LDR.RECIPE_ID + WHERE + LDR.ROW_NUM = 1 + AND R.FEATURED_REVISION IS NULL + UNION + --Used when there is a featured recipe + SELECT + RECIPE_REVISIONS.RECIPE_DESCRIPTION, + RECIPE_REVISIONS.TITLE, + RECIPE_REVISIONS.ID AS REVISIONID + FROM RECIPES AS R + INNER JOIN RECIPE_REVISIONS ON R.FEATURED_REVISION = RECIPE_REVISIONS.ID +), + +RANKINGS AS ( + SELECT + R.*, + TS_RANK( + SETWEIGHT(TO_TSVECTOR('english', LATESTREVISION.TITLE), 'A') + || SETWEIGHT( + TO_TSVECTOR( + 'english', + COALESCE(LATESTREVISION.RECIPE_DESCRIPTION, '') + ), + 'B' + ), + WEBSEARCH_TO_TSQUERY('english', sqlc.arg('query')) + ) AS RANK + FROM RECIPES AS R + INNER JOIN LATESTREVISION ON R.ID = LATESTREVISION.RECIPEID +) + +SELECT + ID, + AUTHOR_ID, + SLUG, + PRIVATE, + INITIAL_PUBLISH_DATE, + FORKED_FROM, + FEATURED_REVISION, + RANK +FROM RANKINGS +WHERE + RANK > 0 + AND CASE + WHEN sqlc.narg('author_id')::uuid IS NOT NULL + THEN AUTHOR_ID = sqlc.narg('author_id')::uuid + ELSE TRUE + END + AND CASE + WHEN sqlc.narg('forked_from')::uuid IS NOT NULL + THEN FORKED_FROM = sqlc.narg('forked_from')::uuid + ELSE TRUE + END + AND CASE + WHEN + sqlc.narg('current_user')::uuid IS NOT NULL + AND sqlc.narg('private')::bool IS NOT NULL + AND sqlc.narg('private')::bool + THEN + AUTHOR_ID = sqlc.narg('current_user')::uuid + AND PRIVATE = TRUE + ELSE + PRIVATE = FALSE + OR PRIVATE IS NULL + END + AND CASE + WHEN sqlc.narg('publish_start')::timestamp IS NOT NULL + THEN INITIAL_PUBLISH_DATE >= sqlc.narg('publish_start')::timestamp + ELSE TRUE + END + AND CASE + WHEN sqlc.narg('publish_end')::timestamp IS NOT NULL + THEN INITIAL_PUBLISH_DATE <= sqlc.narg('publish_end')::timestamp + ELSE TRUE + END + AND CASE + WHEN + sqlc.arg('sort_col')::text = 'publish_date' + AND sqlc.arg('sort_dir')::bool + AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL + THEN + sqlc.narg('publish_cursor')::timestamp > INITIAL_PUBLISH_DATE + ELSE TRUE + END + AND CASE + WHEN + NOT sqlc.arg('sort_dir')::bool + AND sqlc.arg('sort_col')::text = 'publish_date' + AND sqlc.narg('publish_cursor')::timestamp IS NOT NULL + THEN + sqlc.narg('publish_cursor')::timestamp < INITIAL_PUBLISH_DATE + ELSE TRUE + END + AND CASE + WHEN + sqlc.arg('sort_col')::text = 'slug' + AND sqlc.arg('sort_dir')::bool + AND sqlc.narg('slug_cursor')::text IS NOT NULL + THEN sqlc.narg('slug_cursor')::text > SLUG + ELSE TRUE + END + AND CASE + WHEN + NOT sqlc.arg('sort_dir')::bool + AND sqlc.arg('sort_col')::text = 'slug' + AND sqlc.narg('slug_cursor')::text IS NOT NULL + THEN sqlc.narg('slug_cursor')::text < SLUG + ELSE TRUE + END +ORDER BY + RANK DESC, + CASE + WHEN + sqlc.arg('sort_col')::text = 'publish_date' + AND sqlc.arg('sort_dir')::bool + THEN INITIAL_PUBLISH_DATE + END DESC, + CASE + WHEN + sqlc.arg('sort_col')::text = 'publish_date' + AND NOT sqlc.arg('sort_dir')::bool + THEN INITIAL_PUBLISH_DATE + END ASC, + CASE + WHEN + sqlc.arg('sort_col')::text = 'slug' + AND sqlc.arg('sort_dir')::bool + THEN SLUG + END DESC, + CASE + WHEN + sqlc.arg('sort_col')::text = 'slug' + AND NOT sqlc.arg('sort_dir')::bool + THEN SLUG + END ASC +LIMIT sqlc.arg('limit'); From 5d887361e630fbc189a26b07511fe52f1dded6d3 Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Fri, 18 Jul 2025 22:22:08 -0400 Subject: [PATCH 4/7] build: add compose entry and tasks for sqlfluff --- Taskfile.yml | 15 +++++++++++++++ docker-compose.yaml | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 376fadb..0d0f694 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -56,12 +56,14 @@ tasks: deps: - lint:ts - lint:go + - lint:sql lint:fix: desc: "Run linting for the entire repository (ESLint for non-Go code and golangci-lint for Go) with auto-fixes" deps: - lint:ts:fix - lint:go:fix + - lint:sql:fix lint:ts: desc: "Run ESLint" @@ -79,6 +81,14 @@ tasks: desc: "Run Go linting with golangci-lint" cmd: docker-compose --profile "scripts" run --rm golangci-lint golangci-lint run -v --fix {{.CLI_ARGS}} + lint:sql: + desc: "Run sqlfluff against our queries and migrations" + cmd: docker-compose --profile "scripts" run --build --rm sqlfluff lint --dialect postgres + + lint:sql:fix: + desc: "Run sqlfluff against our queries and migrations" + cmd: docker-compose --profile "scripts" run --build --rm sqlfluff fix --dialect postgres + format:check: desc: "Check formatting for whole project" deps: @@ -90,6 +100,11 @@ tasks: deps: - gofmt - prettier + - sql:format + + sql:format: + desc: "Run sqlfluff against our queries and migrations" + cmd: docker-compose --profile "scripts" run --build --rm sqlfluff lint --dialect postgres gofmt: desc: "Apply Go formatting" diff --git a/docker-compose.yaml b/docker-compose.yaml index 734ef93..1bd5d4f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -239,6 +239,14 @@ services: - ./:/app profiles: - "scripts" + sqlfluff: + image: sqlfluff/sqlfluff:3.4.2 + restart: no + volumes: + - ./db:/db + working_dir: /db + profiles: + - "scripts" seed: # This specifies the docker file/context to build for the api container build: From d0cc2e21c82b12846b10a9c4f08b38a090f6c884 Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Fri, 18 Jul 2025 22:29:50 -0400 Subject: [PATCH 5/7] build: add gh action for sqlfluff --- .github/workflows/sql-check.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/sql-check.yml diff --git a/.github/workflows/sql-check.yml b/.github/workflows/sql-check.yml new file mode 100644 index 0000000..909b5a3 --- /dev/null +++ b/.github/workflows/sql-check.yml @@ -0,0 +1,25 @@ +name: SQL Code Checks + +on: + push: + branches: + - main + - master + pull_request: + +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + # pull-requests: read + +jobs: + sql-check: + name: SQL Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - name: Install SQLFluff + run: pip install sqlfluff + - name: Run SQLFluff + run: sqlfluff lint db/**/*.sql --dialect postgres From d7c7c82564099b52414a998e78afdea8f872f5fa Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Thu, 24 Jul 2025 21:34:22 -0400 Subject: [PATCH 6/7] fix: change key to use recipe id --- web/app/components/recipeList/RecipeList.tsx | 45 ++++++++++++++++++++ web/app/routes/_app._index/route.tsx | 19 +-------- 2 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 web/app/components/recipeList/RecipeList.tsx diff --git a/web/app/components/recipeList/RecipeList.tsx b/web/app/components/recipeList/RecipeList.tsx new file mode 100644 index 0000000..eedec30 --- /dev/null +++ b/web/app/components/recipeList/RecipeList.tsx @@ -0,0 +1,45 @@ +import { SimpleGrid } from "@mantine/core" +import { ReactNode } from "react" +import { ListRecipesQuery } from "~/gql/forkd.g" +import { RecipeCard } from "../recipeCard/recipeCard" + +type Props = { + recipes: Exclude["list"] +} + +export default function RecipeList({ recipes }: Props): ReactNode { + return ( + <> + {/* recipe component */} + + {recipes?.items.map((recipe) => ( +
+ +
+ ))} +
+ + ) +} + +const styles = { + grid: { + background: "#fff", + width: "90%", + margin: "auto", + }, + col: { + padding: 10, + margin: 10, + borderWidth: 0, + borderColor: "black", + borderStyle: "solid", + justifyContent: "space-evenly", + boxShadow: "0px 5px 15px #bfbfbf", + }, +} diff --git a/web/app/routes/_app._index/route.tsx b/web/app/routes/_app._index/route.tsx index 512bce5..b0781e8 100644 --- a/web/app/routes/_app._index/route.tsx +++ b/web/app/routes/_app._index/route.tsx @@ -46,7 +46,7 @@ export default function Index() { style={styles.grid} > {recipes?.items.map((recipe) => ( -
+
))} @@ -54,20 +54,3 @@ export default function Index() { ) } - -const styles = { - grid: { - background: "#fff", - width: "90%", - margin: "auto", - }, - col: { - padding: 10, - margin: 10, - borderWidth: 0, - borderColor: "black", - borderStyle: "solid", - justifyContent: "space-evenly", - boxShadow: "0px 5px 15px #bfbfbf", - }, -} From 655e4198481d488752eb2179990844fa02c17932 Mon Sep 17 00:00:00 2001 From: Graham Vasquez Date: Sun, 3 Aug 2025 01:10:31 -0400 Subject: [PATCH 7/7] refactor: use RecipeList component in index route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode --- web/app/routes/_app._index/route.tsx | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/web/app/routes/_app._index/route.tsx b/web/app/routes/_app._index/route.tsx index b0781e8..9f7c8f9 100644 --- a/web/app/routes/_app._index/route.tsx +++ b/web/app/routes/_app._index/route.tsx @@ -1,11 +1,10 @@ -import { SimpleGrid } from "@mantine/core" -import { RecipeCard } from "../../components/recipeCard/recipeCard" import { MetaFunction, useLoaderData } from "@remix-run/react" import { LoaderFunctionArgs } from "@remix-run/node" import { ClientError } from "graphql-request" import { getSessionOrThrow } from "~/.server/session" import { getSDK } from "~/gql/client" import { environment } from "~/.server/env" +import RecipeList from "../../components/recipeList/RecipeList" export const meta: MetaFunction = () => { return [ @@ -36,21 +35,7 @@ export async function loader(args: LoaderFunctionArgs) { export default function Index() { const recipes = useLoaderData() - return ( - <> - {/* recipe component */} - - {recipes?.items.map((recipe) => ( -
- -
- ))} -
- - ) + if (!recipes) return null + + return }