feat(ADM-018): completed feature
This commit is contained in:
27
project/migrations/005_categories.js
Normal file
27
project/migrations/005_categories.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Categories module table. Module-owned naming: categories_<table>.
|
||||
*/
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE categories_categories (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
parent_id uuid REFERENCES categories_categories(id) ON DELETE RESTRICT,
|
||||
name text NOT NULL,
|
||||
slug text NOT NULL UNIQUE,
|
||||
seo_title text,
|
||||
seo_description text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT categories_categories_no_self_parent CHECK (parent_id IS NULL OR parent_id <> id)
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX categories_categories_parent_id_idx ON categories_categories (parent_id)');
|
||||
pgm.sql('CREATE INDEX categories_categories_slug_idx ON categories_categories (slug)');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS categories_categories');
|
||||
};
|
||||
41
project/migrations/006_catalog_products.js
Normal file
41
project/migrations/006_catalog_products.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Catalog core product tables. Module-owned naming: catalog_<table>.
|
||||
*/
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE catalog_products (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
slug text NOT NULL UNIQUE,
|
||||
description text,
|
||||
state text NOT NULL DEFAULT 'draft',
|
||||
seo_title text,
|
||||
seo_description text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT catalog_products_state_check CHECK (state IN ('draft', 'active', 'archived'))
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX catalog_products_state_idx ON catalog_products (state)');
|
||||
pgm.sql('CREATE INDEX catalog_products_slug_idx ON catalog_products (slug)');
|
||||
|
||||
pgm.sql(`
|
||||
CREATE TABLE catalog_product_categories (
|
||||
product_id uuid NOT NULL REFERENCES catalog_products(id) ON DELETE CASCADE,
|
||||
category_id uuid NOT NULL REFERENCES categories_categories(id) ON DELETE RESTRICT,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (product_id, category_id)
|
||||
)
|
||||
`);
|
||||
pgm.sql(
|
||||
'CREATE INDEX catalog_product_categories_category_id_idx ON catalog_product_categories (category_id)',
|
||||
);
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS catalog_product_categories');
|
||||
pgm.sql('DROP TABLE IF EXISTS catalog_products');
|
||||
};
|
||||
32
project/migrations/007_brands.js
Normal file
32
project/migrations/007_brands.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Brands module table and catalog product-brand assignment.
|
||||
*/
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE brands_brands (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
slug text NOT NULL UNIQUE,
|
||||
seo_title text,
|
||||
seo_description text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX brands_brands_slug_idx ON brands_brands (slug)');
|
||||
|
||||
pgm.sql(`
|
||||
ALTER TABLE catalog_products
|
||||
ADD COLUMN brand_id uuid REFERENCES brands_brands(id) ON DELETE SET NULL
|
||||
`);
|
||||
pgm.sql('CREATE INDEX catalog_products_brand_id_idx ON catalog_products (brand_id)');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP INDEX IF EXISTS catalog_products_brand_id_idx');
|
||||
pgm.sql('ALTER TABLE catalog_products DROP COLUMN IF EXISTS brand_id');
|
||||
pgm.sql('DROP TABLE IF EXISTS brands_brands');
|
||||
};
|
||||
45
project/migrations/008_catalog_variants_rich_data.js
Normal file
45
project/migrations/008_catalog_variants_rich_data.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Catalog product variants and rich product data.
|
||||
*/
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE catalog_product_variants (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id uuid NOT NULL REFERENCES catalog_products(id) ON DELETE CASCADE,
|
||||
sku text NOT NULL UNIQUE,
|
||||
ean text UNIQUE,
|
||||
attributes jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
`);
|
||||
pgm.sql(
|
||||
'CREATE INDEX catalog_product_variants_product_id_idx ON catalog_product_variants (product_id)',
|
||||
);
|
||||
|
||||
pgm.sql(`
|
||||
CREATE TABLE catalog_product_rich_data (
|
||||
product_id uuid PRIMARY KEY REFERENCES catalog_products(id) ON DELETE CASCADE,
|
||||
ingredients text,
|
||||
allergens text[] NOT NULL DEFAULT ARRAY[]::text[],
|
||||
nutrition jsonb,
|
||||
nutrition_source text,
|
||||
is_organic boolean NOT NULL DEFAULT false,
|
||||
organic_certification text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT catalog_product_rich_data_nutrition_source_check
|
||||
CHECK (nutrition_source IS NULL OR nutrition_source IN ('manual', 'manufacturer', 'openfoodfacts')),
|
||||
CONSTRAINT catalog_product_rich_data_nutrition_source_required_check
|
||||
CHECK (nutrition IS NULL OR nutrition_source IS NOT NULL)
|
||||
)
|
||||
`);
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS catalog_product_rich_data');
|
||||
pgm.sql('DROP TABLE IF EXISTS catalog_product_variants');
|
||||
};
|
||||
63
project/migrations/009_catalog_product_images.js
Normal file
63
project/migrations/009_catalog_product_images.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Catalog product and variant images.
|
||||
*/
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE catalog_product_images (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id uuid NOT NULL REFERENCES catalog_products(id) ON DELETE CASCADE,
|
||||
variant_id uuid REFERENCES catalog_product_variants(id) ON DELETE CASCADE,
|
||||
url text NOT NULL,
|
||||
alt_text text NOT NULL,
|
||||
position integer NOT NULL DEFAULT 0,
|
||||
role text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT catalog_product_images_position_check CHECK (position >= 0),
|
||||
CONSTRAINT catalog_product_images_role_check CHECK (role IN ('main', 'gallery'))
|
||||
)
|
||||
`);
|
||||
pgm.sql(
|
||||
'CREATE INDEX catalog_product_images_product_scope_idx ON catalog_product_images (product_id, variant_id, position)',
|
||||
);
|
||||
pgm.sql(`
|
||||
CREATE UNIQUE INDEX catalog_product_images_product_main_unique
|
||||
ON catalog_product_images (product_id)
|
||||
WHERE variant_id IS NULL AND role = 'main'
|
||||
`);
|
||||
pgm.sql(`
|
||||
CREATE UNIQUE INDEX catalog_product_images_variant_main_unique
|
||||
ON catalog_product_images (variant_id)
|
||||
WHERE variant_id IS NOT NULL AND role = 'main'
|
||||
`);
|
||||
pgm.sql(`
|
||||
CREATE OR REPLACE FUNCTION catalog_product_image_variant_scope_check()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF NEW.variant_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM catalog_product_variants v
|
||||
WHERE v.id = NEW.variant_id AND v.product_id = NEW.product_id
|
||||
) THEN
|
||||
RAISE EXCEPTION 'catalog product image variant must belong to product';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
`);
|
||||
pgm.sql(`
|
||||
CREATE TRIGGER catalog_product_images_variant_scope_check
|
||||
BEFORE INSERT OR UPDATE OF product_id, variant_id ON catalog_product_images
|
||||
FOR EACH ROW EXECUTE FUNCTION catalog_product_image_variant_scope_check()
|
||||
`);
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql(
|
||||
'DROP TRIGGER IF EXISTS catalog_product_images_variant_scope_check ON catalog_product_images',
|
||||
);
|
||||
pgm.sql('DROP FUNCTION IF EXISTS catalog_product_image_variant_scope_check');
|
||||
pgm.sql('DROP TABLE IF EXISTS catalog_product_images');
|
||||
};
|
||||
47
project/migrations/010_catalog_search_fts.js
Normal file
47
project/migrations/010_catalog_search_fts.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* PostgreSQL full-text search support for catalog products.
|
||||
*/
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE INDEX catalog_products_fts_idx
|
||||
ON catalog_products USING gin (
|
||||
(
|
||||
setweight(to_tsvector('spanish', COALESCE(name, '')), 'A') ||
|
||||
setweight(to_tsvector('spanish', COALESCE(description, '')), 'B') ||
|
||||
setweight(to_tsvector('spanish', COALESCE(seo_title, '')), 'B') ||
|
||||
setweight(to_tsvector('spanish', COALESCE(seo_description, '')), 'C')
|
||||
)
|
||||
)
|
||||
`);
|
||||
pgm.sql(`
|
||||
CREATE INDEX brands_brands_fts_idx
|
||||
ON brands_brands USING gin (
|
||||
(
|
||||
setweight(to_tsvector('spanish', COALESCE(name, '')), 'A') ||
|
||||
setweight(to_tsvector('spanish', COALESCE(slug, '')), 'B') ||
|
||||
setweight(to_tsvector('spanish', COALESCE(seo_title, '')), 'B') ||
|
||||
setweight(to_tsvector('spanish', COALESCE(seo_description, '')), 'C')
|
||||
)
|
||||
)
|
||||
`);
|
||||
pgm.sql(`
|
||||
CREATE INDEX categories_categories_fts_idx
|
||||
ON categories_categories USING gin (
|
||||
(
|
||||
setweight(to_tsvector('spanish', COALESCE(name, '')), 'A') ||
|
||||
setweight(to_tsvector('spanish', COALESCE(slug, '')), 'B') ||
|
||||
setweight(to_tsvector('spanish', COALESCE(seo_title, '')), 'B') ||
|
||||
setweight(to_tsvector('spanish', COALESCE(seo_description, '')), 'C')
|
||||
)
|
||||
)
|
||||
`);
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP INDEX IF EXISTS categories_categories_fts_idx');
|
||||
pgm.sql('DROP INDEX IF EXISTS brands_brands_fts_idx');
|
||||
pgm.sql('DROP INDEX IF EXISTS catalog_products_fts_idx');
|
||||
};
|
||||
44
project/migrations/011_inventory.js
Normal file
44
project/migrations/011_inventory.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Inventory stock counters and movement audit.
|
||||
*/
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE inventory_stock (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
variant_id uuid NOT NULL UNIQUE,
|
||||
available integer NOT NULL DEFAULT 0,
|
||||
reserved integer NOT NULL DEFAULT 0,
|
||||
sold integer NOT NULL DEFAULT 0,
|
||||
incoming integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT inventory_stock_available_non_negative CHECK (available >= 0),
|
||||
CONSTRAINT inventory_stock_reserved_non_negative CHECK (reserved >= 0),
|
||||
CONSTRAINT inventory_stock_sold_non_negative CHECK (sold >= 0),
|
||||
CONSTRAINT inventory_stock_incoming_non_negative CHECK (incoming >= 0)
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX inventory_stock_variant_id_idx ON inventory_stock (variant_id)');
|
||||
|
||||
pgm.sql(`
|
||||
CREATE TABLE inventory_movements (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
variant_id uuid NOT NULL,
|
||||
operation text NOT NULL,
|
||||
quantity integer NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT inventory_movements_quantity_non_negative CHECK (quantity >= 0),
|
||||
CONSTRAINT inventory_movements_operation_check
|
||||
CHECK (operation IN ('reserve', 'release', 'confirm', 'set_available'))
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX inventory_movements_variant_id_idx ON inventory_movements (variant_id)');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS inventory_movements');
|
||||
pgm.sql('DROP TABLE IF EXISTS inventory_stock');
|
||||
};
|
||||
52
project/migrations/012_pricing.js
Normal file
52
project/migrations/012_pricing.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Pricing variant prices and append-only price history.
|
||||
*/
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE pricing_variant_prices (
|
||||
variant_id uuid PRIMARY KEY,
|
||||
net_unit_amount_cents integer NOT NULL,
|
||||
vat_rate text NOT NULL,
|
||||
currency text NOT NULL DEFAULT 'EUR',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT pricing_variant_prices_amount_non_negative CHECK (net_unit_amount_cents >= 0),
|
||||
CONSTRAINT pricing_variant_prices_vat_rate_check CHECK (vat_rate IN ('general', 'reduced')),
|
||||
CONSTRAINT pricing_variant_prices_currency_check CHECK (currency = 'EUR')
|
||||
)
|
||||
`);
|
||||
|
||||
pgm.sql(`
|
||||
CREATE TABLE pricing_price_history (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
variant_id uuid NOT NULL,
|
||||
previous_net_unit_amount_cents integer,
|
||||
previous_vat_rate text,
|
||||
previous_currency text,
|
||||
new_net_unit_amount_cents integer NOT NULL,
|
||||
new_vat_rate text NOT NULL,
|
||||
new_currency text NOT NULL DEFAULT 'EUR',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT pricing_price_history_previous_amount_non_negative
|
||||
CHECK (previous_net_unit_amount_cents IS NULL OR previous_net_unit_amount_cents >= 0),
|
||||
CONSTRAINT pricing_price_history_new_amount_non_negative CHECK (new_net_unit_amount_cents >= 0),
|
||||
CONSTRAINT pricing_price_history_previous_vat_rate_check
|
||||
CHECK (previous_vat_rate IS NULL OR previous_vat_rate IN ('general', 'reduced')),
|
||||
CONSTRAINT pricing_price_history_new_vat_rate_check CHECK (new_vat_rate IN ('general', 'reduced')),
|
||||
CONSTRAINT pricing_price_history_previous_currency_check
|
||||
CHECK (previous_currency IS NULL OR previous_currency = 'EUR'),
|
||||
CONSTRAINT pricing_price_history_new_currency_check CHECK (new_currency = 'EUR')
|
||||
)
|
||||
`);
|
||||
pgm.sql(
|
||||
'CREATE INDEX pricing_price_history_variant_id_idx ON pricing_price_history (variant_id)',
|
||||
);
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS pricing_price_history');
|
||||
pgm.sql('DROP TABLE IF EXISTS pricing_variant_prices');
|
||||
};
|
||||
34
project/migrations/013_cart.js
Normal file
34
project/migrations/013_cart.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/** Cart persistence: product, variant and quantity only. */
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE cart_carts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL UNIQUE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
`);
|
||||
|
||||
pgm.sql(`
|
||||
CREATE TABLE cart_items (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
cart_id uuid NOT NULL REFERENCES cart_carts(id) ON DELETE CASCADE,
|
||||
product_id uuid NOT NULL,
|
||||
variant_id uuid NOT NULL,
|
||||
quantity integer NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT cart_items_quantity_positive CHECK (quantity > 0),
|
||||
CONSTRAINT cart_items_unique_variant_per_cart UNIQUE (cart_id, variant_id)
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX cart_items_cart_id_idx ON cart_items (cart_id)');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS cart_items');
|
||||
pgm.sql('DROP TABLE IF EXISTS cart_carts');
|
||||
};
|
||||
31
project/migrations/014_promotions.js
Normal file
31
project/migrations/014_promotions.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/** Promotions and selected cart promo code. */
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE promotions_promotions (
|
||||
code text PRIMARY KEY,
|
||||
type text NOT NULL,
|
||||
value integer NOT NULL,
|
||||
starts_at timestamptz NOT NULL,
|
||||
ends_at timestamptz NOT NULL,
|
||||
usage_limit integer,
|
||||
usage_count integer NOT NULL DEFAULT 0,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT promotions_type_check CHECK (type IN ('percent', 'fixed_amount')),
|
||||
CONSTRAINT promotions_value_positive CHECK (value > 0),
|
||||
CONSTRAINT promotions_usage_limit_positive CHECK (usage_limit IS NULL OR usage_limit > 0),
|
||||
CONSTRAINT promotions_usage_count_non_negative CHECK (usage_count >= 0),
|
||||
CONSTRAINT promotions_window_check CHECK (ends_at > starts_at)
|
||||
)
|
||||
`);
|
||||
pgm.sql('ALTER TABLE cart_carts ADD COLUMN promo_code text');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('ALTER TABLE cart_carts DROP COLUMN IF EXISTS promo_code');
|
||||
pgm.sql('DROP TABLE IF EXISTS promotions_promotions');
|
||||
};
|
||||
39
project/migrations/015_shipping.js
Normal file
39
project/migrations/015_shipping.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/** Shipping zones and methods. */
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE shipping_zones (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
country text NOT NULL,
|
||||
postal_code_prefix text,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX shipping_zones_country_idx ON shipping_zones (country, active)');
|
||||
|
||||
pgm.sql(`
|
||||
CREATE TABLE shipping_methods (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
zone_id uuid NOT NULL REFERENCES shipping_zones(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
base_cost_cents integer NOT NULL,
|
||||
free_shipping_threshold_cents integer,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT shipping_methods_base_cost_non_negative CHECK (base_cost_cents >= 0),
|
||||
CONSTRAINT shipping_methods_threshold_non_negative CHECK (free_shipping_threshold_cents IS NULL OR free_shipping_threshold_cents >= 0)
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX shipping_methods_zone_id_idx ON shipping_methods (zone_id)');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS shipping_methods');
|
||||
pgm.sql('DROP TABLE IF EXISTS shipping_zones');
|
||||
};
|
||||
58
project/migrations/016_orders.js
Normal file
58
project/migrations/016_orders.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/** Orders with item snapshots and explicit state machine. */
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE orders_orders (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL,
|
||||
idempotency_key text UNIQUE,
|
||||
state text NOT NULL DEFAULT 'PENDING',
|
||||
currency text NOT NULL DEFAULT 'EUR',
|
||||
subtotal_cents integer NOT NULL DEFAULT 0,
|
||||
discount_cents integer NOT NULL DEFAULT 0,
|
||||
tax_cents integer NOT NULL DEFAULT 0,
|
||||
total_cents integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT orders_orders_state_check CHECK (state IN (
|
||||
'PENDING','AWAITING_PAYMENT','PAID','PROCESSING','SHIPPED','DELIVERED',
|
||||
'CANCELLED','REFUNDED','PARTIALLY_REFUNDED'
|
||||
)),
|
||||
CONSTRAINT orders_orders_currency_check CHECK (currency = 'EUR'),
|
||||
CONSTRAINT orders_orders_subtotal_non_negative CHECK (subtotal_cents >= 0),
|
||||
CONSTRAINT orders_orders_discount_non_negative CHECK (discount_cents >= 0),
|
||||
CONSTRAINT orders_orders_tax_non_negative CHECK (tax_cents >= 0),
|
||||
CONSTRAINT orders_orders_total_non_negative CHECK (total_cents >= 0)
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX orders_orders_user_id_idx ON orders_orders (user_id)');
|
||||
|
||||
pgm.sql(`
|
||||
CREATE TABLE orders_items (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id uuid NOT NULL REFERENCES orders_orders(id) ON DELETE CASCADE,
|
||||
product_id uuid NOT NULL,
|
||||
variant_id uuid NOT NULL,
|
||||
sku text NOT NULL,
|
||||
ean text,
|
||||
name text NOT NULL,
|
||||
unit_price_cents integer NOT NULL,
|
||||
discount_cents integer NOT NULL DEFAULT 0,
|
||||
tax_cents integer NOT NULL DEFAULT 0,
|
||||
quantity integer NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT orders_items_quantity_positive CHECK (quantity > 0),
|
||||
CONSTRAINT orders_items_unit_price_non_negative CHECK (unit_price_cents >= 0),
|
||||
CONSTRAINT orders_items_discount_non_negative CHECK (discount_cents >= 0),
|
||||
CONSTRAINT orders_items_tax_non_negative CHECK (tax_cents >= 0)
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX orders_items_order_id_idx ON orders_items (order_id)');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS orders_items');
|
||||
pgm.sql('DROP TABLE IF EXISTS orders_orders');
|
||||
};
|
||||
29
project/migrations/017_payments.js
Normal file
29
project/migrations/017_payments.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/** Payment transactions and idempotency. */
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE payments_transactions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider text NOT NULL,
|
||||
provider_event_id text NOT NULL,
|
||||
provider_payment_id text,
|
||||
order_id uuid,
|
||||
amount_cents integer NOT NULL,
|
||||
currency text NOT NULL DEFAULT 'EUR',
|
||||
status text NOT NULL,
|
||||
raw jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT payments_transactions_amount_non_negative CHECK (amount_cents >= 0),
|
||||
CONSTRAINT payments_transactions_status_check CHECK (status IN ('requires_payment','succeeded','failed','refunded','chargeback')),
|
||||
CONSTRAINT payments_transactions_currency_check CHECK (currency = 'EUR'),
|
||||
CONSTRAINT payments_transactions_provider_event_unique UNIQUE (provider, provider_event_id)
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX payments_transactions_order_id_idx ON payments_transactions (order_id)');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS payments_transactions');
|
||||
};
|
||||
26
project/migrations/018_notifications.js
Normal file
26
project/migrations/018_notifications.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/** Notification messages with idempotent dispatch. */
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE notifications_messages (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id text NOT NULL UNIQUE,
|
||||
channel text NOT NULL DEFAULT 'email',
|
||||
template text NOT NULL,
|
||||
recipient text NOT NULL,
|
||||
subject text NOT NULL,
|
||||
body text NOT NULL,
|
||||
status text NOT NULL DEFAULT 'sent',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT notifications_messages_channel_check CHECK (channel = 'email'),
|
||||
CONSTRAINT notifications_messages_status_check CHECK (status IN ('sent', 'failed', 'queued'))
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX notifications_messages_template_idx ON notifications_messages (template)');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS notifications_messages');
|
||||
};
|
||||
31
project/migrations/019_reviews.js
Normal file
31
project/migrations/019_reviews.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/** Verified-purchase reviews. */
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE reviews_reviews (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL,
|
||||
product_id uuid NOT NULL,
|
||||
order_id uuid NOT NULL,
|
||||
order_item_id uuid NOT NULL UNIQUE,
|
||||
rating integer NOT NULL,
|
||||
title text NOT NULL,
|
||||
body text NOT NULL,
|
||||
status text NOT NULL DEFAULT 'pending',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT reviews_reviews_rating_check CHECK (rating BETWEEN 1 AND 5),
|
||||
CONSTRAINT reviews_reviews_status_check CHECK (status IN ('pending','published','rejected'))
|
||||
)
|
||||
`);
|
||||
pgm.sql(
|
||||
'CREATE INDEX reviews_reviews_product_status_idx ON reviews_reviews (product_id, status)',
|
||||
);
|
||||
pgm.sql('CREATE INDEX reviews_reviews_user_id_idx ON reviews_reviews (user_id)');
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS reviews_reviews');
|
||||
};
|
||||
22
project/migrations/020_cms.js
Normal file
22
project/migrations/020_cms.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/** CMS pages with slug routing and draft/publish states. */
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE cms_pages (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
slug text NOT NULL UNIQUE,
|
||||
title text NOT NULL,
|
||||
body text NOT NULL,
|
||||
status text NOT NULL DEFAULT 'draft',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT cms_pages_status_check CHECK (status IN ('draft','published'))
|
||||
)
|
||||
`);
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS cms_pages');
|
||||
};
|
||||
28
project/migrations/021_security.js
Normal file
28
project/migrations/021_security.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/** Security audit log. */
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE security_audit_log (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
actor_id uuid,
|
||||
action text NOT NULL,
|
||||
target text NOT NULL,
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
`);
|
||||
pgm.sql('CREATE INDEX security_audit_log_actor_id_idx ON security_audit_log (actor_id)');
|
||||
pgm.sql('CREATE INDEX security_audit_log_action_idx ON security_audit_log (action)');
|
||||
|
||||
pgm.sql(`
|
||||
ALTER TABLE identity_users
|
||||
ADD COLUMN mfa_enrolled boolean NOT NULL DEFAULT false
|
||||
`);
|
||||
};
|
||||
|
||||
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
||||
export const down = (pgm) => {
|
||||
pgm.sql('ALTER TABLE identity_users DROP COLUMN IF EXISTS mfa_enrolled');
|
||||
pgm.sql('DROP TABLE IF EXISTS security_audit_log');
|
||||
};
|
||||
30
project/migrations/022_store_settings.js
Normal file
30
project/migrations/022_store_settings.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Migration 022: Store settings key-value table.
|
||||
* Stores dynamic store configuration editable from admin panel.
|
||||
*/
|
||||
exports.up = async (pgm) => {
|
||||
await pgm.sql(`
|
||||
CREATE TABLE IF NOT EXISTS store_settings (
|
||||
key VARCHAR(64) PRIMARY KEY,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_by UUID REFERENCES identity_users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Seed default settings
|
||||
INSERT INTO store_settings (key, value) VALUES
|
||||
('store_name', 'Mercado de Vida'),
|
||||
('store_tagline', 'Productos naturales y ecológicos'),
|
||||
('contact_email', 'info@mercadodevida.es'),
|
||||
('contact_phone', ''),
|
||||
('contact_address', ''),
|
||||
('footer_text', '© 2026 Mercado de Vida. Todos los derechos reservados.'),
|
||||
('facebook_url', ''),
|
||||
('instagram_url', '')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = async (pgm) => {
|
||||
await pgm.sql(`DROP TABLE IF EXISTS store_settings;`);
|
||||
};
|
||||
27
project/migrations/023_tax_rates.js
Normal file
27
project/migrations/023_tax_rates.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Migration 023: Tax rates configuration.
|
||||
* Stores configurable tax rates per category.
|
||||
*/
|
||||
exports.up = async (pgm) => {
|
||||
pgm.sql(`
|
||||
CREATE TABLE IF NOT EXISTS tax_rates (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(40) NOT NULL,
|
||||
rate_percent NUMERIC(5,2) NOT NULL,
|
||||
country VARCHAR(2) NOT NULL DEFAULT 'ES',
|
||||
applies_to VARCHAR(20) NOT NULL DEFAULT 'general',
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
INSERT INTO tax_rates (name, rate_percent, country, applies_to, active) VALUES
|
||||
('IVA General', 21.0, 'ES', 'general', true),
|
||||
('IVA Reducido', 10.0, 'ES', 'reduced', true),
|
||||
('IVA Superreducido', 4.0, 'ES', 'super-reduced', true)
|
||||
ON CONFLICT DO NOTHING;
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = async (pgm) => {
|
||||
pgm.sql('DROP TABLE IF EXISTS tax_rates');
|
||||
};
|
||||
46
project/migrations/024_product_channels_attributes.js
Normal file
46
project/migrations/024_product_channels_attributes.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// migration: 024_product_channels_attributes
|
||||
// Adds sales channels, featured flag, and attribute tags to the catalog_products table
|
||||
|
||||
/** @param {import('pg').Pool} pool */
|
||||
exports.up = async (pool) => {
|
||||
// Add channels column
|
||||
await pool.query(`
|
||||
ALTER TABLE catalog_products
|
||||
ADD COLUMN IF NOT EXISTS channels TEXT NOT NULL DEFAULT 'all'
|
||||
CHECK (channels IN ('online', 'offline', 'all'))
|
||||
`);
|
||||
|
||||
// Add featured column
|
||||
await pool.query(`
|
||||
ALTER TABLE catalog_products
|
||||
ADD COLUMN IF NOT EXISTS featured BOOLEAN NOT NULL DEFAULT FALSE
|
||||
`);
|
||||
|
||||
// Add attributes column (stored as JSONB array of strings)
|
||||
await pool.query(`
|
||||
ALTER TABLE catalog_products
|
||||
ADD COLUMN IF NOT EXISTS attributes JSONB NOT NULL DEFAULT '[]'::jsonb
|
||||
`);
|
||||
|
||||
// Add offer_cents and cost_cents to pricing_variant_prices
|
||||
await pool.query(`
|
||||
ALTER TABLE pricing_variant_prices
|
||||
ADD COLUMN IF NOT EXISTS offer_cents INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS cost_cents INTEGER
|
||||
`);
|
||||
|
||||
// Create index for featured products (common query)
|
||||
await pool.query(`
|
||||
CREATE INDEX IF NOT EXISTS catalog_products_featured_idx
|
||||
ON catalog_products (featured) WHERE featured = TRUE
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = async (pool) => {
|
||||
await pool.query(`ALTER TABLE catalog_products DROP COLUMN IF EXISTS channels`);
|
||||
await pool.query(`ALTER TABLE catalog_products DROP COLUMN IF EXISTS featured`);
|
||||
await pool.query(`ALTER TABLE catalog_products DROP COLUMN IF EXISTS attributes`);
|
||||
await pool.query(`ALTER TABLE pricing_variant_prices DROP COLUMN IF EXISTS offer_cents`);
|
||||
await pool.query(`ALTER TABLE pricing_variant_prices DROP COLUMN IF EXISTS cost_cents`);
|
||||
await pool.query(`DROP INDEX IF EXISTS catalog_products_featured_idx`);
|
||||
};
|
||||
57
project/migrations/025_cms_footer_pages.js
Normal file
57
project/migrations/025_cms_footer_pages.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// migration: 025_cms_footer_pages
|
||||
// Seeds initial CMS pages for footer links if they don't exist yet
|
||||
|
||||
/** @param {import('pg').Pool} pool */
|
||||
exports.up = async (pool) => {
|
||||
const pages = [
|
||||
{
|
||||
slug: 'about',
|
||||
title: 'Quiénes somos',
|
||||
body: '<h2>Nuestra historia</h2><p>MercadoDeVida nació de la convicción de que comer bien no debería ser complicado.</p><h2>Nuestra misión</h2><p>Facilitar el acceso a productos naturales y orgánicos de alta calidad.</p>',
|
||||
status: 'published',
|
||||
},
|
||||
{
|
||||
slug: 'contact',
|
||||
title: 'Contacto',
|
||||
body: '<h2>Email</h2><p><a href="mailto:hola@mercadodevida.es">hola@mercadodevida.es</a></p><p>Intentamos responder en 24-48h laborables.</p>',
|
||||
status: 'published',
|
||||
},
|
||||
{
|
||||
slug: 'shipping',
|
||||
title: 'Envíos y entregas',
|
||||
body: '<h2>Zonas de envío</h2><p>España peninsular. Canarias, Ceuta y Melilla: contacta con nosotros.</p><h2>Envío estándar (3-5 días)</h2><p>Coste según peso. Gratis a partir de un umbral.</p><h2>Envío express 24h</h2><p>Disponible para pedidos antes de las 13:00h.</p>',
|
||||
status: 'published',
|
||||
},
|
||||
{
|
||||
slug: 'privacy',
|
||||
title: 'Política de privacidad',
|
||||
body: '<h2>Responsable del tratamiento</h2><p>MercadoDeVida. hola@mercadodevida.es</p><h2>Finalidad</h2><p>Gestión de pedidos, atención al cliente y envío de comunicaciones comerciales.</p><h2>Tus derechos</h2><p>Acceso, rectificación, supresión y portabilidad de tus datos. Contacta: hola@mercadodevida.es</p>',
|
||||
status: 'published',
|
||||
},
|
||||
{
|
||||
slug: 'terms',
|
||||
title: 'Términos y condiciones',
|
||||
body: '<h2>Contratación</h2><p>Al realizar un pedido aceptas estos términos. Los precios incluyen IVA.</p><h2>Envíos</h2><p>Ver sección de envíos.</p><h2>Devoluciones</h2><p>Tienes 14 días para devolver productos no perecederos en su estado original.</p>',
|
||||
status: 'published',
|
||||
},
|
||||
{
|
||||
slug: 'cookies',
|
||||
title: 'Política de cookies',
|
||||
body: '<h2>¿Qué son las cookies?</h2><p>Pequños archivos que se almacenan en tu navegador para mejorar tu experiencia.</p><h2>Cookies que usamos</h2><p>Cookies de sesión (carrito, login), cookies analíticas (opcionales) y cookies de preferencias.</p><h2>Tu consentimiento</h2><p>Al navegar aceptas el uso de cookies. Puedes desactivarlas en tu navegador.</p>',
|
||||
status: 'published',
|
||||
},
|
||||
];
|
||||
|
||||
for (const p of pages) {
|
||||
await pool.query(
|
||||
`INSERT INTO cms_pages (slug, title, body, status, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
ON CONFLICT (slug) DO NOTHING`,
|
||||
[p.slug, p.title, p.body, p.status],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async (pool) => {
|
||||
await pool.query(`DELETE FROM cms_pages WHERE slug IN ('about','contact','shipping','privacy','terms','cookies')`);
|
||||
};
|
||||
83
project/migrations/026_fuzzy_search.cjs
Normal file
83
project/migrations/026_fuzzy_search.cjs
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Migration 026: Fuzzy search with trigram similarity (pg_trgm)
|
||||
*
|
||||
* - Enables pg_trgm extension for Levenshtein-like fuzzy matching
|
||||
* - Creates GIN indexes on product name/description for fast trigram similarity
|
||||
* - Adds similarity index on brands and categories for term suggestions
|
||||
*
|
||||
* Run: node migrations/026_fuzzy_search.js
|
||||
*/
|
||||
|
||||
const { Pool } = require('pg');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgres://mdv:mdv_dev_only@localhost:5432/mercadodevida',
|
||||
});
|
||||
|
||||
async function migrate() {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 1. Enable pg_trgm extension
|
||||
await client.query(`
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
`);
|
||||
console.log('✓ pg_trgm extension enabled');
|
||||
|
||||
// 2. GIN index on product name for trigram similarity
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS catalog_products_name_trgm_idx
|
||||
ON catalog_products
|
||||
USING GIN (name gin_trgm_ops);
|
||||
`);
|
||||
console.log('✓ catalog_products name trigram index');
|
||||
|
||||
// 3. GIN index on product description
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS catalog_products_description_trgm_idx
|
||||
ON catalog_products
|
||||
USING GIN (description gin_trgm_ops);
|
||||
`);
|
||||
console.log('✓ catalog_products description trigram index');
|
||||
|
||||
// 4. GIN index on brand names (for suggestions)
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS brands_brands_name_trgm_idx
|
||||
ON brands_brands
|
||||
USING GIN (name gin_trgm_ops);
|
||||
`);
|
||||
console.log('✓ brands_brands name trigram index');
|
||||
|
||||
// 5. GIN index on category names (for suggestions)
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS categories_categories_name_trgm_idx
|
||||
ON categories_categories
|
||||
USING GIN (name gin_trgm_ops);
|
||||
`);
|
||||
console.log('✓ categories_categories name trigram index');
|
||||
|
||||
// 6. Record migration
|
||||
await client.query(`
|
||||
INSERT INTO pgmigrations (name, run_on)
|
||||
VALUES ('026_fuzzy_search', NOW())
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
`);
|
||||
console.log('✓ Migration recorded');
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log('\n✅ Migration 026 complete: fuzzy search indexes created');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
migrate().catch((err) => {
|
||||
console.error('❌ Migration failed:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user