(FIX): Finished translating the entire recipe repo to new infra.
This commit is contained in:
parent
413510d2cd
commit
30a5396d11
@ -20,13 +20,8 @@
|
||||
go
|
||||
gopls
|
||||
go-tools
|
||||
htmx-lsp2
|
||||
templ
|
||||
tailwindcss_4
|
||||
tailwindcss-language-server
|
||||
watchman
|
||||
docker-language-server
|
||||
dockerfile-language-server-nodejs
|
||||
gcc_multi
|
||||
glibc_multi
|
||||
nodejs
|
||||
|
||||
@ -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
|
||||
// and the standard "not-found" error will be returned.
|
||||
func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
||||
query := `SELECT
|
||||
id, title, description, instructions, serves, difficulty, duration, category, ingredients,
|
||||
userid, modified, created, deleted
|
||||
FROM recipes
|
||||
WHERE id = $1 AND deleted = false;
|
||||
`
|
||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
|
||||
query := psql.
|
||||
Select(
|
||||
"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 instructions pq.StringArray
|
||||
var ingredientBytes []byte
|
||||
|
||||
var recipe domain.Recipe
|
||||
if err := r.db.QueryRow(query, id).Scan(
|
||||
if err := r.db.QueryRowx(_sql, args...).Scan(
|
||||
&recipe.Id,
|
||||
&recipe.Title,
|
||||
&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
|
||||
// to the caller.
|
||||
func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string) error {
|
||||
tx, err := r.db.Begin()
|
||||
tx, err := r.db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Normalize the tag names (lower case with trimmed space)
|
||||
normalized := make(map[string]struct{}) // Use map to disallow duplicates
|
||||
psql := sq.StatementBuilder.
|
||||
PlaceholderFormat(sq.Dollar).
|
||||
RunWith(tx)
|
||||
|
||||
// Normalize tags (lowercase, trimmed, no duplicates)
|
||||
normalized := make(map[string]struct{})
|
||||
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
|
||||
var tagIds []int
|
||||
// Insert tags and collect IDs
|
||||
var tagIDs []int
|
||||
for tag := range normalized {
|
||||
var tagId int
|
||||
var tagID int
|
||||
|
||||
query := `
|
||||
INSERT INTO tags (name) VALUES ($1)
|
||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id;
|
||||
`
|
||||
|
||||
err := tx.QueryRow(query, tag).Scan(&tagId)
|
||||
_sql, args, err := psql.
|
||||
Insert("tags").
|
||||
Columns("name").
|
||||
Values(tag).
|
||||
Suffix("ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id").
|
||||
ToSql()
|
||||
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)
|
||||
}
|
||||
|
||||
tagIDs = append(tagIDs, tagID)
|
||||
}
|
||||
|
||||
// Using a prepared statement, execute the mapping insertions one-by-one
|
||||
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 {
|
||||
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 {
|
||||
return fmt.Errorf("Failed to insert tag-recipe-mapping. %s\n", err.Error())
|
||||
if _, err = tx.Exec(_sql, args...); err != nil {
|
||||
return fmt.Errorf("failed to insert recipe tag mapping: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
if err = tx.Commit(); err != nil {
|
||||
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
|
||||
// a valid ID. Any errors will be bubbled to the caller.
|
||||
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 {
|
||||
return fmt.Errorf("[ERROR] Recipe must have a valid ID")
|
||||
}
|
||||
|
||||
// Step 1: Delete all existing tag associations for this recipe
|
||||
deleteQuery := `DELETE FROM RecipeTags WHERE RecipeId = $1;`
|
||||
if _, err := tx.Exec(deleteQuery, recipe.Id); err != nil {
|
||||
return fmt.Errorf("[ERROR] Failed to delete existing recipe tags: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Normalize the tag names (lower case with trimmed space)
|
||||
normalized := make(map[string]struct{}) // Use map to disallow duplicates
|
||||
for _, tag := range tags {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(tag))
|
||||
if trimmed != "" {
|
||||
normalized[trimmed] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// If no tags provided, we're done (all tags removed)
|
||||
if len(normalized) == 0 {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 3: Insert the tags into the DB and return their IDs into the tag ID list
|
||||
var tagIds []int
|
||||
for tag := range normalized {
|
||||
var tagId int
|
||||
query := `
|
||||
INSERT INTO tags (name) VALUES ($1)
|
||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id;
|
||||
`
|
||||
err := tx.QueryRow(query, tag).Scan(&tagId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[ERROR] Failed to retrieve or create tag: %w", err)
|
||||
}
|
||||
tagIds = append(tagIds, tagId)
|
||||
}
|
||||
|
||||
// Step 4: Insert the new tag associations
|
||||
// Use a single prepared statement for all inserts
|
||||
stmt, err := tx.Prepare("INSERT INTO RecipeTags (RecipeId, TagId) VALUES ($1, $2);")
|
||||
tx, err := r.db.Beginx()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[ERROR] Failed to create statement for recipe tag mapping: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, id := range tagIds {
|
||||
if _, err := stmt.Exec(recipe.Id, id); err != nil {
|
||||
return fmt.Errorf("[ERROR] Failed to insert tag-recipe mapping: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
return nil
|
||||
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)
|
||||
}
|
||||
|
||||
if _, err = tx.Exec(_sql, args...); err != nil {
|
||||
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 {
|
||||
t := strings.ToLower(strings.TrimSpace(tag))
|
||||
if t != "" {
|
||||
normalized[t] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// No tags means "remove all tags" — we’re done
|
||||
if len(normalized) == 0 {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Step 3: upsert tags and collect IDs
|
||||
var tagIDs []int
|
||||
for tag := range normalized {
|
||||
var tagID int
|
||||
|
||||
_sql, args, err := psql.
|
||||
Insert("tags").
|
||||
Columns("name").
|
||||
Values(tag).
|
||||
Suffix("ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id").
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[ERROR] failed to build tag upsert query: %w", err)
|
||||
}
|
||||
|
||||
if err = tx.QueryRowx(_sql, args...).Scan(&tagID); err != nil {
|
||||
return fmt.Errorf("[ERROR] failed to retrieve or create tag: %w", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("[ERROR] failed to build recipe tag mapping query: %w", err)
|
||||
}
|
||||
|
||||
if _, err = tx.Exec(_sql, args...); err != nil {
|
||||
return fmt.Errorf("[ERROR] failed to insert recipe tag mapping: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
recipe.Tags = []domain.Tag{}
|
||||
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})
|
||||
|
||||
query := `
|
||||
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)
|
||||
_sql, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get tags for recipe. %s\n", err.Error())
|
||||
return fmt.Errorf("Failed to construct sql query: %w", err)
|
||||
}
|
||||
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)
|
||||
recipe.Tags = []domain.Tag{}
|
||||
if err := r.db.Select(&recipe.Tags, _sql, args...); err != nil {
|
||||
return fmt.Errorf("Failed to get recipe tags: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user