Hide the recipe link, pick the creating suggestion, refuse duplicate positions
Dinner detail review fixes for GET /api/v1/dinners/{id}:
- source is now {site_name} with no url. A stored source_url may be a
pasted or offered link carrying a personal token, and nothing can show
a stored link is free of one; the candidate flow keeps every page URL
on the server for the same reason, so the detail does too.
- why_picked comes from GetDinnerSuggestionReasons, which reads only
pending or accepted suggestions: a suggestion is only ever created with
a new dinner and declining one archives that dinner, so a declined row
never created a live dinner. More than one candidate row is an error
rather than a guess. sqlc regenerated.
- Two recipe ingredients or steps at one position leave recipe order
unknown, so the read fails rather than ordering them by id.
Maestro task 099e9952-7f98-4700-8369-241c589a1e9d.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -213,10 +213,15 @@ automatic** (D-021).
|
|||||||
raw_text}]` and `steps` the step texts, both in recipe position order and
|
raw_text}]` and `steps` the step texts, both in recipe position order and
|
||||||
empty (`[]`) with no recipe, as for a dinner whose grounding is
|
empty (`[]`) with no recipe, as for a dinner whose grounding is
|
||||||
`pending` or `failed`; a recipe entered by hand has only `raw_text`.
|
`pending` or `failed`; a recipe entered by hand has only `raw_text`.
|
||||||
`source` is `{url, site_name}` of the page the recipe was fetched from,
|
Two items at one position leave recipe order unknown, so such a recipe
|
||||||
null for a manual recipe or none; `why_picked` is the reason of the
|
is 500 rather than shown out of order. `source` is `{site_name}` of the
|
||||||
suggestion that created the dinner (its earliest), null for one no
|
page the recipe was fetched from (`site_name` null when the page names
|
||||||
suggestion created. An unknown or archived dinner is 404.
|
none), null for a manual recipe or none; it has no `url`, as the page's
|
||||||
|
link may carry a personal token. `why_picked` is the reason of the
|
||||||
|
suggestion that created the dinner — its pending or accepted one, as
|
||||||
|
declining a suggestion archives its dinner — null for one no suggestion
|
||||||
|
created; a dinner with more than one such suggestion is 500, as nothing
|
||||||
|
records which created it. An unknown or archived dinner is 404.
|
||||||
- Dinner grounding. `POST /api/v1/dinners` also enqueues a `ground_dinner`
|
- Dinner grounding. `POST /api/v1/dinners` also enqueues a `ground_dinner`
|
||||||
job and answers with its `grounding_job_id` without waiting for it
|
job and answers with its `grounding_job_id` without waiting for it
|
||||||
(`GET /api/v1/jobs/{id}` reports it); if the job cannot be enqueued it
|
(`GET /api/v1/jobs/{id}` reports it); if the job cannot be enqueued it
|
||||||
|
|||||||
@@ -103,9 +103,13 @@ type Querier interface {
|
|||||||
// Returns sql.ErrNoRows until the row exists.
|
// Returns sql.ErrNoRows until the row exists.
|
||||||
GetChatSettings(ctx context.Context) (ChatSetting, error)
|
GetChatSettings(ctx context.Context) (ChatSetting, error)
|
||||||
GetDinner(ctx context.Context, id string) (Dinner, error)
|
GetDinner(ctx context.Context, id string) (Dinner, error)
|
||||||
// The reason of the suggestion that created dinner_id, its earliest.
|
// The reason of each suggestion of dinner_id that can have created it,
|
||||||
// Returns sql.ErrNoRows for a dinner no suggestion created.
|
// none for a dinner no suggestion created. A suggestion is only ever
|
||||||
GetDinnerSuggestionReason(ctx context.Context, dinnerID string) (string, error)
|
// created with a new dinner, and declining one archives its dinner, so a
|
||||||
|
// live dinner's creating suggestion is pending (the dinner still
|
||||||
|
// suggested) or accepted; a declined one did not create it. More than one
|
||||||
|
// row means nothing records which did.
|
||||||
|
GetDinnerSuggestionReasons(ctx context.Context, dinnerID string) ([]string, error)
|
||||||
// Returns sql.ErrNoRows until the row exists.
|
// Returns sql.ErrNoRows until the row exists.
|
||||||
GetHouseholdSettings(ctx context.Context) (HouseholdSetting, error)
|
GetHouseholdSettings(ctx context.Context) (HouseholdSetting, error)
|
||||||
// Returns sql.ErrNoRows until the row exists.
|
// Returns sql.ErrNoRows until the row exists.
|
||||||
|
|||||||
@@ -187,20 +187,39 @@ func (q *Queries) GetActiveSuggestionBatch(ctx context.Context) (SuggestionBatch
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDinnerSuggestionReason = `-- name: GetDinnerSuggestionReason :one
|
const getDinnerSuggestionReasons = `-- name: GetDinnerSuggestionReasons :many
|
||||||
SELECT reason FROM suggestions
|
SELECT reason FROM suggestions
|
||||||
WHERE dinner_id = ?
|
WHERE dinner_id = ? AND status IN ('pending', 'accepted')
|
||||||
ORDER BY created_at, id
|
ORDER BY id
|
||||||
LIMIT 1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// The reason of the suggestion that created dinner_id, its earliest.
|
// The reason of each suggestion of dinner_id that can have created it,
|
||||||
// Returns sql.ErrNoRows for a dinner no suggestion created.
|
// none for a dinner no suggestion created. A suggestion is only ever
|
||||||
func (q *Queries) GetDinnerSuggestionReason(ctx context.Context, dinnerID string) (string, error) {
|
// created with a new dinner, and declining one archives its dinner, so a
|
||||||
row := q.db.QueryRowContext(ctx, getDinnerSuggestionReason, dinnerID)
|
// live dinner's creating suggestion is pending (the dinner still
|
||||||
var reason string
|
// suggested) or accepted; a declined one did not create it. More than one
|
||||||
err := row.Scan(&reason)
|
// row means nothing records which did.
|
||||||
return reason, err
|
func (q *Queries) GetDinnerSuggestionReasons(ctx context.Context, dinnerID string) ([]string, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getDinnerSuggestionReasons, dinnerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []string
|
||||||
|
for rows.Next() {
|
||||||
|
var reason string
|
||||||
|
if err := rows.Scan(&reason); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, reason)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLatestSuggestionBatch = `-- name: GetLatestSuggestionBatch :one
|
const getLatestSuggestionBatch = `-- name: GetLatestSuggestionBatch :one
|
||||||
|
|||||||
+38
-14
@@ -30,15 +30,16 @@ type Detail struct {
|
|||||||
// the dinner has no recipe.
|
// the dinner has no recipe.
|
||||||
Ingredients []Ingredient `json:"ingredients"`
|
Ingredients []Ingredient `json:"ingredients"`
|
||||||
Steps []string `json:"steps"`
|
Steps []string `json:"steps"`
|
||||||
// Source is the recipe page the dinner is grounded in; null for a
|
// Source is the site of the recipe page the dinner is grounded in;
|
||||||
// recipe entered by hand or a dinner with no recipe.
|
// null for a recipe entered by hand or a dinner with no recipe.
|
||||||
Source *Source `json:"source"`
|
Source *Source `json:"source"`
|
||||||
GroundingStatus string `json:"grounding_status"`
|
GroundingStatus string `json:"grounding_status"`
|
||||||
// GroundingError is why grounding failed, one of the Reason codes;
|
// GroundingError is why grounding failed, one of the Reason codes;
|
||||||
// null unless GroundingStatus is failed.
|
// null unless GroundingStatus is failed.
|
||||||
GroundingError *string `json:"grounding_error"`
|
GroundingError *string `json:"grounding_error"`
|
||||||
// WhyPicked is the reason of the suggestion that created the dinner;
|
// WhyPicked is the reason of the suggestion that created the dinner
|
||||||
// null for a dinner no suggestion created.
|
// (see GetDinnerSuggestionReasons); null for a dinner no suggestion
|
||||||
|
// created.
|
||||||
WhyPicked *string `json:"why_picked"`
|
WhyPicked *string `json:"why_picked"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +55,12 @@ type Ingredient struct {
|
|||||||
RawText string `json:"raw_text"`
|
RawText string `json:"raw_text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source is the page a recipe was fetched from.
|
// Source is the site a recipe was fetched from. It has no url: the page's
|
||||||
|
// link, pasted or offered as a candidate, may carry a personal token, and
|
||||||
|
// no stored link can be shown to be free of one, so like a dinner's
|
||||||
|
// recipe_url and a candidate's page it never leaves the server. SiteName
|
||||||
|
// is null when the page names no site.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
URL string `json:"url"`
|
|
||||||
SiteName *string `json:"site_name"`
|
SiteName *string `json:"site_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,13 +117,19 @@ func (s *Service) Get(ctx context.Context, id string) (d Detail, err error) {
|
|||||||
return Detail{}, fmt.Errorf("dinners: read recipe of %s: %w", id, err)
|
return Detail{}, fmt.Errorf("dinners: read recipe of %s: %w", id, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reason, err := q.GetDinnerSuggestionReason(ctx, id)
|
reasons, err := q.GetDinnerSuggestionReasons(ctx, id)
|
||||||
switch {
|
if err != nil {
|
||||||
case err == nil:
|
|
||||||
d.WhyPicked = &reason
|
|
||||||
case !errors.Is(err, sql.ErrNoRows):
|
|
||||||
return Detail{}, fmt.Errorf("dinners: read suggestion reason of %s: %w", id, err)
|
return Detail{}, fmt.Errorf("dinners: read suggestion reason of %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
switch len(reasons) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
d.WhyPicked = &reasons[0]
|
||||||
|
default:
|
||||||
|
// Nothing records which of them created the dinner, and any
|
||||||
|
// one picked would be a guess.
|
||||||
|
return Detail{}, fmt.Errorf("dinners: %s has %d suggestions that could have created it", id, len(reasons))
|
||||||
|
}
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,13 +140,16 @@ func readRecipe(ctx context.Context, q *db.Queries, recipeID string, d *Detail)
|
|||||||
return fmt.Errorf("recipe %s: %w", recipeID, err)
|
return fmt.Errorf("recipe %s: %w", recipeID, err)
|
||||||
}
|
}
|
||||||
if !IsManualRecipe(rec.SourceUrl) {
|
if !IsManualRecipe(rec.SourceUrl) {
|
||||||
d.Source = &Source{URL: rec.SourceUrl, SiteName: nullString(rec.SiteName)}
|
d.Source = &Source{SiteName: nullString(rec.SiteName)}
|
||||||
}
|
}
|
||||||
ings, err := q.ListRecipeIngredients(ctx, recipeID)
|
ings, err := q.ListRecipeIngredients(ctx, recipeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ingredients: %w", err)
|
return fmt.Errorf("ingredients: %w", err)
|
||||||
}
|
}
|
||||||
for _, ing := range ings {
|
for i, ing := range ings {
|
||||||
|
if i > 0 && ing.Position == ings[i-1].Position {
|
||||||
|
return duplicatePosition("ingredient", ing.Position)
|
||||||
|
}
|
||||||
d.Ingredients = append(d.Ingredients, Ingredient{
|
d.Ingredients = append(d.Ingredients, Ingredient{
|
||||||
Name: nullString(ing.Name),
|
Name: nullString(ing.Name),
|
||||||
Qty: nullString(ing.Qty),
|
Qty: nullString(ing.Qty),
|
||||||
@@ -149,8 +162,19 @@ func readRecipe(ctx context.Context, q *db.Queries, recipeID string, d *Detail)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("steps: %w", err)
|
return fmt.Errorf("steps: %w", err)
|
||||||
}
|
}
|
||||||
for _, step := range steps {
|
for i, step := range steps {
|
||||||
|
if i > 0 && step.Position == steps[i-1].Position {
|
||||||
|
return duplicatePosition("step", step.Position)
|
||||||
|
}
|
||||||
d.Steps = append(d.Steps, step.Text)
|
d.Steps = append(d.Steps, step.Text)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// duplicatePosition is the error for two recipe items of kind at position.
|
||||||
|
// Position is the only record of recipe order, and the id that breaks the
|
||||||
|
// tie says nothing about it, so a recipe with a duplicate is refused rather
|
||||||
|
// than shown in an order nobody can trust.
|
||||||
|
func duplicatePosition(kind string, position int64) error {
|
||||||
|
return fmt.Errorf("two %ss at position %d: recipe order is unknown", kind, position)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
|
||||||
@@ -42,7 +44,7 @@ func wantNull(t *testing.T, raw map[string]any, keys ...string) {
|
|||||||
|
|
||||||
// TestGetDinnerGroundedSideQuest: a Side quest grounded in a fetched page
|
// TestGetDinnerGroundedSideQuest: a Side quest grounded in a fetched page
|
||||||
// shows its photo at both stored sizes, its recipe's ingredients and steps
|
// shows its photo at both stored sizes, its recipe's ingredients and steps
|
||||||
// in recipe order (not row order), its source page and the reason it was
|
// in recipe order (not row order), its source site and the reason it was
|
||||||
// suggested.
|
// suggested.
|
||||||
func TestGetDinnerGroundedSideQuest(t *testing.T) {
|
func TestGetDinnerGroundedSideQuest(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -109,7 +111,7 @@ func TestGetDinnerGroundedSideQuest(t *testing.T) {
|
|||||||
{Name: str("onion"), Qty: str("1"), RawText: "1 onion"},
|
{Name: str("onion"), Qty: str("1"), RawText: "1 onion"},
|
||||||
},
|
},
|
||||||
Steps: []string{"Fry with the garlic.", "Chop the onion."},
|
Steps: []string{"Fry with the garlic.", "Chop the onion."},
|
||||||
Source: &dinners.Source{URL: curryPage, SiteName: str("Curry House")},
|
Source: &dinners.Source{SiteName: str("Curry House")},
|
||||||
GroundingStatus: "grounded",
|
GroundingStatus: "grounded",
|
||||||
WhyPicked: str(reason),
|
WhyPicked: str(reason),
|
||||||
}
|
}
|
||||||
@@ -213,3 +215,116 @@ func TestGetDinnerNotFound(t *testing.T) {
|
|||||||
t.Errorf("GET without a session = %d %v, want 401", rec.Code, body)
|
t.Errorf("GET without a session = %d %v, want 401", rec.Code, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetDinnerSourceHidesTokenURL: a recipe grounded in a pasted link that
|
||||||
|
// carries a token in its path or query shows its site name, but never the
|
||||||
|
// link or any part of it.
|
||||||
|
func TestGetDinnerSourceHidesTokenURL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
e := newDinnersEnv(t)
|
||||||
|
const (
|
||||||
|
pathToken = "path-token-abc123"
|
||||||
|
queryToken = "query-token-def456"
|
||||||
|
pathPage = "https://secret-path.example/u/" + pathToken + "/chicken-curry"
|
||||||
|
queryPage = "https://secret-query.example/curry?access_token=" + queryToken
|
||||||
|
)
|
||||||
|
e.pages.Bodies[pathPage] = recipestest.RecipePage("Chicken Curry", "Curry House", "https://a.example/curry.jpg", 40)
|
||||||
|
e.pages.Bodies[queryPage] = recipestest.RecipePage("Weeknight Curry", "B Kitchen", "https://b.example/curry.jpg", 25)
|
||||||
|
e.images.JPEGs["https://a.example/curry.jpg"] = recipestest.JPEG(t, 400, 320, 1)
|
||||||
|
e.images.JPEGs["https://b.example/curry.jpg"] = recipestest.JPEG(t, 400, 320, 2)
|
||||||
|
for _, tc := range []struct{ dish, page, site string }{
|
||||||
|
{"Chicken curry", pathPage, "Curry House"},
|
||||||
|
{"Weeknight curry", queryPage, "B Kitchen"},
|
||||||
|
} {
|
||||||
|
id := createDinner(t, e.h, `{"name":"`+tc.dish+`"}`) // runner not started: stays pending
|
||||||
|
e.model.QueueGenerate(recipestest.NormalizedReply("stovetop", 25))
|
||||||
|
if rec, _ := callDinners(t, e.h, http.MethodPost, dinnersPath+"/"+id+"/ground", `{"recipe_url":"`+tc.page+`"}`); rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("POST ground %s = %d: %s", tc.page, rec.Code, rec.Body)
|
||||||
|
}
|
||||||
|
var stored string
|
||||||
|
if err := e.st.Reader.QueryRow(`SELECT source_url FROM recipes WHERE id = (SELECT recipe_id FROM dinners WHERE id = ?)`, id).Scan(&stored); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stored != tc.page {
|
||||||
|
t.Fatalf("stored source_url = %q, want %q", stored, tc.page)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec, _ := callDinners(t, e.h, http.MethodGet, dinnersPath+"/"+id, "")
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET dinner = %d: %s", rec.Code, rec.Body)
|
||||||
|
}
|
||||||
|
for _, secret := range []string{tc.page, "secret-path.example", "secret-query.example", pathToken, queryToken, "access_token", `"url"`} {
|
||||||
|
if strings.Contains(rec.Body.String(), secret) {
|
||||||
|
t.Errorf("detail of %s contains %q: %s", tc.dish, secret, rec.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw, _ := getDetail(t, e.h, id)
|
||||||
|
if src, _ := raw["source"].(map[string]any); !reflect.DeepEqual(src, map[string]any{"site_name": tc.site}) {
|
||||||
|
t.Errorf("source of %s = %v, want only site_name %q", tc.dish, raw["source"], tc.site)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetDinnerWhyPickedIsCreatingSuggestion: why_picked is the reason of
|
||||||
|
// the suggestion that created the dinner, never that of an earlier
|
||||||
|
// suggestion row for it that was declined.
|
||||||
|
func TestGetDinnerWhyPickedIsCreatingSuggestion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
e := newDinnersEnv(t)
|
||||||
|
id := createDinner(t, e.h, `{"name":"Chicken curry"}`)
|
||||||
|
if _, err := e.st.Writer.Exec(`UPDATE dinners SET status = 'quest_log', origin = 'side_quest' WHERE id = ?`, id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
q := db.New(e.st.Writer)
|
||||||
|
now := store.Now().String()
|
||||||
|
batch, err := q.CreateSuggestionBatch(t.Context(), db.CreateSuggestionBatchParams{ID: store.NewID(), Trigger: "manual", CreatedAt: now, UpdatedAt: now})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const reason = "Like your Tikka masala, but quicker."
|
||||||
|
// The declined row is created first, so it is the earliest by both
|
||||||
|
// created_at and id.
|
||||||
|
for _, s := range []struct{ reason, status string }{
|
||||||
|
{"Passed on last week.", "declined"},
|
||||||
|
{reason, "accepted"},
|
||||||
|
} {
|
||||||
|
sid := store.NewID()
|
||||||
|
if err := q.CreateSuggestion(t.Context(), db.CreateSuggestionParams{
|
||||||
|
ID: sid, DinnerID: id, BatchID: batch.ID, Name: "Chicken curry",
|
||||||
|
Reason: s.reason, CitedRegularIds: "[]", CreatedAt: now, UpdatedAt: now,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n, err := q.DecideSuggestion(t.Context(), db.DecideSuggestionParams{
|
||||||
|
Status: s.status, Now: sql.NullString{String: now, Valid: true}, ID: sid,
|
||||||
|
}); err != nil || n != 1 {
|
||||||
|
t.Fatalf("decide %s = %d, %v", s.status, n, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, got := getDetail(t, e.h, id); got.WhyPicked == nil || *got.WhyPicked != reason {
|
||||||
|
t.Errorf("why_picked = %v, want %q", got.WhyPicked, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetDinnerDuplicatePositionFails: a recipe with two ingredients or two
|
||||||
|
// steps at one position has no order anyone can trust, so its detail is a
|
||||||
|
// 500 rather than the recipe in some order.
|
||||||
|
func TestGetDinnerDuplicatePositionFails(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
e := newDinnersEnv(t)
|
||||||
|
for _, table := range []string{"recipe_ingredients", "recipe_steps"} {
|
||||||
|
id := createDinner(t, e.h, `{"name":"Family chili `+table+`"}`)
|
||||||
|
if rec, _ := callDinners(t, e.h, http.MethodPost, dinnersPath+"/"+id+"/ground",
|
||||||
|
`{"manual":{"ingredients":["1 lb beef","1 can beans"],"steps":["Brown.","Simmer."],"minutes":240,"method":"crockpot"}}`); rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("POST ground = %d: %s", rec.Code, rec.Body)
|
||||||
|
}
|
||||||
|
if _, err := e.st.Writer.Exec(`UPDATE `+table+` SET position = 0 WHERE recipe_id = (SELECT recipe_id FROM dinners WHERE id = ?)`, id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rec, _ := callDinners(t, e.h, http.MethodGet, dinnersPath+"/"+id, "")
|
||||||
|
if rec.Code != http.StatusInternalServerError || strings.Contains(rec.Body.String(), "Simmer.") || strings.Contains(rec.Body.String(), "1 lb beef") {
|
||||||
|
t.Errorf("GET with duplicate %s positions = %d: %s, want 500 and no recipe", table, rec.Code, rec.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -117,10 +117,13 @@ UPDATE dinners
|
|||||||
SET status = sqlc.arg(status), updated_by = sqlc.arg(updated_by), updated_at = sqlc.arg(updated_at)
|
SET status = sqlc.arg(status), updated_by = sqlc.arg(updated_by), updated_at = sqlc.arg(updated_at)
|
||||||
WHERE id = sqlc.arg(id) AND status = 'suggested';
|
WHERE id = sqlc.arg(id) AND status = 'suggested';
|
||||||
|
|
||||||
-- name: GetDinnerSuggestionReason :one
|
-- name: GetDinnerSuggestionReasons :many
|
||||||
-- The reason of the suggestion that created dinner_id, its earliest.
|
-- The reason of each suggestion of dinner_id that can have created it,
|
||||||
-- Returns sql.ErrNoRows for a dinner no suggestion created.
|
-- none for a dinner no suggestion created. A suggestion is only ever
|
||||||
|
-- created with a new dinner, and declining one archives its dinner, so a
|
||||||
|
-- live dinner's creating suggestion is pending (the dinner still
|
||||||
|
-- suggested) or accepted; a declined one did not create it. More than one
|
||||||
|
-- row means nothing records which did.
|
||||||
SELECT reason FROM suggestions
|
SELECT reason FROM suggestions
|
||||||
WHERE dinner_id = ?
|
WHERE dinner_id = ? AND status IN ('pending', 'accepted')
|
||||||
ORDER BY created_at, id
|
ORDER BY id;
|
||||||
LIMIT 1;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user