feat: implement comprehensive recipe management schema
- Add full database schema with accounts, dishes, ingredients, categories, and pricing - Implement custom types for weight, currency, recipe difficulty, and color hex - Add soft delete pattern with deleted_at and active views for all tables - Include journaling triggers for created_at/updated_at automation feat: add SQLC configuration with proper type overrides - Configure SQLC to use UUID, decimal, and timestamptz types with proper Go mappings - Add github.com/shopspring/decimal dependency for precise decimal handling - Set up proper pointer handling for nullable fields in generated structs refactor: replace simple food_items with full dish management system - Remove old FoodItem model and replace with comprehensive Dish model - Implement dish creation and retrieval queries with full field support - Add ingredient management with weight/amount tracking chore: update infrastructure dependencies - Switch to custom PostgreSQL image with pg_idkit extension for UUIDv7 support - Add DATABASE_URI environment variable configuration - Update Docker Compose configuration for new database image chore: organize SQL with fold markers and section comments - Add vim fold markers for better code navigation - Structure schema into clear sections: extensions, types, tables, triggers - Separate query files with organized comment blocks
This commit is contained in:
@@ -28,6 +28,7 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/dig v1.19.0 // indirect
|
||||
|
||||
@@ -52,6 +52,8 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
||||
@@ -5,11 +5,399 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type FoodItem struct {
|
||||
Guid pgtype.UUID
|
||||
Title string
|
||||
Description string
|
||||
type Currency string
|
||||
|
||||
const (
|
||||
CurrencyRUB Currency = "RUB"
|
||||
CurrencyEUR Currency = "EUR"
|
||||
CurrencyUSD Currency = "USD"
|
||||
)
|
||||
|
||||
func (e *Currency) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = Currency(s)
|
||||
case string:
|
||||
*e = Currency(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for Currency: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullCurrency struct {
|
||||
Currency Currency
|
||||
Valid bool // Valid is true if Currency is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullCurrency) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.Currency, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.Currency.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullCurrency) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.Currency), nil
|
||||
}
|
||||
|
||||
type RecipeDifficulty string
|
||||
|
||||
const (
|
||||
RecipeDifficultyBeginner RecipeDifficulty = "beginner"
|
||||
RecipeDifficultyEasy RecipeDifficulty = "easy"
|
||||
RecipeDifficultyMedium RecipeDifficulty = "medium"
|
||||
RecipeDifficultyHard RecipeDifficulty = "hard"
|
||||
RecipeDifficultyExpert RecipeDifficulty = "expert"
|
||||
)
|
||||
|
||||
func (e *RecipeDifficulty) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = RecipeDifficulty(s)
|
||||
case string:
|
||||
*e = RecipeDifficulty(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for RecipeDifficulty: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullRecipeDifficulty struct {
|
||||
RecipeDifficulty RecipeDifficulty
|
||||
Valid bool // Valid is true if RecipeDifficulty is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullRecipeDifficulty) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.RecipeDifficulty, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.RecipeDifficulty.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullRecipeDifficulty) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.RecipeDifficulty), nil
|
||||
}
|
||||
|
||||
type Weight string
|
||||
|
||||
const (
|
||||
WeightMg Weight = "mg"
|
||||
WeightG Weight = "g"
|
||||
WeightKg Weight = "kg"
|
||||
WeightLb Weight = "lb"
|
||||
WeightOz Weight = "oz"
|
||||
)
|
||||
|
||||
func (e *Weight) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = Weight(s)
|
||||
case string:
|
||||
*e = Weight(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for Weight: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullWeight struct {
|
||||
Weight Weight
|
||||
Valid bool // Valid is true if Weight is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullWeight) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.Weight, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.Weight.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullWeight) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.Weight), nil
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
Guid uuid.UUID
|
||||
Username string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveAccount struct {
|
||||
Guid uuid.UUID
|
||||
Username string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveDish struct {
|
||||
Guid uuid.UUID
|
||||
Title string
|
||||
Description *string
|
||||
Instructions *string
|
||||
PreparationTimeMinutes *int16
|
||||
CookingTimeMinutes *int16
|
||||
Difficulty RecipeDifficulty
|
||||
ThumbnailS3Key *uuid.UUID
|
||||
MadePublicAt *time.Time
|
||||
Tags []string
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveDishCategory struct {
|
||||
Guid uuid.UUID
|
||||
Title string
|
||||
Description *string
|
||||
Color *string
|
||||
SortOrder *int16
|
||||
ThumbnailS3Key *uuid.UUID
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveDishCategoryDish struct {
|
||||
Guid uuid.UUID
|
||||
DishCategoryGuid uuid.UUID
|
||||
DishGuid uuid.UUID
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveDishIngredient struct {
|
||||
Guid uuid.UUID
|
||||
DishGuid uuid.UUID
|
||||
IngredientGuid uuid.UUID
|
||||
Amount *decimal.Decimal
|
||||
Weight *decimal.Decimal
|
||||
WeightUnit Weight
|
||||
Notes *string
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveIngredient struct {
|
||||
Guid uuid.UUID
|
||||
Title string
|
||||
Description *string
|
||||
ThumbnailS3Key *uuid.UUID
|
||||
MadePublicAt *time.Time
|
||||
Tags []string
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveIngredientPrice struct {
|
||||
Guid uuid.UUID
|
||||
IngredientGuid uuid.UUID
|
||||
Price decimal.Decimal
|
||||
PriceCurrency Currency
|
||||
Weight decimal.Decimal
|
||||
WeightUnit Weight
|
||||
StoreGuid uuid.UUID
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveLoginInfo struct {
|
||||
Guid uuid.UUID
|
||||
AccountGuid uuid.UUID
|
||||
Email *string
|
||||
PasswordHash string
|
||||
SuspendedAt *time.Time
|
||||
SuspendedReason *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveProfile struct {
|
||||
Guid uuid.UUID
|
||||
AccountGuid uuid.UUID
|
||||
Name string
|
||||
Surname *string
|
||||
Patronymic *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ActiveStore struct {
|
||||
Guid uuid.UUID
|
||||
Title string
|
||||
Description *string
|
||||
ThumbnailS3Key *uuid.UUID
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Dish struct {
|
||||
Guid uuid.UUID
|
||||
Title string
|
||||
Description *string
|
||||
Instructions *string
|
||||
PreparationTimeMinutes *int16
|
||||
CookingTimeMinutes *int16
|
||||
Difficulty RecipeDifficulty
|
||||
ThumbnailS3Key *uuid.UUID
|
||||
MadePublicAt *time.Time
|
||||
Tags []string
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type DishCategoriesDish struct {
|
||||
Guid uuid.UUID
|
||||
DishCategoryGuid uuid.UUID
|
||||
DishGuid uuid.UUID
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type DishCategory struct {
|
||||
Guid uuid.UUID
|
||||
Title string
|
||||
Description *string
|
||||
Color *string
|
||||
SortOrder *int16
|
||||
ThumbnailS3Key *uuid.UUID
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type DishIngredient struct {
|
||||
Guid uuid.UUID
|
||||
DishGuid uuid.UUID
|
||||
IngredientGuid uuid.UUID
|
||||
Amount *decimal.Decimal
|
||||
Weight *decimal.Decimal
|
||||
WeightUnit Weight
|
||||
Notes *string
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Ingredient struct {
|
||||
Guid uuid.UUID
|
||||
Title string
|
||||
Description *string
|
||||
ThumbnailS3Key *uuid.UUID
|
||||
MadePublicAt *time.Time
|
||||
Tags []string
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type IngredientPrice struct {
|
||||
Guid uuid.UUID
|
||||
IngredientGuid uuid.UUID
|
||||
Price decimal.Decimal
|
||||
PriceCurrency Currency
|
||||
Weight decimal.Decimal
|
||||
WeightUnit Weight
|
||||
StoreGuid uuid.UUID
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type LoginInfo struct {
|
||||
Guid uuid.UUID
|
||||
AccountGuid uuid.UUID
|
||||
Email *string
|
||||
PasswordHash string
|
||||
SuspendedAt *time.Time
|
||||
SuspendedReason *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
Guid uuid.UUID
|
||||
AccountGuid uuid.UUID
|
||||
Name string
|
||||
Surname *string
|
||||
Patronymic *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
Guid uuid.UUID
|
||||
Title string
|
||||
Description *string
|
||||
ThumbnailS3Key *uuid.UUID
|
||||
Author *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type ViewAccountProfile struct {
|
||||
Guid uuid.UUID
|
||||
Username string
|
||||
Name string
|
||||
Surname *string
|
||||
Patronymic *string
|
||||
CreatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
@@ -7,22 +7,74 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const createFoodItem = `-- name: CreateFoodItem :one
|
||||
INSERT INTO food_items(title, description)
|
||||
VALUES ($1, $2)
|
||||
RETURNING guid, title, description
|
||||
const createDish = `-- name: CreateDish :one
|
||||
INSERT INTO dishes(title, description, difficulty, thumbnail_s3_key)
|
||||
VALUES($1, $2, $3, $4)
|
||||
RETURNING guid, title, description, instructions, preparation_time_minutes, cooking_time_minutes, difficulty, thumbnail_s3_key, made_public_at, tags, author, created_at, updated_at, deleted_at
|
||||
`
|
||||
|
||||
type CreateFoodItemParams struct {
|
||||
Title string
|
||||
Description string
|
||||
type CreateDishParams struct {
|
||||
Title string
|
||||
Description *string
|
||||
Difficulty RecipeDifficulty
|
||||
ThumbnailS3Key *uuid.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) CreateFoodItem(ctx context.Context, arg CreateFoodItemParams) (FoodItem, error) {
|
||||
row := q.db.QueryRow(ctx, createFoodItem, arg.Title, arg.Description)
|
||||
var i FoodItem
|
||||
err := row.Scan(&i.Guid, &i.Title, &i.Description)
|
||||
func (q *Queries) CreateDish(ctx context.Context, arg CreateDishParams) (Dish, error) {
|
||||
row := q.db.QueryRow(ctx, createDish,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Difficulty,
|
||||
arg.ThumbnailS3Key,
|
||||
)
|
||||
var i Dish
|
||||
err := row.Scan(
|
||||
&i.Guid,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Instructions,
|
||||
&i.PreparationTimeMinutes,
|
||||
&i.CookingTimeMinutes,
|
||||
&i.Difficulty,
|
||||
&i.ThumbnailS3Key,
|
||||
&i.MadePublicAt,
|
||||
&i.Tags,
|
||||
&i.Author,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.DeletedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getDish = `-- name: GetDish :one
|
||||
SELECT guid, title, description, instructions, preparation_time_minutes, cooking_time_minutes, difficulty, thumbnail_s3_key, made_public_at, tags, author, created_at, updated_at, deleted_at FROM active_dishes
|
||||
WHERE guid = $1
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetDish(ctx context.Context, guid uuid.UUID) (ActiveDish, error) {
|
||||
row := q.db.QueryRow(ctx, getDish, guid)
|
||||
var i ActiveDish
|
||||
err := row.Scan(
|
||||
&i.Guid,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Instructions,
|
||||
&i.PreparationTimeMinutes,
|
||||
&i.CookingTimeMinutes,
|
||||
&i.Difficulty,
|
||||
&i.ThumbnailS3Key,
|
||||
&i.MadePublicAt,
|
||||
&i.Tags,
|
||||
&i.Author,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.DeletedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user