feat: rename ingredients→products, products→user_products; add barcode/OFF import
- Rename catalog: ingredient/* → product/* (canonical_name, barcode, nutrition per 100g)
- Rename pantry: product/* → userproduct/* (user-owned items with expiry)
- Squash migrations into single 001_initial_schema.sql (clean-db baseline)
- product_categories: add English canonical name column; fix COALESCE in queries
- Remove product_translations: product names are stored in their original language
- Add default_unit_name to product API responses via unit_translations JOIN
- Add cmd/importoff: bulk import from OpenFoodFacts JSONL dump (COPY + ON CONFLICT)
- Diary: support product_id entries alongside dish_id (CHECK num_nonnulls = 1)
- Home: getLoggedCalories joins both recipes and catalog products
- Flutter: rename models/providers/services to match backend rename
- Flutter: add barcode scan flow for diary (mobile_scanner, product_portion_sheet)
- Flutter: localise 6 new keys across 12 languages (barcode scan, portion weight)
- Routes: GET /products/search, GET /products/barcode/{barcode}, /user-products
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,11 +44,11 @@ $$ LANGUAGE plpgsql VOLATILE;
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Enums
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TYPE user_plan AS ENUM ('free', 'paid');
|
||||
CREATE TYPE user_gender AS ENUM ('male', 'female');
|
||||
CREATE TYPE user_goal AS ENUM ('lose', 'maintain', 'gain');
|
||||
CREATE TYPE activity_level AS ENUM ('low', 'moderate', 'high');
|
||||
CREATE TYPE recipe_source AS ENUM ('spoonacular', 'ai', 'user');
|
||||
CREATE TYPE user_plan AS ENUM ('free', 'paid');
|
||||
CREATE TYPE user_gender AS ENUM ('male', 'female');
|
||||
CREATE TYPE user_goal AS ENUM ('lose', 'maintain', 'gain');
|
||||
CREATE TYPE activity_level AS ENUM ('low', 'moderate', 'high');
|
||||
CREATE TYPE recipe_source AS ENUM ('spoonacular', 'ai', 'user');
|
||||
CREATE TYPE recipe_difficulty AS ENUM ('easy', 'medium', 'hard');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -104,28 +104,30 @@ CREATE TABLE unit_translations (
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ingredient_categories + translations
|
||||
-- product_categories + product_category_translations
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE ingredient_categories (
|
||||
CREATE TABLE product_categories (
|
||||
slug VARCHAR(50) PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
sort_order SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE ingredient_category_translations (
|
||||
category_slug VARCHAR(50) NOT NULL REFERENCES ingredient_categories(slug) ON DELETE CASCADE,
|
||||
lang VARCHAR(10) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
PRIMARY KEY (category_slug, lang)
|
||||
CREATE TABLE product_category_translations (
|
||||
product_category_slug VARCHAR(50) NOT NULL REFERENCES product_categories(slug) ON DELETE CASCADE,
|
||||
lang VARCHAR(10) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
PRIMARY KEY (product_category_slug, lang)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ingredients (canonical catalog)
|
||||
-- products (canonical catalog)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE ingredients (
|
||||
CREATE TABLE products (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
canonical_name VARCHAR(255) NOT NULL,
|
||||
category VARCHAR(50) REFERENCES ingredient_categories(slug),
|
||||
category VARCHAR(50) REFERENCES product_categories(slug),
|
||||
default_unit VARCHAR(20) REFERENCES units(code),
|
||||
barcode TEXT UNIQUE,
|
||||
calories_per_100g DECIMAL(8,2),
|
||||
protein_per_100g DECIMAL(8,2),
|
||||
fat_per_100g DECIMAL(8,2),
|
||||
@@ -134,33 +136,23 @@ CREATE TABLE ingredients (
|
||||
storage_days INTEGER,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT uq_ingredient_canonical_name UNIQUE (canonical_name)
|
||||
CONSTRAINT uq_product_canonical_name UNIQUE (canonical_name)
|
||||
);
|
||||
CREATE INDEX idx_ingredients_canonical_name ON ingredients (canonical_name);
|
||||
CREATE INDEX idx_ingredients_category ON ingredients (category);
|
||||
CREATE INDEX idx_products_canonical_name ON products (canonical_name);
|
||||
CREATE INDEX idx_products_category ON products (category);
|
||||
CREATE INDEX idx_products_barcode ON products (barcode) WHERE barcode IS NOT NULL;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ingredient_translations
|
||||
-- product_aliases
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE ingredient_translations (
|
||||
ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||
lang VARCHAR(10) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (ingredient_id, lang)
|
||||
CREATE TABLE product_aliases (
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
lang VARCHAR(10) NOT NULL,
|
||||
alias TEXT NOT NULL,
|
||||
PRIMARY KEY (product_id, lang, alias)
|
||||
);
|
||||
CREATE INDEX idx_ingredient_translations_ingredient_id ON ingredient_translations (ingredient_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ingredient_aliases
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE ingredient_aliases (
|
||||
ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||
lang VARCHAR(10) NOT NULL,
|
||||
alias TEXT NOT NULL,
|
||||
PRIMARY KEY (ingredient_id, lang, alias)
|
||||
);
|
||||
CREATE INDEX idx_ingredient_aliases_lookup ON ingredient_aliases (ingredient_id, lang);
|
||||
CREATE INDEX idx_ingredient_aliases_trgm ON ingredient_aliases USING GIN (alias gin_trgm_ops);
|
||||
CREATE INDEX idx_product_aliases_lookup ON product_aliases (product_id, lang);
|
||||
CREATE INDEX idx_product_aliases_trgm ON product_aliases USING GIN (alias gin_trgm_ops);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- cuisines + cuisine_translations
|
||||
@@ -279,22 +271,22 @@ CREATE TABLE recipe_translations (
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- recipe_ingredients + recipe_ingredient_translations
|
||||
-- recipe_products + recipe_product_translations
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE recipe_ingredients (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
recipe_id UUID NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
||||
ingredient_id UUID REFERENCES ingredients(id) ON DELETE SET NULL,
|
||||
name TEXT NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
unit_code VARCHAR(20),
|
||||
is_optional BOOLEAN NOT NULL DEFAULT false,
|
||||
sort_order SMALLINT NOT NULL DEFAULT 0
|
||||
CREATE TABLE recipe_products (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
recipe_id UUID NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
||||
product_id UUID REFERENCES products(id) ON DELETE SET NULL,
|
||||
name TEXT NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
unit_code VARCHAR(20),
|
||||
is_optional BOOLEAN NOT NULL DEFAULT false,
|
||||
sort_order SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_recipe_ingredients_recipe_id ON recipe_ingredients (recipe_id);
|
||||
CREATE INDEX idx_recipe_products_recipe_id ON recipe_products (recipe_id);
|
||||
|
||||
CREATE TABLE recipe_ingredient_translations (
|
||||
ri_id UUID NOT NULL REFERENCES recipe_ingredients(id) ON DELETE CASCADE,
|
||||
CREATE TABLE recipe_product_translations (
|
||||
ri_id UUID NOT NULL REFERENCES recipe_products(id) ON DELETE CASCADE,
|
||||
lang VARCHAR(10) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
PRIMARY KEY (ri_id, lang)
|
||||
@@ -322,29 +314,29 @@ CREATE TABLE recipe_step_translations (
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- products (user fridge / pantry items)
|
||||
-- user_products (user fridge / pantry items)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE products (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
primary_ingredient_id UUID REFERENCES ingredients(id),
|
||||
name TEXT NOT NULL,
|
||||
quantity DECIMAL(10,2) NOT NULL DEFAULT 1,
|
||||
unit TEXT NOT NULL DEFAULT 'pcs' REFERENCES units(code),
|
||||
category TEXT,
|
||||
storage_days INT NOT NULL DEFAULT 7,
|
||||
added_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
CREATE TABLE user_products (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
primary_product_id UUID REFERENCES products(id),
|
||||
name TEXT NOT NULL,
|
||||
quantity DECIMAL(10,2) NOT NULL DEFAULT 1,
|
||||
unit TEXT NOT NULL DEFAULT 'pcs' REFERENCES units(code),
|
||||
category TEXT,
|
||||
storage_days INT NOT NULL DEFAULT 7,
|
||||
added_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX idx_products_user_id ON products (user_id);
|
||||
CREATE INDEX idx_user_products_user_id ON user_products (user_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- product_ingredients (M2M: composite product ↔ ingredients)
|
||||
-- user_product_components (M2M: composite user product ↔ catalog products)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE product_ingredients (
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||
CREATE TABLE user_product_components (
|
||||
user_product_id UUID NOT NULL REFERENCES user_products(id) ON DELETE CASCADE,
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
amount_per_100g DECIMAL(10,2),
|
||||
PRIMARY KEY (product_id, ingredient_id)
|
||||
PRIMARY KEY (user_product_id, product_id)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -394,21 +386,21 @@ CREATE TABLE shopping_lists (
|
||||
-- dish_recognition_jobs
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE dish_recognition_jobs (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
user_plan TEXT NOT NULL,
|
||||
image_base64 TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL DEFAULT 'image/jpeg',
|
||||
lang TEXT NOT NULL DEFAULT 'en',
|
||||
target_date DATE,
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
user_plan TEXT NOT NULL,
|
||||
image_base64 TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL DEFAULT 'image/jpeg',
|
||||
lang TEXT NOT NULL DEFAULT 'en',
|
||||
target_date DATE,
|
||||
target_meal_type TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
-- pending | processing | done | failed
|
||||
result JSONB,
|
||||
error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ
|
||||
result JSONB,
|
||||
error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_dish_recognition_jobs_user
|
||||
ON dish_recognition_jobs (user_id, created_at DESC);
|
||||
@@ -420,16 +412,18 @@ CREATE INDEX idx_dish_recognition_jobs_pending
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE meal_diary (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
date DATE NOT NULL,
|
||||
meal_type TEXT NOT NULL,
|
||||
portions DECIMAL(5,2) NOT NULL DEFAULT 1,
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
dish_id UUID NOT NULL REFERENCES dishes(id) ON DELETE RESTRICT,
|
||||
recipe_id UUID REFERENCES recipes(id) ON DELETE SET NULL,
|
||||
dish_id UUID REFERENCES dishes(id) ON DELETE RESTRICT,
|
||||
product_id UUID REFERENCES products(id) ON DELETE RESTRICT,
|
||||
recipe_id UUID REFERENCES recipes(id) ON DELETE SET NULL,
|
||||
portion_g DECIMAL(10,2),
|
||||
job_id UUID REFERENCES dish_recognition_jobs(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
job_id UUID REFERENCES dish_recognition_jobs(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT chk_meal_entry_source CHECK (num_nonnulls(dish_id, product_id) = 1)
|
||||
);
|
||||
CREATE INDEX idx_meal_diary_user_date ON meal_diary (user_id, date);
|
||||
CREATE INDEX idx_meal_diary_job_id ON meal_diary (job_id) WHERE job_id IS NOT NULL;
|
||||
@@ -471,18 +465,18 @@ INSERT INTO unit_translations (unit_code, lang, name) VALUES
|
||||
('pack', 'ru', 'уп');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Seed data: ingredient_categories + ingredient_category_translations
|
||||
-- Seed data: product_categories + product_category_translations
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO ingredient_categories (slug, sort_order) VALUES
|
||||
('dairy', 1),
|
||||
('meat', 2),
|
||||
('produce', 3),
|
||||
('bakery', 4),
|
||||
('frozen', 5),
|
||||
('beverages', 6),
|
||||
('other', 7);
|
||||
INSERT INTO product_categories (slug, name, sort_order) VALUES
|
||||
('dairy', 'Dairy', 1),
|
||||
('meat', 'Meat', 2),
|
||||
('produce', 'Produce', 3),
|
||||
('bakery', 'Bakery', 4),
|
||||
('frozen', 'Frozen', 5),
|
||||
('beverages', 'Beverages', 6),
|
||||
('other', 'Other', 7);
|
||||
|
||||
INSERT INTO ingredient_category_translations (category_slug, lang, name) VALUES
|
||||
INSERT INTO product_category_translations (product_category_slug, lang, name) VALUES
|
||||
('dairy', 'ru', 'Молочные продукты'),
|
||||
('meat', 'ru', 'Мясо и птица'),
|
||||
('produce', 'ru', 'Овощи и фрукты'),
|
||||
@@ -617,12 +611,12 @@ DROP TABLE IF EXISTS shopping_lists;
|
||||
DROP TABLE IF EXISTS menu_items;
|
||||
DROP TABLE IF EXISTS menu_plans;
|
||||
DROP TABLE IF EXISTS user_saved_recipes;
|
||||
DROP TABLE IF EXISTS product_ingredients;
|
||||
DROP TABLE IF EXISTS products;
|
||||
DROP TABLE IF EXISTS user_product_components;
|
||||
DROP TABLE IF EXISTS user_products;
|
||||
DROP TABLE IF EXISTS recipe_step_translations;
|
||||
DROP TABLE IF EXISTS recipe_steps;
|
||||
DROP TABLE IF EXISTS recipe_ingredient_translations;
|
||||
DROP TABLE IF EXISTS recipe_ingredients;
|
||||
DROP TABLE IF EXISTS recipe_product_translations;
|
||||
DROP TABLE IF EXISTS recipe_products;
|
||||
DROP TABLE IF EXISTS recipe_translations;
|
||||
DROP TABLE IF EXISTS recipes;
|
||||
DROP TABLE IF EXISTS dish_tags;
|
||||
@@ -634,11 +628,10 @@ DROP TABLE IF EXISTS tag_translations;
|
||||
DROP TABLE IF EXISTS tags;
|
||||
DROP TABLE IF EXISTS cuisine_translations;
|
||||
DROP TABLE IF EXISTS cuisines;
|
||||
DROP TABLE IF EXISTS ingredient_aliases;
|
||||
DROP TABLE IF EXISTS ingredient_translations;
|
||||
DROP TABLE IF EXISTS ingredients;
|
||||
DROP TABLE IF EXISTS ingredient_category_translations;
|
||||
DROP TABLE IF EXISTS ingredient_categories;
|
||||
DROP TABLE IF EXISTS product_aliases;
|
||||
DROP TABLE IF EXISTS products;
|
||||
DROP TABLE IF EXISTS product_category_translations;
|
||||
DROP TABLE IF EXISTS product_categories;
|
||||
DROP TABLE IF EXISTS unit_translations;
|
||||
DROP TABLE IF EXISTS units;
|
||||
DROP TABLE IF EXISTS languages;
|
||||
|
||||
Reference in New Issue
Block a user