Compare commits

...

1 Commits

Author SHA1 Message Date
Hayden Hargreaves
30a5396d11 (FIX): Finished translating the entire recipe repo to new infra. 2026-02-09 22:37:33 -07:00
2 changed files with 152 additions and 116 deletions

View File

@ -20,13 +20,8 @@
go go
gopls gopls
go-tools go-tools
htmx-lsp2
templ
tailwindcss_4 tailwindcss_4
tailwindcss-language-server tailwindcss-language-server
watchman
docker-language-server
dockerfile-language-server-nodejs
gcc_multi gcc_multi
glibc_multi glibc_multi
nodejs nodejs

View File

@ -210,19 +210,41 @@ func (r *RecipeRepository) DeleteRecipe(recipeId, userId int) error {
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored // This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
// and the standard "not-found" error will be returned. // and the standard "not-found" error will be returned.
func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) { func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
query := `SELECT psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
id, title, description, instructions, serves, difficulty, duration, category, ingredients,
userid, modified, created, deleted query := psql.
FROM recipes Select(
WHERE id = $1 AND deleted = false; "id",
` "title",
"description",
"instructions",
"serves",
"difficulty",
"duration",
"category",
"ingredients",
"userid",
"modified",
"created",
"deleted",
).
From("recipes").
Where(sq.Eq{
"id": id,
"deleted": false,
})
_sql, args, err := query.ToSql()
if err != nil {
return nil, fmt.Errorf("Failed to construct sql query: %w", err)
}
var durationBytes []byte var durationBytes []byte
var instructions pq.StringArray var instructions pq.StringArray
var ingredientBytes []byte var ingredientBytes []byte
var recipe domain.Recipe var recipe domain.Recipe
if err := r.db.QueryRow(query, id).Scan( if err := r.db.QueryRowx(_sql, args...).Scan(
&recipe.Id, &recipe.Id,
&recipe.Title, &recipe.Title,
&recipe.Description, &recipe.Description,
@ -504,51 +526,64 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *i
// this function successfully, therefore, it must be an existing recipe. Any errors will be bubbled // this function successfully, therefore, it must be an existing recipe. Any errors will be bubbled
// to the caller. // to the caller.
func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string) error { func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string) error {
tx, err := r.db.Begin() tx, err := r.db.Beginx()
if err != nil { if err != nil {
return err return err
} }
defer tx.Rollback()
// Normalize the tag names (lower case with trimmed space) psql := sq.StatementBuilder.
normalized := make(map[string]struct{}) // Use map to disallow duplicates PlaceholderFormat(sq.Dollar).
RunWith(tx)
// Normalize tags (lowercase, trimmed, no duplicates)
normalized := make(map[string]struct{})
for _, tag := range tags { for _, tag := range tags {
normalized[strings.ToLower(strings.TrimSpace(tag))] = struct{}{} t := strings.ToLower(strings.TrimSpace(tag))
if t != "" {
normalized[t] = struct{}{}
}
} }
// Insert the tags into the DB and return their IDS into the tag ID list // Insert tags and collect IDs
var tagIds []int var tagIDs []int
for tag := range normalized { for tag := range normalized {
var tagId int var tagID int
query := ` _sql, args, err := psql.
INSERT INTO tags (name) VALUES ($1) Insert("tags").
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name Columns("name").
RETURNING id; Values(tag).
` Suffix("ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id").
ToSql()
err := tx.QueryRow(query, tag).Scan(&tagId)
if err != nil { if err != nil {
return fmt.Errorf("Failed to retrieve or create tag. %s\n", err.Error()) return fmt.Errorf("failed to build tag insert query: %w", err)
} }
tagIds = append(tagIds, tagId) if err = tx.QueryRowx(_sql, args...).Scan(&tagID); err != nil {
return fmt.Errorf("failed to retrieve or create tag: %w", err)
} }
// Using a prepared statement, execute the mapping insertions one-by-one tagIDs = append(tagIDs, tagID)
for _, id := range tagIds { }
stmt, err := tx.Prepare("INSERT INTO RecipeTags (RecipeId, TagId) VALUES ($1, $2);")
// Insert recipe <-> tag mappings
for _, tagID := range tagIDs {
_sql, args, err := psql.
Insert("RecipeTags").
Columns("RecipeId", "TagId").
Values(recipe.Id, tagID).
ToSql()
if err != nil { if err != nil {
return fmt.Errorf("Failed to create statement for recipe tag mapping. %s\n", err.Error()) return fmt.Errorf("failed to build recipe tag mapping query: %w", err)
} }
defer stmt.Close()
if _, err := stmt.Exec(recipe.Id, id); err != nil { if _, err = tx.Exec(_sql, args...); err != nil {
return fmt.Errorf("Failed to insert tag-recipe-mapping. %s\n", err.Error()) return fmt.Errorf("failed to insert recipe tag mapping: %w", err)
} }
} }
if err := tx.Commit(); err != nil { if err = tx.Commit(); err != nil {
tx.Rollback()
return err return err
} }
@ -560,75 +595,88 @@ func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string)
// and creates new associations for the provided tags. The recipe object must contain // and creates new associations for the provided tags. The recipe object must contain
// a valid ID. Any errors will be bubbled to the caller. // a valid ID. Any errors will be bubbled to the caller.
func (r *RecipeRepository) UpdateRecipeTags(recipe domain.Recipe, tags []string) error { func (r *RecipeRepository) UpdateRecipeTags(recipe domain.Recipe, tags []string) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // Rollback if we don't commit
if recipe.Id <= 0 { if recipe.Id <= 0 {
return fmt.Errorf("[ERROR] Recipe must have a valid ID") return fmt.Errorf("[ERROR] Recipe must have a valid ID")
} }
// Step 1: Delete all existing tag associations for this recipe tx, err := r.db.Beginx()
deleteQuery := `DELETE FROM RecipeTags WHERE RecipeId = $1;` if err != nil {
if _, err := tx.Exec(deleteQuery, recipe.Id); err != nil { return err
return fmt.Errorf("[ERROR] Failed to delete existing recipe tags: %w", err) }
defer tx.Rollback()
psql := sq.StatementBuilder.
PlaceholderFormat(sq.Dollar).
RunWith(tx)
// Step 1: delete existing tag mappings
{
_sql, args, err := psql.
Delete("RecipeTags").
Where(sq.Eq{"RecipeId": recipe.Id}).
ToSql()
if err != nil {
return fmt.Errorf("[ERROR] failed to build delete recipe tags query: %w", err)
} }
// Step 2: Normalize the tag names (lower case with trimmed space) if _, err = tx.Exec(_sql, args...); err != nil {
normalized := make(map[string]struct{}) // Use map to disallow duplicates return fmt.Errorf("[ERROR] failed to delete existing recipe tags: %w", err)
}
}
// Step 2: normalize tags
normalized := make(map[string]struct{})
for _, tag := range tags { for _, tag := range tags {
trimmed := strings.ToLower(strings.TrimSpace(tag)) t := strings.ToLower(strings.TrimSpace(tag))
if trimmed != "" { if t != "" {
normalized[trimmed] = struct{}{} normalized[t] = struct{}{}
} }
} }
// If no tags provided, we're done (all tags removed) // No tags means "remove all tags" — were done
if len(normalized) == 0 { if len(normalized) == 0 {
if err := tx.Commit(); err != nil { return tx.Commit()
return err
}
return nil
} }
// Step 3: Insert the tags into the DB and return their IDs into the tag ID list // Step 3: upsert tags and collect IDs
var tagIds []int var tagIDs []int
for tag := range normalized { for tag := range normalized {
var tagId int var tagID int
query := `
INSERT INTO tags (name) VALUES ($1) _sql, args, err := psql.
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name Insert("tags").
RETURNING id; Columns("name").
` Values(tag).
err := tx.QueryRow(query, tag).Scan(&tagId) Suffix("ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id").
ToSql()
if err != nil { if err != nil {
return fmt.Errorf("[ERROR] Failed to retrieve or create tag: %w", err) return fmt.Errorf("[ERROR] failed to build tag upsert query: %w", err)
}
tagIds = append(tagIds, tagId)
} }
// Step 4: Insert the new tag associations if err = tx.QueryRowx(_sql, args...).Scan(&tagID); err != nil {
// Use a single prepared statement for all inserts return fmt.Errorf("[ERROR] failed to retrieve or create tag: %w", err)
stmt, err := tx.Prepare("INSERT INTO RecipeTags (RecipeId, TagId) VALUES ($1, $2);") }
tagIDs = append(tagIDs, tagID)
}
// Step 4: insert new recipe ↔ tag mappings
for _, tagID := range tagIDs {
_sql, args, err := psql.
Insert("RecipeTags").
Columns("RecipeId", "TagId").
Values(recipe.Id, tagID).
ToSql()
if err != nil { if err != nil {
return fmt.Errorf("[ERROR] Failed to create statement for recipe tag mapping: %w", err) return fmt.Errorf("[ERROR] failed to build recipe tag mapping query: %w", err)
} }
defer stmt.Close()
for _, id := range tagIds { if _, err = tx.Exec(_sql, args...); err != nil {
if _, err := stmt.Exec(recipe.Id, id); err != nil { return fmt.Errorf("[ERROR] failed to insert recipe tag mapping: %w", err)
return fmt.Errorf("[ERROR] Failed to insert tag-recipe mapping: %w", err)
} }
} }
// Commit the transaction return tx.Commit()
if err := tx.Commit(); err != nil {
return err
}
return nil
} }
// GetUserRecipes gets a list of a users owned recipes. This function does not ensure the user is // GetUserRecipes gets a list of a users owned recipes. This function does not ensure the user is
@ -709,28 +757,21 @@ func (r *RecipeRepository) GetRecipeTags(recipe *domain.Recipe) error {
return nil return nil
} }
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
query := psql.
Select("t.*").
From("tags t").
Join("recipetags rt on rt.tagid = t.id").
Where(sq.Eq{"rt.recipeid": recipe.Id})
_sql, args, err := query.ToSql()
if err != nil {
return fmt.Errorf("Failed to construct sql query: %w", err)
}
recipe.Tags = []domain.Tag{} recipe.Tags = []domain.Tag{}
if err := r.db.Select(&recipe.Tags, _sql, args...); err != nil {
query := ` return fmt.Errorf("Failed to get recipe tags: %w", err)
SELECT t.* FROM tags t
JOIN recipetags rt ON rt.tagid = t.id
WHERE rt.recipeid = $1;
`
rows, err := r.db.Query(query, recipe.Id)
if err != nil {
return fmt.Errorf("Failed to get tags for recipe. %s\n", err.Error())
}
defer rows.Close()
for rows.Next() {
var tag domain.Tag
err := rows.Scan(&tag.Id, &tag.Name, &tag.Created)
if err != nil {
return fmt.Errorf("Failed to scan tag onto domain model. %s\n", err.Error())
}
recipe.Tags = append(recipe.Tags, tag)
} }
return nil return nil