From d6270144259f71501402cce1c8b506678e588f51 Mon Sep 17 00:00:00 2001 From: "Sven (AAMOS AI)" Date: Wed, 5 Aug 2026 19:21:32 +0700 Subject: [PATCH] Fas 1a: product analytics schema + GDPR controls --- apps/api/src/routes/auth.ts | 10 + apps/api/src/routes/me.ts | 3 +- apps/mobile/src/app/profile.tsx | 1 + apps/mobile/src/locales/da/common.json | 3 +- apps/mobile/src/locales/de/common.json | 3 +- apps/mobile/src/locales/en/common.json | 3 +- apps/mobile/src/locales/es/common.json | 3 +- apps/mobile/src/locales/fi/common.json | 3 +- apps/mobile/src/locales/fr/common.json | 3 +- apps/mobile/src/locales/it/common.json | 3 +- apps/mobile/src/locales/nb/common.json | 3 +- apps/mobile/src/locales/nl/common.json | 3 +- apps/mobile/src/locales/pl/common.json | 3 +- apps/mobile/src/locales/pt/common.json | 3 +- apps/mobile/src/locales/sv/common.json | 3 +- .../migrations/0001_eager_wilson_fisk.sql | 37 + .../migrations/meta/0001_snapshot.json | 8598 +++++++++++++++++ infrastructure/migrations/meta/_journal.json | 9 +- packages/database/src/analytics-gdpr.ts | 50 + packages/database/src/index.ts | 1 + packages/database/src/schema/analytics.ts | 67 + packages/database/src/schema/index.ts | 1 + packages/database/test/analytics-gdpr.test.ts | 105 + packages/shared-types/src/analytics.ts | 132 + packages/shared-types/src/enums.ts | 2 + packages/shared-types/src/index.ts | 1 + 26 files changed, 9039 insertions(+), 14 deletions(-) create mode 100644 infrastructure/migrations/0001_eager_wilson_fisk.sql create mode 100644 infrastructure/migrations/meta/0001_snapshot.json create mode 100644 packages/database/src/analytics-gdpr.ts create mode 100644 packages/database/src/schema/analytics.ts create mode 100644 packages/database/test/analytics-gdpr.test.ts create mode 100644 packages/shared-types/src/analytics.ts diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 2798d9c..96d12b1 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -54,6 +54,16 @@ export async function authRoutes(app: FastifyInstance) { .values({ userId: user.id }) .onConflictDoNothing(); + // Product analytics: legitimate interest with opt-out (spec §8, §33). + const consentNow = new Date(); + await app.db.insert(schema.userConsents).values({ + userId: user.id, + kind: "product_analytics", + status: "granted", + grantedAt: consentNow, + updatedAt: consentNow, + }); + // 7 dagars trial utan kort startar direkt (spec §46) const now = new Date(); await app.db diff --git a/apps/api/src/routes/me.ts b/apps/api/src/routes/me.ts index 685f05d..2fad5c1 100644 --- a/apps/api/src/routes/me.ts +++ b/apps/api/src/routes/me.ts @@ -1,6 +1,6 @@ import type { FastifyInstance } from "fastify"; import { eq } from "drizzle-orm"; -import { schema } from "@app/database"; +import { deleteUserAnalyticsEvents, schema } from "@app/database"; import { consentInputSchema, onboardingInputSchema, @@ -316,6 +316,7 @@ export async function meRoutes(app: FastifyInstance) { .delete(schema.userHealthProfiles) .where(eq(schema.userHealthProfiles.userId, userId)); await app.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, userId)); + await deleteUserAnalyticsEvents(app.db, userId); await app.db.delete(schema.refreshTokens).where(eq(schema.refreshTokens.userId, userId)); await app.db.delete(schema.userCredentials).where(eq(schema.userCredentials.userId, userId)); await app.db diff --git a/apps/mobile/src/app/profile.tsx b/apps/mobile/src/app/profile.tsx index 53e48f4..6ee63dd 100644 --- a/apps/mobile/src/app/profile.tsx +++ b/apps/mobile/src/app/profile.tsx @@ -45,6 +45,7 @@ const CONSENT_KINDS = [ "health_integration", "location_weather", "push_notifications", + "product_analytics", ] as const; export default function ProfileScreen() { diff --git a/apps/mobile/src/locales/da/common.json b/apps/mobile/src/locales/da/common.json index bbfca36..34d80f0 100644 --- a/apps/mobile/src/locales/da/common.json +++ b/apps/mobile/src/locales/da/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ 1 mere …", "home.moreItems_other": "+ {count} flere …", "shopping.completedBody_one": "1 vare blev lagt i madlageret.", - "shopping.completedBody_other": "{count} varer blev lagt i madlageret." + "shopping.completedBody_other": "{count} varer blev lagt i madlageret.", + "profile.consent.product_analytics": "Produktanalyse" } diff --git a/apps/mobile/src/locales/de/common.json b/apps/mobile/src/locales/de/common.json index 589ef68..ab09d01 100644 --- a/apps/mobile/src/locales/de/common.json +++ b/apps/mobile/src/locales/de/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ 1 weiterer …", "home.moreItems_other": "+ {count} weitere …", "shopping.completedBody_one": "1 Artikel wurde in den Vorrat übernommen.", - "shopping.completedBody_other": "{count} Artikel wurden in den Vorrat übernommen." + "shopping.completedBody_other": "{count} Artikel wurden in den Vorrat übernommen.", + "profile.consent.product_analytics": "Produktanalyse" } diff --git a/apps/mobile/src/locales/en/common.json b/apps/mobile/src/locales/en/common.json index ebcad46..543d8e9 100644 --- a/apps/mobile/src/locales/en/common.json +++ b/apps/mobile/src/locales/en/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ 1 more …", "home.moreItems_other": "+ {count} more …", "shopping.completedBody_one": "1 item was added to your inventory.", - "shopping.completedBody_other": "{count} items were added to your inventory." + "shopping.completedBody_other": "{count} items were added to your inventory.", + "profile.consent.product_analytics": "Product analytics" } diff --git a/apps/mobile/src/locales/es/common.json b/apps/mobile/src/locales/es/common.json index 12a0aa1..e330756 100644 --- a/apps/mobile/src/locales/es/common.json +++ b/apps/mobile/src/locales/es/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ 1 más …", "home.moreItems_other": "+ {count} más …", "shopping.completedBody_one": "Se añadió 1 producto al inventario.", - "shopping.completedBody_other": "Se añadieron {count} productos al inventario." + "shopping.completedBody_other": "Se añadieron {count} productos al inventario.", + "profile.consent.product_analytics": "Análisis de producto" } diff --git a/apps/mobile/src/locales/fi/common.json b/apps/mobile/src/locales/fi/common.json index 46b8470..902c111 100644 --- a/apps/mobile/src/locales/fi/common.json +++ b/apps/mobile/src/locales/fi/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ 1 lisää …", "home.moreItems_other": "+ {count} lisää …", "shopping.completedBody_one": "1 tuote lisättiin ruokavarastoon.", - "shopping.completedBody_other": "{count} tuotetta lisättiin ruokavarastoon." + "shopping.completedBody_other": "{count} tuotetta lisättiin ruokavarastoon.", + "profile.consent.product_analytics": "Tuoteanalyysi" } diff --git a/apps/mobile/src/locales/fr/common.json b/apps/mobile/src/locales/fr/common.json index 49b0654..e53099f 100644 --- a/apps/mobile/src/locales/fr/common.json +++ b/apps/mobile/src/locales/fr/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ 1 autre …", "home.moreItems_other": "+ {count} autres …", "shopping.completedBody_one": "1 article a été ajouté au stock.", - "shopping.completedBody_other": "{count} articles ont été ajoutés au stock." + "shopping.completedBody_other": "{count} articles ont été ajoutés au stock.", + "profile.consent.product_analytics": "Analyse produit" } diff --git a/apps/mobile/src/locales/it/common.json b/apps/mobile/src/locales/it/common.json index 2ff224a..59b8030 100644 --- a/apps/mobile/src/locales/it/common.json +++ b/apps/mobile/src/locales/it/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ 1 altro …", "home.moreItems_other": "+ {count} altri …", "shopping.completedBody_one": "1 prodotto aggiunto alla dispensa.", - "shopping.completedBody_other": "{count} prodotti aggiunti alla dispensa." + "shopping.completedBody_other": "{count} prodotti aggiunti alla dispensa.", + "profile.consent.product_analytics": "Analisi del prodotto" } diff --git a/apps/mobile/src/locales/nb/common.json b/apps/mobile/src/locales/nb/common.json index e45f344..34bca76 100644 --- a/apps/mobile/src/locales/nb/common.json +++ b/apps/mobile/src/locales/nb/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ 1 til …", "home.moreItems_other": "+ {count} til …", "shopping.completedBody_one": "1 vare ble lagt inn i matlageret.", - "shopping.completedBody_other": "{count} varer ble lagt inn i matlageret." + "shopping.completedBody_other": "{count} varer ble lagt inn i matlageret.", + "profile.consent.product_analytics": "Produktanalyse" } diff --git a/apps/mobile/src/locales/nl/common.json b/apps/mobile/src/locales/nl/common.json index 5dba213..df186da 100644 --- a/apps/mobile/src/locales/nl/common.json +++ b/apps/mobile/src/locales/nl/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ nog 1 …", "home.moreItems_other": "+ nog {count} …", "shopping.completedBody_one": "1 product is aan de voorraad toegevoegd.", - "shopping.completedBody_other": "{count} producten zijn aan de voorraad toegevoegd." + "shopping.completedBody_other": "{count} producten zijn aan de voorraad toegevoegd.", + "profile.consent.product_analytics": "Productanalyse" } diff --git a/apps/mobile/src/locales/pl/common.json b/apps/mobile/src/locales/pl/common.json index d78474b..dfe7517 100644 --- a/apps/mobile/src/locales/pl/common.json +++ b/apps/mobile/src/locales/pl/common.json @@ -348,5 +348,6 @@ "shopping.completedBody_one": "1 produkt trafił do spiżarni.", "shopping.completedBody_few": "{count} produkty trafiły do spiżarni.", "shopping.completedBody_many": "{count} produktów trafiło do spiżarni.", - "shopping.completedBody_other": "{count} produktu trafiło do spiżarni." + "shopping.completedBody_other": "{count} produktu trafiło do spiżarni.", + "profile.consent.product_analytics": "Analiza produktu" } diff --git a/apps/mobile/src/locales/pt/common.json b/apps/mobile/src/locales/pt/common.json index af2edf8..3b7092b 100644 --- a/apps/mobile/src/locales/pt/common.json +++ b/apps/mobile/src/locales/pt/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ mais 1 …", "home.moreItems_other": "+ mais {count} …", "shopping.completedBody_one": "1 produto foi adicionado à despensa.", - "shopping.completedBody_other": "{count} produtos foram adicionados à despensa." + "shopping.completedBody_other": "{count} produtos foram adicionados à despensa.", + "profile.consent.product_analytics": "Análise de produto" } diff --git a/apps/mobile/src/locales/sv/common.json b/apps/mobile/src/locales/sv/common.json index 6193a72..5ecfad8 100644 --- a/apps/mobile/src/locales/sv/common.json +++ b/apps/mobile/src/locales/sv/common.json @@ -334,5 +334,6 @@ "home.moreItems_one": "+ 1 till …", "home.moreItems_other": "+ {count} till …", "shopping.completedBody_one": "1 vara lades in i matlagret.", - "shopping.completedBody_other": "{count} varor lades in i matlagret." + "shopping.completedBody_other": "{count} varor lades in i matlagret.", + "profile.consent.product_analytics": "Produktanalys" } diff --git a/infrastructure/migrations/0001_eager_wilson_fisk.sql b/infrastructure/migrations/0001_eager_wilson_fisk.sql new file mode 100644 index 0000000..dbd382d --- /dev/null +++ b/infrastructure/migrations/0001_eager_wilson_fisk.sql @@ -0,0 +1,37 @@ +ALTER TYPE "public"."consent_kind" ADD VALUE 'product_analytics';--> statement-breakpoint +CREATE TABLE "analytics_daily_snapshots" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "date" varchar(10) NOT NULL, + "household_id" uuid, + "event_name" varchar(64) NOT NULL, + "count" integer DEFAULT 0 NOT NULL, + "unique_sessions" integer DEFAULT 0 NOT NULL, + "properties_fingerprint" varchar(64) +); +--> statement-breakpoint +CREATE TABLE "product_analytics_events" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "occurred_at" timestamp with time zone DEFAULT now() NOT NULL, + "received_at" timestamp with time zone DEFAULT now() NOT NULL, + "event_name" varchar(64) NOT NULL, + "anonymous_id" varchar(64), + "session_id" varchar(64), + "user_id" uuid, + "household_id" uuid, + "app_version" varchar(32), + "platform" varchar(16), + "locale" varchar(16), + "experiment_variant" varchar(128), + "properties" jsonb DEFAULT '{}'::jsonb NOT NULL +); +--> statement-breakpoint +ALTER TABLE "analytics_daily_snapshots" ADD CONSTRAINT "analytics_daily_snapshots_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "product_analytics_events" ADD CONSTRAINT "product_analytics_events_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "product_analytics_events" ADD CONSTRAINT "product_analytics_events_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "pa_daily_household_date_idx" ON "analytics_daily_snapshots" USING btree ("household_id","date");--> statement-breakpoint +CREATE INDEX "pa_daily_date_event_idx" ON "analytics_daily_snapshots" USING btree ("date","event_name");--> statement-breakpoint +CREATE INDEX "pa_events_name_occurred_idx" ON "product_analytics_events" USING btree ("event_name","occurred_at");--> statement-breakpoint +CREATE INDEX "pa_events_household_occurred_idx" ON "product_analytics_events" USING btree ("household_id","occurred_at");--> statement-breakpoint +CREATE INDEX "pa_events_user_occurred_idx" ON "product_analytics_events" USING btree ("user_id","occurred_at");--> statement-breakpoint +CREATE INDEX "pa_events_session_idx" ON "product_analytics_events" USING btree ("session_id");--> statement-breakpoint +CREATE INDEX "pa_events_received_idx" ON "product_analytics_events" USING btree ("received_at"); \ No newline at end of file diff --git a/infrastructure/migrations/meta/0001_snapshot.json b/infrastructure/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..750f2dc --- /dev/null +++ b/infrastructure/migrations/meta/0001_snapshot.json @@ -0,0 +1,8598 @@ +{ + "id": "46df54d4-8878-49c0-89f3-6e0f0b67f5ab", + "prevId": "d83177b4-c967-4df0-8c6d-a72c25766aa4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.admin_totp": { + "name": "admin_totp", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "secret_base32": { + "name": "secret_base32", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled_at": { + "name": "enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_step": { + "name": "last_used_step", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "admin_totp_user_id_users_id_fk": { + "name": "admin_totp_user_id_users_id_fk", + "tableFrom": "admin_totp", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification_tokens": { + "name": "email_verification_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_verification_tokens_hash_unique": { + "name": "email_verification_tokens_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "password_reset_tokens_hash_unique": { + "name": "password_reset_tokens_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family_id": { + "name": "family_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "replaced_by_token_id": { + "name": "replaced_by_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "refresh_tokens_user_idx": { + "name": "refresh_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "refresh_tokens_hash_unique": { + "name": "refresh_tokens_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_consents": { + "name": "user_consents", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "consent_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "consent_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_consents_user_id_users_id_fk": { + "name": "user_consents_user_id_users_id_fk", + "tableFrom": "user_consents", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_consents_user_id_kind_pk": { + "name": "user_consents_user_id_kind_pk", + "columns": [ + "user_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_credentials": { + "name": "user_credentials", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_updated_at": { + "name": "password_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_credentials_user_id_users_id_fk": { + "name": "user_credentials_user_id_users_id_fk", + "tableFrom": "user_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_health_profiles": { + "name": "user_health_profiles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "birth_year": { + "name": "birth_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sex": { + "name": "sex", + "type": "sex", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "height_cm": { + "name": "height_cm", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "weight_kg": { + "name": "weight_kg", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "target_weight_kg": { + "name": "target_weight_kg", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "activity_level": { + "name": "activity_level", + "type": "activity_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'moderate'" + }, + "training_sessions_per_week": { + "name": "training_sessions_per_week", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "training_types": { + "name": "training_types", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_health_profiles_user_id_users_id_fk": { + "name": "user_health_profiles_user_id_users_id_fk", + "tableFrom": "user_health_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "primary_goal": { + "name": "primary_goal", + "type": "goal_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "goals": { + "name": "goals", + "type": "goal_type[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "diet_pattern": { + "name": "diet_pattern", + "type": "diet_pattern", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'omnivore'" + }, + "religious_rule": { + "name": "religious_rule", + "type": "religious_rule", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "allergens": { + "name": "allergens", + "type": "allergen[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "intolerances": { + "name": "intolerances", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "avoid_ingredient_ids": { + "name": "avoid_ingredient_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "favorite_cuisines": { + "name": "favorite_cuisines", + "type": "cuisine[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "disliked_dishes": { + "name": "disliked_dishes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "spice_level_max": { + "name": "spice_level_max", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "weekly_budget_minor": { + "name": "weekly_budget_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_cooking_minutes_weekday": { + "name": "max_cooking_minutes_weekday", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "equipment": { + "name": "equipment", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "default_portions": { + "name": "default_portions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sv-SE'" + }, + "precision_mode": { + "name": "precision_mode", + "type": "precision_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'simple'" + }, + "onboarding_completed": { + "name": "onboarding_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_locale_preferences": { + "name": "user_locale_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "language_tag": { + "name": "language_tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sv-SE'" + }, + "region_code": { + "name": "region_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'SE'" + }, + "time_zone": { + "name": "time_zone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Europe/Stockholm'" + }, + "measurement_system": { + "name": "measurement_system", + "type": "measurement_system", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'METRIC'" + }, + "temperature_unit": { + "name": "temperature_unit", + "type": "temperature_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CELSIUS'" + }, + "currency_code": { + "name": "currency_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'SEK'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "use_24_hour_time": { + "name": "use_24_hour_time", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_locale_preferences_user_id_users_id_fk": { + "name": "user_locale_preferences_user_id_users_id_fk", + "tableFrom": "user_locale_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.household_members": { + "name": "household_members", + "schema": "", + "columns": { + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "household_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "portion_factor": { + "name": "portion_factor", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "household_members_user_idx": { + "name": "household_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "household_members_household_id_households_id_fk": { + "name": "household_members_household_id_households_id_fk", + "tableFrom": "household_members", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "household_members_user_id_users_id_fk": { + "name": "household_members_user_id_users_id_fk", + "tableFrom": "household_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "household_members_household_id_user_id_pk": { + "name": "household_members_household_id_user_id_pk", + "columns": [ + "household_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.households": { + "name": "households", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invite_code": { + "name": "invite_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weekly_budget_minor": { + "name": "weekly_budget_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency_code": { + "name": "currency_code", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'SEK'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "households_invite_code_unique": { + "name": "households_invite_code_unique", + "columns": [ + { + "expression": "invite_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_locations": { + "name": "storage_locations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "storage_location_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sublocations": { + "name": "sublocations", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "storage_locations_household_idx": { + "name": "storage_locations_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "storage_locations_household_id_households_id_fk": { + "name": "storage_locations_household_id_households_id_fk", + "tableFrom": "storage_locations", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.canonical_ingredients": { + "name": "canonical_ingredients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name_sv": { + "name": "name_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_unit": { + "name": "default_unit", + "type": "unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "density_g_per_ml": { + "name": "density_g_per_ml", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "grams_per_piece": { + "name": "grams_per_piece", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "allergens": { + "name": "allergens", + "type": "allergen[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_vegan": { + "name": "is_vegan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_vegetarian": { + "name": "is_vegetarian", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "contains_gluten": { + "name": "contains_gluten", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "contains_lactose": { + "name": "contains_lactose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_pork": { + "name": "is_pork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_beef": { + "name": "is_beef", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_alcohol": { + "name": "is_alcohol", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "nutrition_per_100": { + "name": "nutrition_per_100", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "nutrition_provenance": { + "name": "nutrition_provenance", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "peak_seasons": { + "name": "peak_seasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "shelf_life_guidance": { + "name": "shelf_life_guidance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "default_price_minor_per_kg": { + "name": "default_price_minor_per_kg", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "canonical_ingredients_category_idx": { + "name": "canonical_ingredients_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.substitutions": { + "name": "substitutions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "from_ingredient_id": { + "name": "from_ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_ingredient_id": { + "name": "to_ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ratio": { + "name": "ratio", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "instructions_sv": { + "name": "instructions_sv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "best_for": { + "name": "best_for", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "not_recommended_for": { + "name": "not_recommended_for", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "flavor_impact_sv": { + "name": "flavor_impact_sv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "texture_impact_sv": { + "name": "texture_impact_sv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "substitutions_from_idx": { + "name": "substitutions_from_idx", + "columns": [ + { + "expression": "from_ingredient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "substitutions_pair_unique": { + "name": "substitutions_pair_unique", + "columns": [ + { + "expression": "from_ingredient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_ingredient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "substitutions_from_ingredient_id_canonical_ingredients_id_fk": { + "name": "substitutions_from_ingredient_id_canonical_ingredients_id_fk", + "tableFrom": "substitutions", + "tableTo": "canonical_ingredients", + "columnsFrom": [ + "from_ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "substitutions_to_ingredient_id_canonical_ingredients_id_fk": { + "name": "substitutions_to_ingredient_id_canonical_ingredients_id_fk", + "tableFrom": "substitutions", + "tableTo": "canonical_ingredients", + "columnsFrom": [ + "to_ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "gtin": { + "name": "gtin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_ingredient_id": { + "name": "canonical_ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_size_value": { + "name": "package_size_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "package_size_unit": { + "name": "package_size_unit", + "type": "unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "ingredients_text": { + "name": "ingredients_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allergens": { + "name": "allergens", + "type": "allergen[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "may_contain_allergens": { + "name": "may_contain_allergens", + "type": "allergen[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "nutrition": { + "name": "nutrition", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "image_urls": { + "name": "image_urls", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sv'" + }, + "market": { + "name": "market", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'SE'" + }, + "data_source": { + "name": "data_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_status": { + "name": "verification_status", + "type": "verification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unverified'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "valid_from": { + "name": "valid_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "valid_to": { + "name": "valid_to", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "products_gtin_version_unique": { + "name": "products_gtin_version_unique", + "columns": [ + { + "expression": "gtin", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"products\".\"gtin\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "products_gtin_current_idx": { + "name": "products_gtin_current_idx", + "columns": [ + { + "expression": "gtin", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"products\".\"valid_to\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "products_canonical_idx": { + "name": "products_canonical_idx", + "columns": [ + { + "expression": "canonical_ingredient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "products_canonical_ingredient_id_canonical_ingredients_id_fk": { + "name": "products_canonical_ingredient_id_canonical_ingredients_id_fk", + "tableFrom": "products", + "tableTo": "canonical_ingredients", + "columnsFrom": [ + "canonical_ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventory_items": { + "name": "inventory_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "canonical_ingredient_id": { + "name": "canonical_ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unit": { + "name": "unit", + "type": "unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_location_id": { + "name": "storage_location_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sublocation": { + "name": "sublocation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purchased_at": { + "name": "purchased_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "opened_at": { + "name": "opened_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "best_before_date": { + "name": "best_before_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "use_by_date": { + "name": "use_by_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "date_kind": { + "name": "date_kind", + "type": "date_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "frozen_at": { + "name": "frozen_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "thawed_at": { + "name": "thawed_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "price_minor": { + "name": "price_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "nutrition_per_100": { + "name": "nutrition_per_100", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "inventory_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "verified_by_user": { + "name": "verified_by_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_verified_at": { + "name": "last_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expiry_status": { + "name": "expiry_status", + "type": "expiry_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "depleted_at": { + "name": "depleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inventory_items_household_idx": { + "name": "inventory_items_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inventory_items_household_bb_idx": { + "name": "inventory_items_household_bb_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "best_before_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inventory_items_location_idx": { + "name": "inventory_items_location_idx", + "columns": [ + { + "expression": "storage_location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inventory_items_canonical_idx": { + "name": "inventory_items_canonical_idx", + "columns": [ + { + "expression": "canonical_ingredient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inventory_items_household_id_households_id_fk": { + "name": "inventory_items_household_id_households_id_fk", + "tableFrom": "inventory_items", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inventory_items_canonical_ingredient_id_canonical_ingredients_id_fk": { + "name": "inventory_items_canonical_ingredient_id_canonical_ingredients_id_fk", + "tableFrom": "inventory_items", + "tableTo": "canonical_ingredients", + "columnsFrom": [ + "canonical_ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventory_items_product_id_products_id_fk": { + "name": "inventory_items_product_id_products_id_fk", + "tableFrom": "inventory_items", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventory_items_storage_location_id_storage_locations_id_fk": { + "name": "inventory_items_storage_location_id_storage_locations_id_fk", + "tableFrom": "inventory_items", + "tableTo": "storage_locations", + "columnsFrom": [ + "storage_location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventory_transactions": { + "name": "inventory_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "inventory_item_id": { + "name": "inventory_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "inventory_transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "quantity_delta": { + "name": "quantity_delta", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "ref_type": { + "name": "ref_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ref_id": { + "name": "ref_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value_minor": { + "name": "value_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inventory_tx_item_idx": { + "name": "inventory_tx_item_idx", + "columns": [ + { + "expression": "inventory_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inventory_tx_household_time_idx": { + "name": "inventory_tx_household_time_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inventory_tx_type_idx": { + "name": "inventory_tx_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inventory_transactions_household_id_households_id_fk": { + "name": "inventory_transactions_household_id_households_id_fk", + "tableFrom": "inventory_transactions", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inventory_transactions_inventory_item_id_inventory_items_id_fk": { + "name": "inventory_transactions_inventory_item_id_inventory_items_id_fk", + "tableFrom": "inventory_transactions", + "tableTo": "inventory_items", + "columnsFrom": [ + "inventory_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inventory_transactions_actor_user_id_users_id_fk": { + "name": "inventory_transactions_actor_user_id_users_id_fk", + "tableFrom": "inventory_transactions", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_cooks": { + "name": "recipe_cooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "portions_cooked": { + "name": "portions_cooked", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cooked_at": { + "name": "cooked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipe_cooks_recipe_idx": { + "name": "recipe_cooks_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipe_cooks_user_idx": { + "name": "recipe_cooks_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_cooks_recipe_id_recipes_id_fk": { + "name": "recipe_cooks_recipe_id_recipes_id_fk", + "tableFrom": "recipe_cooks", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_cooks_user_id_users_id_fk": { + "name": "recipe_cooks_user_id_users_id_fk", + "tableFrom": "recipe_cooks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_favorites": { + "name": "recipe_favorites", + "schema": "", + "columns": { + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipe_favorites_user_idx": { + "name": "recipe_favorites_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_favorites_recipe_id_recipes_id_fk": { + "name": "recipe_favorites_recipe_id_recipes_id_fk", + "tableFrom": "recipe_favorites", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_favorites_user_id_users_id_fk": { + "name": "recipe_favorites_user_id_users_id_fk", + "tableFrom": "recipe_favorites", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recipe_favorites_recipe_id_user_id_pk": { + "name": "recipe_favorites_recipe_id_user_id_pk", + "columns": [ + "recipe_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_ingredients": { + "name": "recipe_ingredients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "canonical_ingredient_id": { + "name": "canonical_ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_sv": { + "name": "display_name_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "optional": { + "name": "optional", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "recipe_ingredients_recipe_idx": { + "name": "recipe_ingredients_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipe_ingredients_canonical_idx": { + "name": "recipe_ingredients_canonical_idx", + "columns": [ + { + "expression": "canonical_ingredient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_ingredients_recipe_id_recipes_id_fk": { + "name": "recipe_ingredients_recipe_id_recipes_id_fk", + "tableFrom": "recipe_ingredients", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_ingredients_canonical_ingredient_id_canonical_ingredients_id_fk": { + "name": "recipe_ingredients_canonical_ingredient_id_canonical_ingredients_id_fk", + "tableFrom": "recipe_ingredients", + "tableTo": "canonical_ingredients", + "columnsFrom": [ + "canonical_ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_ratings": { + "name": "recipe_ratings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "feedback_tags": { + "name": "feedback_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipe_ratings_user_recipe_unique": { + "name": "recipe_ratings_user_recipe_unique", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipe_ratings_recipe_idx": { + "name": "recipe_ratings_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_ratings_recipe_id_recipes_id_fk": { + "name": "recipe_ratings_recipe_id_recipes_id_fk", + "tableFrom": "recipe_ratings", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_ratings_user_id_users_id_fk": { + "name": "recipe_ratings_user_id_users_id_fk", + "tableFrom": "recipe_ratings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_similarities": { + "name": "recipe_similarities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "recipe_a_id": { + "name": "recipe_a_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipe_b_id": { + "name": "recipe_b_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "similarity_score": { + "name": "similarity_score", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "recipe_similarity_class", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipe_similarities_pair_unique": { + "name": "recipe_similarities_pair_unique", + "columns": [ + { + "expression": "recipe_a_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipe_b_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_similarities_recipe_a_id_recipes_id_fk": { + "name": "recipe_similarities_recipe_a_id_recipes_id_fk", + "tableFrom": "recipe_similarities", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_a_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_similarities_recipe_b_id_recipes_id_fk": { + "name": "recipe_similarities_recipe_b_id_recipes_id_fk", + "tableFrom": "recipe_similarities", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_b_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_source_registry": { + "name": "recipe_source_registry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_name": { + "name": "source_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "license": { + "name": "license", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "right_to_store": { + "name": "right_to_store", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "right_to_modify": { + "name": "right_to_modify", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "right_to_display": { + "name": "right_to_display", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "attribution_required": { + "name": "attribution_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attribution_text": { + "name": "attribution_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commercial_use": { + "name": "commercial_use", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "valid_from": { + "name": "valid_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "valid_to": { + "name": "valid_to", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_steps": { + "name": "recipe_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "instruction_sv": { + "name": "instruction_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timer_seconds": { + "name": "timer_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "temperature_c": { + "name": "temperature_c", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tip": { + "name": "tip", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "recipe_steps_recipe_idx": { + "name": "recipe_steps_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipe_steps_recipe_step_unique": { + "name": "recipe_steps_recipe_step_unique", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_steps_recipe_id_recipes_id_fk": { + "name": "recipe_steps_recipe_id_recipes_id_fk", + "tableFrom": "recipe_steps", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipes": { + "name": "recipes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_sv": { + "name": "title_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_sv": { + "name": "description_sv", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cuisine": { + "name": "cuisine", + "type": "cuisine", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "meal_types": { + "name": "meal_types", + "type": "meal_type[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "methods": { + "name": "methods", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "equipment": { + "name": "equipment", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "difficulty": { + "name": "difficulty", + "type": "recipe_difficulty", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'easy'" + }, + "prep_time_minutes": { + "name": "prep_time_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cook_time_minutes": { + "name": "cook_time_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_time_minutes": { + "name": "total_time_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "portions": { + "name": "portions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 4 + }, + "nutrition_per_portion": { + "name": "nutrition_per_portion", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "allergens": { + "name": "allergens", + "type": "allergen[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "spice_level": { + "name": "spice_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "estimated_cost_minor_per_portion": { + "name": "estimated_cost_minor_per_portion", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "storage_guidance_sv": { + "name": "storage_guidance_sv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meal_prep_friendly": { + "name": "meal_prep_friendly", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "freezer_friendly": { + "name": "freezer_friendly", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "peak_seasons": { + "name": "peak_seasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "holiday_tags": { + "name": "holiday_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "dna": { + "name": "dna", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "variant_type": { + "name": "variant_type", + "type": "recipe_variant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "variant_of_recipe_id": { + "name": "variant_of_recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "forked_from_recipe_id": { + "name": "forked_from_recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "recipe_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "verification_status": { + "name": "verification_status", + "type": "recipe_verification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unverified'" + }, + "source_type": { + "name": "source_type", + "type": "recipe_source_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_registry_id": { + "name": "source_registry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "creator_user_id": { + "name": "creator_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "creator_display_name": { + "name": "creator_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_urls": { + "name": "image_urls", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "rating_average": { + "name": "rating_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "rating_count": { + "name": "rating_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cook_count": { + "name": "cook_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "favorite_count": { + "name": "favorite_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "moderation_note": { + "name": "moderation_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipes_slug_unique": { + "name": "recipes_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipes_status_idx": { + "name": "recipes_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipes_cuisine_idx": { + "name": "recipes_cuisine_idx", + "columns": [ + { + "expression": "cuisine", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipes_variant_of_idx": { + "name": "recipes_variant_of_idx", + "columns": [ + { + "expression": "variant_of_recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipes_creator_idx": { + "name": "recipes_creator_idx", + "columns": [ + { + "expression": "creator_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipes_total_time_idx": { + "name": "recipes_total_time_idx", + "columns": [ + { + "expression": "total_time_minutes", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipes_source_registry_id_recipe_source_registry_id_fk": { + "name": "recipes_source_registry_id_recipe_source_registry_id_fk", + "tableFrom": "recipes", + "tableTo": "recipe_source_registry", + "columnsFrom": [ + "source_registry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "recipes_creator_user_id_users_id_fk": { + "name": "recipes_creator_user_id_users_id_fk", + "tableFrom": "recipes", + "tableTo": "users", + "columnsFrom": [ + "creator_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ingredient_translations": { + "name": "ingredient_translations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ingredient_id": { + "name": "ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language_tag": { + "name": "language_tag", + "type": "varchar(35)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "source": { + "name": "source", + "type": "translation_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'seed'" + }, + "status": { + "name": "status", + "type": "translation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'published'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ingredient_translations_unique": { + "name": "ingredient_translations_unique", + "columns": [ + { + "expression": "ingredient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "language_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ingredient_translations_lang_idx": { + "name": "ingredient_translations_lang_idx", + "columns": [ + { + "expression": "language_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ingredient_translations_ingredient_id_canonical_ingredients_id_fk": { + "name": "ingredient_translations_ingredient_id_canonical_ingredients_id_fk", + "tableFrom": "ingredient_translations", + "tableTo": "canonical_ingredients", + "columnsFrom": [ + "ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_step_translations": { + "name": "recipe_step_translations", + "schema": "", + "columns": { + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "language_tag": { + "name": "language_tag", + "type": "varchar(35)", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "instruction": { + "name": "instruction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tip": { + "name": "tip", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "recipe_step_translations_recipe_id_recipes_id_fk": { + "name": "recipe_step_translations_recipe_id_recipes_id_fk", + "tableFrom": "recipe_step_translations", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recipe_step_translations_recipe_id_language_tag_step_number_pk": { + "name": "recipe_step_translations_recipe_id_language_tag_step_number_pk", + "columns": [ + "recipe_id", + "language_tag", + "step_number" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_translations": { + "name": "recipe_translations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "language_tag": { + "name": "language_tag", + "type": "varchar(35)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_guidance": { + "name": "storage_guidance", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "translation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft_ai'" + }, + "source": { + "name": "source", + "type": "translation_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ai'" + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipe_translations_unique": { + "name": "recipe_translations_unique", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "language_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipe_translations_lang_idx": { + "name": "recipe_translations_lang_idx", + "columns": [ + { + "expression": "language_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_translations_recipe_id_recipes_id_fk": { + "name": "recipe_translations_recipe_id_recipes_id_fk", + "tableFrom": "recipe_translations", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.unit_translations": { + "name": "unit_translations", + "schema": "", + "columns": { + "unit_code": { + "name": "unit_code", + "type": "unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "language_tag": { + "name": "language_tag", + "type": "varchar(35)", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "unit_translations_unit_code_language_tag_pk": { + "name": "unit_translations_unit_code_language_tag_pk", + "columns": [ + "unit_code", + "language_tag" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.allergen_market_rules": { + "name": "allergen_market_rules", + "schema": "", + "columns": { + "region_code": { + "name": "region_code", + "type": "varchar(2)", + "primaryKey": false, + "notNull": true + }, + "allergen": { + "name": "allergen", + "type": "allergen", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "must_highlight": { + "name": "must_highlight", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "allergen_market_rules_region_code_allergen_pk": { + "name": "allergen_market_rules_region_code_allergen_pk", + "columns": [ + "region_code", + "allergen" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nutrition_display_profiles": { + "name": "nutrition_display_profiles", + "schema": "", + "columns": { + "region_code": { + "name": "region_code", + "type": "varchar(2)", + "primaryKey": true, + "notNull": true + }, + "energy_display": { + "name": "energy_display", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "salt_display": { + "name": "salt_display", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_label_key": { + "name": "energy_label_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meal_boxes": { + "name": "meal_boxes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title_sv": { + "name": "title_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portions": { + "name": "portions", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "portions_remaining": { + "name": "portions_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "nutrition_per_portion": { + "name": "nutrition_per_portion", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cooked_at": { + "name": "cooked_at", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "storage_location_id": { + "name": "storage_location_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "frozen": { + "name": "frozen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "recommended_use_by": { + "name": "recommended_use_by", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "reserved_for_user_id": { + "name": "reserved_for_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "meal_boxes_household_idx": { + "name": "meal_boxes_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "meal_boxes_use_by_idx": { + "name": "meal_boxes_use_by_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recommended_use_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "meal_boxes_household_id_households_id_fk": { + "name": "meal_boxes_household_id_households_id_fk", + "tableFrom": "meal_boxes", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meal_boxes_recipe_id_recipes_id_fk": { + "name": "meal_boxes_recipe_id_recipes_id_fk", + "tableFrom": "meal_boxes", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "meal_boxes_storage_location_id_storage_locations_id_fk": { + "name": "meal_boxes_storage_location_id_storage_locations_id_fk", + "tableFrom": "meal_boxes", + "tableTo": "storage_locations", + "columnsFrom": [ + "storage_location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "meal_boxes_reserved_for_user_id_users_id_fk": { + "name": "meal_boxes_reserved_for_user_id_users_id_fk", + "tableFrom": "meal_boxes", + "tableTo": "users", + "columnsFrom": [ + "reserved_for_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meals": { + "name": "meals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "meal_type": { + "name": "meal_type", + "type": "meal_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "meal_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title_sv": { + "name": "title_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portion_fraction": { + "name": "portion_fraction", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "nutrition": { + "name": "nutrition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "nutrition_is_estimate": { + "name": "nutrition_is_estimate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "estimate_min_kcal": { + "name": "estimate_min_kcal", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "estimate_max_kcal": { + "name": "estimate_max_kcal", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "items": { + "name": "items", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scan_job_id": { + "name": "scan_job_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "logged_at": { + "name": "logged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "meals_user_date_idx": { + "name": "meals_user_date_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "meals_household_idx": { + "name": "meals_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "meals_user_id_users_id_fk": { + "name": "meals_user_id_users_id_fk", + "tableFrom": "meals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meals_household_id_households_id_fk": { + "name": "meals_household_id_households_id_fk", + "tableFrom": "meals", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "meals_recipe_id_recipes_id_fk": { + "name": "meals_recipe_id_recipes_id_fk", + "tableFrom": "meals", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shopping_list_items": { + "name": "shopping_list_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "shopping_list_id": { + "name": "shopping_list_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "canonical_ingredient_id": { + "name": "canonical_ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "unit": { + "name": "unit", + "type": "unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'COUNT'" + }, + "store_section": { + "name": "store_section", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'hygien_ovrigt'" + }, + "suggested_package_size": { + "name": "suggested_package_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimated_price_minor": { + "name": "estimated_price_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checked": { + "name": "checked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "shopping_list_items_list_idx": { + "name": "shopping_list_items_list_idx", + "columns": [ + { + "expression": "shopping_list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shopping_list_items_shopping_list_id_shopping_lists_id_fk": { + "name": "shopping_list_items_shopping_list_id_shopping_lists_id_fk", + "tableFrom": "shopping_list_items", + "tableTo": "shopping_lists", + "columnsFrom": [ + "shopping_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shopping_list_items_canonical_ingredient_id_canonical_ingredients_id_fk": { + "name": "shopping_list_items_canonical_ingredient_id_canonical_ingredients_id_fk", + "tableFrom": "shopping_list_items", + "tableTo": "canonical_ingredients", + "columnsFrom": [ + "canonical_ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shopping_list_items_added_by_user_id_users_id_fk": { + "name": "shopping_list_items_added_by_user_id_users_id_fk", + "tableFrom": "shopping_list_items", + "tableTo": "users", + "columnsFrom": [ + "added_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shopping_lists": { + "name": "shopping_lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Inköpslista'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "week_plan_id": { + "name": "week_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "shopping_lists_household_idx": { + "name": "shopping_lists_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shopping_lists_household_id_households_id_fk": { + "name": "shopping_lists_household_id_households_id_fk", + "tableFrom": "shopping_lists", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shopping_lists_week_plan_id_week_plans_id_fk": { + "name": "shopping_lists_week_plan_id_week_plans_id_fk", + "tableFrom": "shopping_lists", + "tableTo": "week_plans", + "columnsFrom": [ + "week_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.week_plan_entries": { + "name": "week_plan_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "week_plan_id": { + "name": "week_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "meal_type": { + "name": "meal_type", + "type": "meal_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "meal_box_id": { + "name": "meal_box_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title_sv": { + "name": "title_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portions": { + "name": "portions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "reschedule_reason_sv": { + "name": "reschedule_reason_sv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "week_plan_entries_plan_idx": { + "name": "week_plan_entries_plan_idx", + "columns": [ + { + "expression": "week_plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "week_plan_entries_week_plan_id_week_plans_id_fk": { + "name": "week_plan_entries_week_plan_id_week_plans_id_fk", + "tableFrom": "week_plan_entries", + "tableTo": "week_plans", + "columnsFrom": [ + "week_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "week_plan_entries_recipe_id_recipes_id_fk": { + "name": "week_plan_entries_recipe_id_recipes_id_fk", + "tableFrom": "week_plan_entries", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "week_plan_entries_meal_box_id_meal_boxes_id_fk": { + "name": "week_plan_entries_meal_box_id_meal_boxes_id_fk", + "tableFrom": "week_plan_entries", + "tableTo": "meal_boxes", + "columnsFrom": [ + "meal_box_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.week_plans": { + "name": "week_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "week_start_date": { + "name": "week_start_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "generated_by": { + "name": "generated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "week_plans_household_week_idx": { + "name": "week_plans_household_week_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_start_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "week_plans_household_id_households_id_fk": { + "name": "week_plans_household_id_households_id_fk", + "tableFrom": "week_plans", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.price_observations": { + "name": "price_observations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "canonical_ingredient_id": { + "name": "canonical_ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "store_name": { + "name": "store_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price_minor": { + "name": "price_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "price_per_kg_minor": { + "name": "price_per_kg_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'receipt'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "price_observations_ingredient_idx": { + "name": "price_observations_ingredient_idx", + "columns": [ + { + "expression": "canonical_ingredient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "price_observations_canonical_ingredient_id_canonical_ingredients_id_fk": { + "name": "price_observations_canonical_ingredient_id_canonical_ingredients_id_fk", + "tableFrom": "price_observations", + "tableTo": "canonical_ingredients", + "columnsFrom": [ + "canonical_ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "price_observations_product_id_products_id_fk": { + "name": "price_observations_product_id_products_id_fk", + "tableFrom": "price_observations", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.receipt_lines": { + "name": "receipt_lines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "receipt_id": { + "name": "receipt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "raw_text": { + "name": "raw_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_name": { + "name": "normalized_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_ingredient_id": { + "name": "canonical_ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "unit_price_minor": { + "name": "unit_price_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_price_minor": { + "name": "total_price_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "verified_by_user": { + "name": "verified_by_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "added_to_inventory": { + "name": "added_to_inventory", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "receipt_lines_receipt_idx": { + "name": "receipt_lines_receipt_idx", + "columns": [ + { + "expression": "receipt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "receipt_lines_receipt_id_receipts_id_fk": { + "name": "receipt_lines_receipt_id_receipts_id_fk", + "tableFrom": "receipt_lines", + "tableTo": "receipts", + "columnsFrom": [ + "receipt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "receipt_lines_canonical_ingredient_id_canonical_ingredients_id_fk": { + "name": "receipt_lines_canonical_ingredient_id_canonical_ingredients_id_fk", + "tableFrom": "receipt_lines", + "tableTo": "canonical_ingredients", + "columnsFrom": [ + "canonical_ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "receipt_lines_product_id_products_id_fk": { + "name": "receipt_lines_product_id_products_id_fk", + "tableFrom": "receipt_lines", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.receipts": { + "name": "receipts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "store_name": { + "name": "store_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purchase_date": { + "name": "purchase_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "total_minor": { + "name": "total_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "discount_minor": { + "name": "discount_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scan_job_id": { + "name": "scan_job_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "receipts_household_idx": { + "name": "receipts_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchase_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "receipts_household_id_households_id_fk": { + "name": "receipts_household_id_households_id_fk", + "tableFrom": "receipts", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_corrections": { + "name": "ai_corrections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scan_job_id": { + "name": "scan_job_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_type": { + "name": "task_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_output": { + "name": "ai_output", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "user_correction": { + "name": "user_correction", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_snapshot": { + "name": "consent_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "exported_to_training": { + "name": "exported_to_training", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_corrections_task_idx": { + "name": "ai_corrections_task_idx", + "columns": [ + { + "expression": "task_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_corrections_scan_job_id_scan_jobs_id_fk": { + "name": "ai_corrections_scan_job_id_scan_jobs_id_fk", + "tableFrom": "ai_corrections", + "tableTo": "scan_jobs", + "columnsFrom": [ + "scan_job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ai_corrections_user_id_users_id_fk": { + "name": "ai_corrections_user_id_users_id_fk", + "tableFrom": "ai_corrections", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scan_jobs": { + "name": "scan_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scan_type": { + "name": "scan_type", + "type": "scan_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "job_type": { + "name": "job_type", + "type": "job_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "s3_keys": { + "name": "s3_keys", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cost_usd": { + "name": "cost_usd", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scan_jobs_user_idx": { + "name": "scan_jobs_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scan_jobs_status_idx": { + "name": "scan_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scan_jobs_user_id_users_id_fk": { + "name": "scan_jobs_user_id_users_id_fk", + "tableFrom": "scan_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scan_jobs_household_id_households_id_fk": { + "name": "scan_jobs_household_id_households_id_fk", + "tableFrom": "scan_jobs", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.food_memories": { + "name": "food_memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "recipe_id": { + "name": "recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title_sv": { + "name": "title_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary_sv": { + "name": "summary_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "holiday_tag": { + "name": "holiday_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stars": { + "name": "stars", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "food_memories_household_idx": { + "name": "food_memories_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "food_memories_holiday_idx": { + "name": "food_memories_holiday_idx", + "columns": [ + { + "expression": "holiday_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "food_memories_user_id_users_id_fk": { + "name": "food_memories_user_id_users_id_fk", + "tableFrom": "food_memories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "food_memories_household_id_households_id_fk": { + "name": "food_memories_household_id_households_id_fk", + "tableFrom": "food_memories", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "food_memories_recipe_id_recipes_id_fk": { + "name": "food_memories_recipe_id_recipes_id_fk", + "tableFrom": "food_memories", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_items": { + "name": "memory_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "memory_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary_sv": { + "name": "summary_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "signal_origin", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0.5 + }, + "verified_by_user": { + "name": "verified_by_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "paused": { + "name": "paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memory_items_user_idx": { + "name": "memory_items_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_items_household_idx": { + "name": "memory_items_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_items_key_idx": { + "name": "memory_items_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_items_user_id_users_id_fk": { + "name": "memory_items_user_id_users_id_fk", + "tableFrom": "memory_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_items_household_id_households_id_fk": { + "name": "memory_items_household_id_households_id_fk", + "tableFrom": "memory_items", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.taste_signals": { + "name": "taste_signals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "axis": { + "name": "axis", + "type": "taste_axis", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "strength": { + "name": "strength", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0.5 + }, + "origin": { + "name": "origin", + "type": "signal_origin", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "ref_recipe_id": { + "name": "ref_recipe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "taste_signals_user_idx": { + "name": "taste_signals_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "axis", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "taste_signals_user_id_users_id_fk": { + "name": "taste_signals_user_id_users_id_fk", + "tableFrom": "taste_signals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "taste_signals_ref_recipe_id_recipes_id_fk": { + "name": "taste_signals_ref_recipe_id_recipes_id_fk", + "tableFrom": "taste_signals", + "tableTo": "recipes", + "columnsFrom": [ + "ref_recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_events": { + "name": "season_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_sv": { + "name": "name_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "market": { + "name": "market", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'SE'" + }, + "date_rule": { + "name": "date_rule", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "lead_days": { + "name": "lead_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 7 + }, + "food_tags": { + "name": "food_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "recipe_slugs": { + "name": "recipe_slugs", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "season_events_slug_market_unique": { + "name": "season_events_slug_market_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "market", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_counters": { + "name": "ai_usage_counters", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "month": { + "name": "month", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_scans": { + "name": "ai_scans", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ai_tokens_in": { + "name": "ai_tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ai_tokens_out": { + "name": "ai_tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_usage_user_month_unique": { + "name": "ai_usage_user_month_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_usage_counters_user_id_users_id_fk": { + "name": "ai_usage_counters_user_id_users_id_fk", + "tableFrom": "ai_usage_counters", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.store_notifications": { + "name": "store_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "subscription_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_payload": { + "name": "raw_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "signature_verified": { + "name": "signature_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "store_notifications_pending_idx": { + "name": "store_notifications_pending_idx", + "columns": [ + { + "expression": "processed", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.store_transactions": { + "name": "store_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "subscription_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "transaction_id": { + "name": "transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_transaction_id": { + "name": "original_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_payload": { + "name": "raw_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "store_transactions_unique": { + "name": "store_transactions_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "store_transactions_user_id_users_id_fk": { + "name": "store_transactions_user_id_users_id_fk", + "tableFrom": "store_transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription_events": { + "name": "subscription_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subscription_events_sub_idx": { + "name": "subscription_events_sub_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_events_subscription_id_subscriptions_id_fk": { + "name": "subscription_events_subscription_id_subscriptions_id_fk", + "tableFrom": "subscription_events", + "tableTo": "subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "subscription_events_user_id_users_id_fk": { + "name": "subscription_events_user_id_users_id_fk", + "tableFrom": "subscription_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "subscription_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "subscription_plan", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "original_transaction_id": { + "name": "original_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "purchased_at": { + "name": "purchased_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "grace_period_expires_at": { + "name": "grace_period_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_verified_at": { + "name": "last_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subscriptions_user_idx": { + "name": "subscriptions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscriptions_original_tx_unique": { + "name": "subscriptions_original_tx_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "original_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"subscriptions\".\"original_transaction_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscriptions_user_id_users_id_fk": { + "name": "subscriptions_user_id_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "subscriptions_household_id_households_id_fk": { + "name": "subscriptions_household_id_households_id_fk", + "tableFrom": "subscriptions", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trials": { + "name": "trials", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "trials_user_id_users_id_fk": { + "name": "trials_user_id_users_id_fk", + "tableFrom": "trials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.analytics_daily_snapshots": { + "name": "analytics_daily_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "date": { + "name": "date", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_name": { + "name": "event_name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unique_sessions": { + "name": "unique_sessions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "properties_fingerprint": { + "name": "properties_fingerprint", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pa_daily_household_date_idx": { + "name": "pa_daily_household_date_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pa_daily_date_event_idx": { + "name": "pa_daily_date_event_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "analytics_daily_snapshots_household_id_households_id_fk": { + "name": "analytics_daily_snapshots_household_id_households_id_fk", + "tableFrom": "analytics_daily_snapshots", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.product_analytics_events": { + "name": "product_analytics_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "event_name": { + "name": "event_name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "anonymous_id": { + "name": "anonymous_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "app_version": { + "name": "app_version", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "experiment_variant": { + "name": "experiment_variant", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "pa_events_name_occurred_idx": { + "name": "pa_events_name_occurred_idx", + "columns": [ + { + "expression": "event_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pa_events_household_occurred_idx": { + "name": "pa_events_household_occurred_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pa_events_user_occurred_idx": { + "name": "pa_events_user_occurred_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pa_events_session_idx": { + "name": "pa_events_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pa_events_received_idx": { + "name": "pa_events_received_idx", + "columns": [ + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "product_analytics_events_user_id_users_id_fk": { + "name": "product_analytics_events_user_id_users_id_fk", + "tableFrom": "product_analytics_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "product_analytics_events_household_id_households_id_fk": { + "name": "product_analytics_events_household_id_households_id_fk", + "tableFrom": "product_analytics_events", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_eval_cases": { + "name": "ai_eval_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_type": { + "name": "task_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_sv": { + "name": "name_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_ref": { + "name": "input_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expected_output": { + "name": "expected_output", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_eval_runs": { + "name": "ai_eval_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_type": { + "name": "task_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "passed": { + "name": "passed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_eval_runs_task_idx": { + "name": "ai_eval_runs_task_idx", + "columns": [ + { + "expression": "task_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_logs_actor_idx": { + "name": "audit_logs_actor_idx", + "columns": [ + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_action_idx": { + "name": "audit_logs_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.creator_follows": { + "name": "creator_follows", + "schema": "", + "columns": { + "follower_user_id": { + "name": "follower_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "creator_user_id": { + "name": "creator_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "creator_follows_follower_user_id_users_id_fk": { + "name": "creator_follows_follower_user_id_users_id_fk", + "tableFrom": "creator_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "creator_follows_creator_user_id_users_id_fk": { + "name": "creator_follows_creator_user_id_users_id_fk", + "tableFrom": "creator_follows", + "tableTo": "users", + "columnsFrom": [ + "creator_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "creator_follows_follower_user_id_creator_user_id_pk": { + "name": "creator_follows_follower_user_id_creator_user_id_pk", + "columns": [ + "follower_user_id", + "creator_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.creator_stats": { + "name": "creator_stats", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "profile_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "level": { + "name": "level", + "type": "creator_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'beginner'" + }, + "published_recipes": { + "name": "published_recipes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "followers": { + "name": "followers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cooks": { + "name": "total_cooks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_favorites": { + "name": "total_favorites", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "average_rating": { + "name": "average_rating", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "verified_recipes": { + "name": "verified_recipes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "badges": { + "name": "badges", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "creator_stats_user_id_users_id_fk": { + "name": "creator_stats_user_id_users_id_fk", + "tableFrom": "creator_stats", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain_events": { + "name": "domain_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "domain_events_unpublished_idx": { + "name": "domain_events_unpublished_idx", + "columns": [ + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_events_type_idx": { + "name": "domain_events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_events_household_idx": { + "name": "domain_events_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feature_flags": { + "name": "feature_flags", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "description_sv": { + "name": "description_sv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollout_percent": { + "name": "rollout_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_keys": { + "name": "idempotency_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "idempotency_keys_user_id_users_id_fk": { + "name": "idempotency_keys_user_id_users_id_fk", + "tableFrom": "idempotency_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "idempotency_keys_user_id_key_endpoint_pk": { + "name": "idempotency_keys_user_id_key_endpoint_pk", + "columns": [ + "user_id", + "key", + "endpoint" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title_sv": { + "name": "title_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_sv": { + "name": "body_sv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_tokens": { + "name": "push_tokens", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "push_tokens_user_id_users_id_fk": { + "name": "push_tokens_user_id_users_id_fk", + "tableFrom": "push_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "push_tokens_user_id_token_pk": { + "name": "push_tokens_user_id_token_pk", + "columns": [ + "user_id", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waste_summaries": { + "name": "waste_summaries", + "schema": "", + "columns": { + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "week_start_date": { + "name": "week_start_date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discarded_count": { + "name": "discarded_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discarded_value_minor": { + "name": "discarded_value_minor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "waste_summaries_household_id_households_id_fk": { + "name": "waste_summaries_household_id_households_id_fk", + "tableFrom": "waste_summaries", + "tableTo": "households", + "columnsFrom": [ + "household_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "waste_summaries_household_id_week_start_date_pk": { + "name": "waste_summaries_household_id_week_start_date_pk", + "columns": [ + "household_id", + "week_start_date" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.activity_level": { + "name": "activity_level", + "schema": "public", + "values": [ + "sedentary", + "light", + "moderate", + "active", + "very_active" + ] + }, + "public.allergen": { + "name": "allergen", + "schema": "public", + "values": [ + "gluten", + "crustaceans", + "eggs", + "fish", + "peanuts", + "soy", + "milk", + "tree_nuts", + "celery", + "mustard", + "sesame", + "sulphites", + "lupin", + "molluscs" + ] + }, + "public.consent_kind": { + "name": "consent_kind", + "schema": "public", + "values": [ + "personalization", + "anonymized_improvement", + "image_training", + "health_integration", + "location_weather", + "push_notifications", + "product_analytics" + ] + }, + "public.consent_status": { + "name": "consent_status", + "schema": "public", + "values": [ + "granted", + "denied", + "revoked" + ] + }, + "public.cooking_method": { + "name": "cooking_method", + "schema": "public", + "values": [ + "stovetop", + "oven", + "grill", + "airfryer", + "wok", + "slow_cooker", + "sous_vide", + "microwave", + "no_cook", + "pressure_cooker", + "deep_fry", + "steam" + ] + }, + "public.creator_level": { + "name": "creator_level", + "schema": "public", + "values": [ + "beginner", + "sous_chef", + "chef", + "master_chef", + "legend" + ] + }, + "public.cuisine": { + "name": "cuisine", + "schema": "public", + "values": [ + "swedish", + "nordic", + "italian", + "french", + "spanish", + "greek", + "thai", + "chinese", + "japanese", + "korean", + "vietnamese", + "indian", + "mexican", + "american", + "turkish", + "lebanese", + "moroccan", + "middle_eastern", + "international" + ] + }, + "public.date_kind": { + "name": "date_kind", + "schema": "public", + "values": [ + "best_before", + "use_by" + ] + }, + "public.diet_pattern": { + "name": "diet_pattern", + "schema": "public", + "values": [ + "omnivore", + "flexitarian", + "pescatarian", + "vegetarian", + "vegan", + "low_carb", + "keto", + "high_protein", + "mediterranean" + ] + }, + "public.event_type": { + "name": "event_type", + "schema": "public", + "values": [ + "PRODUCT_ADDED", + "PRODUCT_UPDATED", + "PRODUCT_CONSUMED", + "PRODUCT_DISCARDED", + "RECIPE_COOKED", + "RECIPE_RATED", + "RECIPE_CREATED", + "RECIPE_FORKED", + "MEAL_LOGGED", + "MEAL_PHOTO_ANALYZED", + "HOUSEHOLD_MEMBER_ADDED", + "SUBSCRIPTION_STARTED", + "SUBSCRIPTION_CHANGED", + "AI_CORRECTED", + "MEMORY_UPDATED", + "SHOPPING_COMPLETED", + "WEEK_PLAN_UPDATED", + "MEAL_BOX_CREATED", + "MEAL_BOX_CONSUMED" + ] + }, + "public.expiry_status": { + "name": "expiry_status", + "schema": "public", + "values": [ + "fresh", + "use_soon", + "expiring", + "expired", + "unknown" + ] + }, + "public.goal_type": { + "name": "goal_type", + "schema": "public", + "values": [ + "lose_weight", + "gain_weight", + "build_muscle", + "maintain_weight", + "more_protein", + "less_fat", + "more_fiber", + "more_variety", + "less_waste", + "lower_cost", + "cook_more" + ] + }, + "public.household_role": { + "name": "household_role", + "schema": "public", + "values": [ + "owner", + "adult", + "member", + "child" + ] + }, + "public.inventory_source": { + "name": "inventory_source", + "schema": "public", + "values": [ + "fridge_photo", + "freezer_photo", + "pantry_photo", + "ingredient_photo", + "barcode", + "receipt", + "digital_receipt", + "label_photo", + "manual_search", + "free_text", + "voice", + "cooked_recipe", + "connector", + "seed" + ] + }, + "public.inventory_transaction_type": { + "name": "inventory_transaction_type", + "schema": "public", + "values": [ + "purchase", + "consume", + "discard", + "adjust", + "cook_use", + "leftover_created", + "leftover_consumed", + "correction", + "freeze", + "thaw", + "move" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "queued", + "running", + "awaiting_confirmation", + "completed", + "failed", + "canceled" + ] + }, + "public.job_type": { + "name": "job_type", + "schema": "public", + "values": [ + "ANALYZE_FRIDGE_IMAGE", + "ANALYZE_PANTRY_IMAGE", + "ANALYZE_MEAL_IMAGE", + "READ_RECEIPT", + "READ_NUTRITION_LABEL", + "READ_EXPIRY_DATE", + "NORMALIZE_PRODUCTS", + "DEDUPLICATE_INVENTORY", + "CALCULATE_NUTRITION", + "GENERATE_RECIPE_OPTIONS", + "RANK_RECIPES", + "UPDATE_USER_MEMORY", + "GENERATE_WEEK_PLAN", + "SEND_EXPIRY_NOTIFICATION", + "VERIFY_SUBSCRIPTION", + "PROCESS_STORE_NOTIFICATION", + "BUILD_TRAINING_SAMPLE", + "RUN_AI_EVALUATION" + ] + }, + "public.meal_log_source": { + "name": "meal_log_source", + "schema": "public", + "values": [ + "cooked_recipe", + "plate_photo", + "barcode", + "product", + "free_text", + "voice", + "previous_meal", + "meal_box", + "manual" + ] + }, + "public.meal_type": { + "name": "meal_type", + "schema": "public", + "values": [ + "breakfast", + "lunch", + "dinner", + "snack", + "dessert", + "starter", + "buffet", + "party" + ] + }, + "public.memory_kind": { + "name": "memory_kind", + "schema": "public", + "values": [ + "structured_fact", + "event", + "semantic", + "profile_summary", + "recipe_memory" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "expiry_warning", + "meal_box_reminder", + "quick_dinner_suggestion", + "holiday_upcoming", + "creator_new_recipe", + "week_plan_change", + "pantry_forecast", + "subscription_status" + ] + }, + "public.precision_mode": { + "name": "precision_mode", + "schema": "public", + "values": [ + "simple", + "exact" + ] + }, + "public.profile_visibility": { + "name": "profile_visibility", + "schema": "public", + "values": [ + "private", + "friends", + "national", + "global" + ] + }, + "public.recipe_difficulty": { + "name": "recipe_difficulty", + "schema": "public", + "values": [ + "beginner", + "easy", + "medium", + "advanced", + "expert" + ] + }, + "public.recipe_similarity_class": { + "name": "recipe_similarity_class", + "schema": "public", + "values": [ + "duplicate", + "variant", + "inspired", + "independent" + ] + }, + "public.recipe_source_type": { + "name": "recipe_source_type", + "schema": "public", + "values": [ + "own_editorial", + "ai_assisted_reviewed", + "open_license", + "public_domain", + "licensed_database", + "creator_agreement", + "user_generated" + ] + }, + "public.recipe_status": { + "name": "recipe_status", + "schema": "public", + "values": [ + "draft", + "submitted", + "ai_checked", + "in_moderation", + "published", + "rejected", + "archived" + ] + }, + "public.recipe_variant_type": { + "name": "recipe_variant_type", + "schema": "public", + "values": [ + "standard", + "high_protein", + "low_calorie", + "low_fat", + "vegetarian", + "vegan", + "gluten_free", + "lactose_free", + "kid_friendly", + "airfryer", + "budget" + ] + }, + "public.recipe_verification_status": { + "name": "recipe_verification_status", + "schema": "public", + "values": [ + "unverified", + "community", + "verified", + "editorial" + ] + }, + "public.religious_rule": { + "name": "religious_rule", + "schema": "public", + "values": [ + "none", + "halal", + "kosher", + "hindu_no_beef", + "buddhist_vegetarian" + ] + }, + "public.scan_type": { + "name": "scan_type", + "schema": "public", + "values": [ + "fridge", + "freezer", + "pantry", + "ingredients", + "plate", + "receipt", + "barcode", + "expiry_date", + "nutrition_label", + "product_package" + ] + }, + "public.sex": { + "name": "sex", + "schema": "public", + "values": [ + "female", + "male", + "unspecified" + ] + }, + "public.signal_origin": { + "name": "signal_origin", + "schema": "public", + "values": [ + "user_stated", + "observed", + "ai_inferred" + ] + }, + "public.storage_location_type": { + "name": "storage_location_type", + "schema": "public", + "values": [ + "fridge", + "freezer", + "pantry", + "garage_freezer", + "wine_fridge", + "cellar", + "meal_box", + "custom" + ] + }, + "public.subscription_plan": { + "name": "subscription_plan", + "schema": "public", + "values": [ + "free", + "household", + "family", + "large_household" + ] + }, + "public.subscription_provider": { + "name": "subscription_provider", + "schema": "public", + "values": [ + "apple", + "google", + "promo", + "none" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "trial", + "active", + "in_grace", + "on_hold", + "paused", + "canceled", + "expired" + ] + }, + "public.taste_axis": { + "name": "taste_axis", + "schema": "public", + "values": [ + "spice", + "salt", + "acid", + "creaminess", + "garlic", + "sweetness", + "herbs", + "umami" + ] + }, + "public.unit": { + "name": "unit", + "schema": "public", + "values": [ + "GRAM", + "KILOGRAM", + "MILLILITER", + "DECILITER", + "LITER", + "TEASPOON", + "TABLESPOON", + "CUP_US", + "FLUID_OUNCE_US", + "OUNCE", + "POUND", + "COUNT", + "PORTION", + "PINCH", + "SLICE", + "CLOVE", + "CAN", + "PACKAGE" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "user", + "moderator", + "admin" + ] + }, + "public.verification_status": { + "name": "verification_status", + "schema": "public", + "values": [ + "unverified", + "user_verified", + "editorially_verified" + ] + }, + "public.measurement_system": { + "name": "measurement_system", + "schema": "public", + "values": [ + "METRIC", + "US_CUSTOMARY", + "MIXED" + ] + }, + "public.temperature_unit": { + "name": "temperature_unit", + "schema": "public", + "values": [ + "CELSIUS", + "FAHRENHEIT" + ] + }, + "public.translation_source": { + "name": "translation_source", + "schema": "public", + "values": [ + "seed", + "ai", + "human" + ] + }, + "public.translation_status": { + "name": "translation_status", + "schema": "public", + "values": [ + "draft_ai", + "in_review", + "published" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/infrastructure/migrations/meta/_journal.json b/infrastructure/migrations/meta/_journal.json index bad7861..49f2b4f 100644 --- a/infrastructure/migrations/meta/_journal.json +++ b/infrastructure/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1785832532499, "tag": "0000_misty_human_robot", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1785933544717, + "tag": "0001_eager_wilson_fisk", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/database/src/analytics-gdpr.ts b/packages/database/src/analytics-gdpr.ts new file mode 100644 index 0000000..50bcdd3 --- /dev/null +++ b/packages/database/src/analytics-gdpr.ts @@ -0,0 +1,50 @@ +/** + * GDPR helpers for product analytics. + * Hard deletion – SET NULL is not enough because anonymous_id can link rows. + * + * Consent model: product_analytics is managed via user_consents + * (CONSENT_KINDS in @app/shared-types). It is registered as granted by default + * on account creation (legitimate interest with opt-out). Users revoke it via + * PUT /v1/me/consents { kind: "product_analytics", granted: false }. + * + * Semantics for existing accounts created before this feature: + * - Missing user_consents row for product_analytics → treated as granted. + * - Row with status = "granted" → granted. + * - Row with status = "revoked" or "denied" → not granted. + */ +import type { NodePgDatabase } from "drizzle-orm/node-postgres"; +import { and, eq } from "drizzle-orm"; +import * as schema from "./schema/index.js"; + +export type DbClient = NodePgDatabase; + +/** + * Delete all product analytics events for a user. + * Called during account deletion (spec §56). + */ +export async function deleteUserAnalyticsEvents(db: DbClient, userId: string): Promise { + const result = await db + .delete(schema.productAnalyticsEvents) + .where(eq(schema.productAnalyticsEvents.userId, userId)); + return result.rowCount ?? 0; +} + +/** + * Check if a user has analytics collection enabled. + * Missing row → granted (legitimate interest, opt-out). + * Only an explicit revoked/denied row disables collection. + */ +export async function isAnalyticsOptedIn(db: DbClient, userId: string): Promise { + const [row] = await db + .select({ status: schema.userConsents.status }) + .from(schema.userConsents) + .where( + and( + eq(schema.userConsents.userId, userId), + eq(schema.userConsents.kind, "product_analytics"), + ), + ) + .limit(1); + if (!row) return true; // default granted for existing accounts + return row.status === "granted"; +} diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 55e83a7..f438b17 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -1,3 +1,4 @@ export * from "./client.js"; export * as schema from "./schema/index.js"; export * from "./schema/index.js"; +export * from "./analytics-gdpr.js"; diff --git a/packages/database/src/schema/analytics.ts b/packages/database/src/schema/analytics.ts new file mode 100644 index 0000000..2c51a11 --- /dev/null +++ b/packages/database/src/schema/analytics.ts @@ -0,0 +1,67 @@ +/** + * Product analytics schema (spec §8). + * Pseudonymous events stored in Postgres, separated from AAMOS memory. + */ +import { index, integer, jsonb, pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; +import { createdAt } from "./_shared.js"; +import { households } from "./households.js"; +import { users } from "./users.js"; + +/** Event name stored as text so the taxonomy can grow without ALTER TYPE. */ +const ANALYTICS_EVENT_NAME_LEN = 64; + +/** + * Raw product analytics events. + * No PII, no raw ingredient names, no free text, no images. + */ +export const productAnalyticsEvents = pgTable( + "product_analytics_events", + { + id: uuid("id").primaryKey().defaultRandom(), + occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(), + receivedAt: timestamp("received_at", { withTimezone: true }).notNull().defaultNow(), + eventName: varchar("event_name", { length: ANALYTICS_EVENT_NAME_LEN }).notNull(), + /** Pseudonymous device/session identifiers. */ + anonymousId: varchar("anonymous_id", { length: 64 }), + sessionId: varchar("session_id", { length: 64 }), + /** Foreign keys are nullable because events may arrive before login. */ + userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }), + householdId: uuid("household_id").references(() => households.id, { onDelete: "set null" }), + /** App version, platform, locale. */ + appVersion: varchar("app_version", { length: 32 }), + platform: varchar("platform", { length: 16 }), + locale: varchar("locale", { length: 16 }), + /** Experiment / feature flag context. */ + experimentVariant: varchar("experiment_variant", { length: 128 }), + /** Structured, safe properties only. */ + properties: jsonb("properties").notNull().default({}), + }, + (t) => [ + index("pa_events_name_occurred_idx").on(t.eventName, t.occurredAt), + index("pa_events_household_occurred_idx").on(t.householdId, t.occurredAt), + index("pa_events_user_occurred_idx").on(t.userId, t.occurredAt), + index("pa_events_session_idx").on(t.sessionId), + index("pa_events_received_idx").on(t.receivedAt), + ], +); + +/** + * Materialized-like daily aggregates per household. + * Kept simple; dashboards can also query product_analytics_events directly. + */ +export const analyticsDailySnapshots = pgTable( + "analytics_daily_snapshots", + { + id: uuid("id").primaryKey().defaultRandom(), + date: varchar("date", { length: 10 }).notNull(), // YYYY-MM-DD UTC + householdId: uuid("household_id").references(() => households.id, { onDelete: "cascade" }), + eventName: varchar("event_name", { length: ANALYTICS_EVENT_NAME_LEN }).notNull(), + count: integer("count").notNull().default(0), + uniqueSessions: integer("unique_sessions").notNull().default(0), + propertiesFingerprint: varchar("properties_fingerprint", { length: 64 }), + }, + (t) => [ + index("pa_daily_household_date_idx").on(t.householdId, t.date), + index("pa_daily_date_event_idx").on(t.date, t.eventName), + ], +); diff --git a/packages/database/src/schema/index.ts b/packages/database/src/schema/index.ts index f537330..70a2a2e 100644 --- a/packages/database/src/schema/index.ts +++ b/packages/database/src/schema/index.ts @@ -15,4 +15,5 @@ export * from "./scans.js"; export * from "./memory.js"; export * from "./seasons.js"; export * from "./subscriptions.js"; +export * from "./analytics.js"; export * from "./platform.js"; diff --git a/packages/database/test/analytics-gdpr.test.ts b/packages/database/test/analytics-gdpr.test.ts new file mode 100644 index 0000000..6475b9c --- /dev/null +++ b/packages/database/test/analytics-gdpr.test.ts @@ -0,0 +1,105 @@ +/** + * GDPR helpers for product analytics – integration tests. + * Assumes DATABASE_URL points to a writable database. + */ +import { describe, expect, it, beforeAll, afterAll } from "vitest"; +import { eq } from "drizzle-orm"; +import { createDatabase, closeDatabase, schema, isAnalyticsOptedIn, deleteUserAnalyticsEvents } from "@app/database"; + +const TEST_USER_EMAIL = "analytics-gdpr-test@example.invalid"; + +describe("analytics GDPR helpers", () => { + const { db, pool } = createDatabase(); + let userId: string; + + beforeAll(async () => { + // Clean up any stale test user. + await db.delete(schema.users).where(eq(schema.users.email, TEST_USER_EMAIL)); + const [user] = await db + .insert(schema.users) + .values({ + email: TEST_USER_EMAIL, + displayName: "Analytics Test", + locale: "sv-SE", + }) + .returning({ id: schema.users.id }); + userId = user!.id; + }); + + afterAll(async () => { + await db.delete(schema.userConsents).where(eq(schema.userConsents.userId, userId)); + await db.delete(schema.users).where(eq(schema.users.id, userId)); + await closeDatabase(); + }); + + it("missing consent row → opted in (legitimate interest, opt-out)", async () => { + await db.delete(schema.userConsents).where(eq(schema.userConsents.userId, userId)); + expect(await isAnalyticsOptedIn(db, userId)).toBe(true); + }); + + it("explicit granted → opted in", async () => { + await db + .insert(schema.userConsents) + .values({ + userId, + kind: "product_analytics", + status: "granted", + grantedAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: [schema.userConsents.userId, schema.userConsents.kind], + set: { status: "granted", revokedAt: null, updatedAt: new Date() }, + }); + expect(await isAnalyticsOptedIn(db, userId)).toBe(true); + }); + + it("explicit revoked → opted out", async () => { + const now = new Date(); + await db + .insert(schema.userConsents) + .values({ + userId, + kind: "product_analytics", + status: "revoked", + revokedAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [schema.userConsents.userId, schema.userConsents.kind], + set: { status: "revoked", revokedAt: now, updatedAt: now }, + }); + expect(await isAnalyticsOptedIn(db, userId)).toBe(false); + }); + + it("deleteUserAnalyticsEvents removes only the target user's events", async () => { + // Insert a dummy event for the test user and another user. + const [otherUser] = await db + .insert(schema.users) + .values({ email: "other-analytics-test@example.invalid", displayName: "Other", locale: "sv-SE" }) + .returning({ id: schema.users.id }); + + await db.insert(schema.productAnalyticsEvents).values({ + userId, + eventName: "app_first_open", + anonymousId: "anon-test", + }); + await db.insert(schema.productAnalyticsEvents).values({ + userId: otherUser!.id, + eventName: "app_first_open", + anonymousId: "anon-other", + }); + + const deleted = await deleteUserAnalyticsEvents(db, userId); + expect(deleted).toBe(1); + + const remaining = await db + .select({ id: schema.productAnalyticsEvents.id }) + .from(schema.productAnalyticsEvents) + .where(eq(schema.productAnalyticsEvents.userId, otherUser!.id)); + expect(remaining).toHaveLength(1); + + await db.delete(schema.productAnalyticsEvents).where(eq(schema.productAnalyticsEvents.userId, otherUser!.id)); + await db.delete(schema.users).where(eq(schema.users.id, otherUser!.id)); + }); +}); diff --git a/packages/shared-types/src/analytics.ts b/packages/shared-types/src/analytics.ts new file mode 100644 index 0000000..4aa3931 --- /dev/null +++ b/packages/shared-types/src/analytics.ts @@ -0,0 +1,132 @@ +/** + * Product analytics taxonomy (spec §8.2). + * Pseudonymous events separated from AAMOS memory and domain events. + * All event names are snake_case and stable. + */ + +// --------------------------------------------------------------------------- +// Acquisition & onboarding +// --------------------------------------------------------------------------- +export const ANALYTICS_EVENT_NAMES = [ + "app_installed", + "app_first_open", + "onboarding_started", + "onboarding_step_completed", + "onboarding_skipped", + "account_created", + "household_created", + "household_joined", + "allergy_setup_completed", + "camera_permission_result", + "notification_permission_result", +] as const; + +// --------------------------------------------------------------------------- +// Inventory +// --------------------------------------------------------------------------- +export const ANALYTICS_INVENTORY_EVENT_NAMES = [ + "scan_started", + "scan_uploaded", + "scan_processing_completed", + "scan_processing_failed", + "scan_review_opened", + "scan_item_confirmed", + "scan_item_corrected", + "scan_item_removed", + "scan_completed", + "inventory_item_added", + "inventory_item_consumed", + "inventory_item_discarded", + "inventory_reconciliation_started", + "inventory_reconciliation_completed", + "inventory_conflict_created", + "inventory_conflict_resolved", +] as const; + +// --------------------------------------------------------------------------- +// Recipes & cooking +// --------------------------------------------------------------------------- +export const ANALYTICS_COOKING_EVENT_NAMES = [ + "recommendations_viewed", + "recommendation_opened", + "recipe_saved", + "cooking_session_started", + "cooking_session_completed", + "cooking_session_cancelled", + "leftovers_created", + "substitution_confirmed", +] as const; + +// --------------------------------------------------------------------------- +// Shopping +// --------------------------------------------------------------------------- +export const ANALYTICS_SHOPPING_EVENT_NAMES = [ + "shopping_item_added", + "shopping_item_checked", + "shopping_list_shared", + "receipt_scanned", + "purchase_imported", +] as const; + +// --------------------------------------------------------------------------- +// Household +// --------------------------------------------------------------------------- +export const ANALYTICS_HOUSEHOLD_EVENT_NAMES = [ + "household_invite_sent", + "household_invite_accepted", + "second_member_first_action", +] as const; + +// --------------------------------------------------------------------------- +// Subscription +// --------------------------------------------------------------------------- +export const ANALYTICS_SUBSCRIPTION_EVENT_NAMES = [ + "paywall_viewed", + "trial_started", + "trial_cancelled", + "subscription_started", + "subscription_renewed", + "subscription_grace_period", + "subscription_cancelled", + "subscription_expired", + "entitlement_restored", +] as const; + +// --------------------------------------------------------------------------- +// Feedback +// --------------------------------------------------------------------------- +export const ANALYTICS_FEEDBACK_EVENT_NAMES = [ + "feedback_submitted", + "incorrect_inventory_reported", + "bad_recommendation_reported", + "support_contact_started", +] as const; + +export const PRODUCT_ANALYTICS_EVENT_NAMES = [ + ...ANALYTICS_EVENT_NAMES, + ...ANALYTICS_INVENTORY_EVENT_NAMES, + ...ANALYTICS_COOKING_EVENT_NAMES, + ...ANALYTICS_SHOPPING_EVENT_NAMES, + ...ANALYTICS_HOUSEHOLD_EVENT_NAMES, + ...ANALYTICS_SUBSCRIPTION_EVENT_NAMES, + ...ANALYTICS_FEEDBACK_EVENT_NAMES, +] as const; + +export type ProductAnalyticsEventName = (typeof PRODUCT_ANALYTICS_EVENT_NAMES)[number]; + +/** + * Built-in funnels. Each funnel is an ordered list of event names. + */ +export const FUNNELS = { + install_to_account: ["app_installed", "app_first_open", "account_created"], + account_to_first_scan: ["account_created", "scan_started", "scan_completed"], + scan_to_inventory: ["scan_completed", "inventory_item_added"], + inventory_to_recipe: ["inventory_item_added", "recommendations_viewed"], + recipe_to_cooking: ["recommendation_opened", "cooking_session_started"], + cooking_to_updated_inventory: ["cooking_session_started", "cooking_session_completed"], + activated_to_trial: ["cooking_session_completed", "trial_started"], + trial_to_paid: ["trial_started", "subscription_started"], + single_to_two_members: ["household_invite_sent", "household_invite_accepted", "second_member_first_action"], +} as const; + +export type FunnelName = keyof typeof FUNNELS; diff --git a/packages/shared-types/src/enums.ts b/packages/shared-types/src/enums.ts index 4f250d2..5a7e4df 100644 --- a/packages/shared-types/src/enums.ts +++ b/packages/shared-types/src/enums.ts @@ -485,6 +485,8 @@ export const CONSENT_KINDS = [ "health_integration", "location_weather", "push_notifications", + /** Product analytics – legitimate interest with opt-out (spec §8, §33). */ + "product_analytics", ] as const; export type ConsentKind = (typeof CONSENT_KINDS)[number]; diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts index 14a8ec9..a2d0941 100644 --- a/packages/shared-types/src/index.ts +++ b/packages/shared-types/src/index.ts @@ -6,3 +6,4 @@ export * from "./brand.js"; export * from "./locale.js"; export * from "./money.js"; export * from "./measurement.js"; +export * from "./analytics.js";