From eff11c61bc2e40c545e2ec93530193e7b8e02e21 Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Fri, 28 Aug 2026 15:12:16 -0500 Subject: [PATCH] spike(db): evaluate Drizzle and Tinqer against the hardest query we have (#216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both libraries converted the same target — `buildItemFilterSql`, six clauses composed at run time, a recursive CTE for the category subtree, an `ANY(...::int[])` tag match with a count equality. Nothing in `src/routes` or `src/itemFilters.ts` is touched; this branch only adds spike artifacts alongside them. Drizzle cleared the blocker the issue named first. `backend/tsconfig.json` is `module: commonjs` and Drizzle is ESM-first, but it compiles under the existing config and requires at runtime, so no ESM migration is hiding inside this one. `drizzle-kit pull` introspected all sixteen tables plus `pgmigrations`, 104 columns, 8 indexes and 20 foreign keys, and got the hard parts right: the self-referencing `categories.parent_id`, and both partial unique indexes with `lower(name)` and their `WHERE` predicates. The converted filter produces byte-equivalent results. Five filter combinations run against the dev database return identical id lists to the current implementation, including the recursive subtree — 1805, 2145, 4, 1918 and 2145 rows respectively. The injection question the issue asked about is answered yes, and it is stronger than expected. In a Drizzle `sql` template `${value}` emits a bind parameter, not text, so there is no way to spell "interpolate this as SQL" by accident. Feeding `"1); DROP TABLE items; --"` as a status produced it in the parameter array and nowhere in the query text. That is the #202 invariant enforced by the type system rather than by a comment and two tests. Two Drizzle findings worth having before committing to 187 call sites. Arrays do not bind the way the raw driver does: `${array}` expands into a placeholder list, so `ANY(($1, $2)::int[])` type-checks, reads correctly, and fails at run time as invalid Postgres. `sql.param()` is required, and nothing warns. And the first generated migration after a pull carried spurious drops and recreations of the three expression indexes; re-running with no schema change reports nothing to migrate, so it settles rather than recurring, but that first migration would need hand-editing. Tinqer is genuinely LINQ-to-SQL — it parses the lambda with OXC at run time and compiles a real expression tree — and it cannot express this query. Compound conditions and array membership work. A ternary fails. A block body with an `if` fails. Those are the only two ways to make a clause optional inside the lambda, and there is no raw-SQL escape hatch in its API, so six independent optional clauses would mean 64 hand-written plans or neutral sentinels that do not exist for the category and tag clauses. Its failure mode compounds that: `defineSelect` parses eagerly and throws, so an unsupported query type-checks cleanly and crashes when the module is first required. `src/db-tinqer/probe.ts` wraps every case in a function for that reason. It is also `0.0.27` with 24 stars, and its Postgres support is a `pg-promise` adapter rather than the `pg` driver already in use. I was wrong earlier to say LINQ-to-SQL is impossible in TypeScript because it needs C# expression trees. Tinqer reconstructs the tree by parsing the lambda source. The claim should have been that it is possible and rare, and the constraint is what the parser accepts. Verified: backend build clean, 280 unit tests and 255 integration tests pass, unchanged by this branch. Refs #216 Co-Authored-By: Claude Opus 5 (1M context) --- backend/drizzle.config.ts | 15 + .../drizzle/0000_sleepy_franklin_richards.sql | 194 ++ backend/drizzle/0001_add_condition_note.sql | 7 + backend/drizzle/meta/0000_snapshot.json | 1384 ++++++++++++++ backend/drizzle/meta/0001_snapshot.json | 1363 +++++++++++++ backend/drizzle/meta/_journal.json | 20 + backend/drizzle/relations.ts | 150 ++ backend/drizzle/schema.ts | 289 +++ backend/package-lock.json | 1690 +++++++++++++++++ backend/package.json | 5 + backend/src/db-drizzle/itemFilters.drizzle.ts | 74 + backend/src/db-drizzle/schema.ts | 290 +++ backend/src/db-tinqer/probe.ts | 71 + 13 files changed, 5552 insertions(+) create mode 100644 backend/drizzle.config.ts create mode 100644 backend/drizzle/0000_sleepy_franklin_richards.sql create mode 100644 backend/drizzle/0001_add_condition_note.sql create mode 100644 backend/drizzle/meta/0000_snapshot.json create mode 100644 backend/drizzle/meta/0001_snapshot.json create mode 100644 backend/drizzle/meta/_journal.json create mode 100644 backend/drizzle/relations.ts create mode 100644 backend/drizzle/schema.ts create mode 100644 backend/src/db-drizzle/itemFilters.drizzle.ts create mode 100644 backend/src/db-drizzle/schema.ts create mode 100644 backend/src/db-tinqer/probe.ts diff --git a/backend/drizzle.config.ts b/backend/drizzle.config.ts new file mode 100644 index 0000000..b3cde78 --- /dev/null +++ b/backend/drizzle.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'drizzle-kit'; + +// Spike configuration (#216). Credentials come from the environment rather than +// this file — the same rule the rest of the repo follows, and this one points at +// a developer's local database, not a deployed one. +// +// DRIZZLE_DATABASE_URL=postgres://redefined_local:redefined_local@localhost:55500/redefined_local +export default defineConfig({ + dialect: 'postgresql', + schema: './src/db-drizzle/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DRIZZLE_DATABASE_URL ?? '' + } +}); diff --git a/backend/drizzle/0000_sleepy_franklin_richards.sql b/backend/drizzle/0000_sleepy_franklin_richards.sql new file mode 100644 index 0000000..4f8730f --- /dev/null +++ b/backend/drizzle/0000_sleepy_franklin_richards.sql @@ -0,0 +1,194 @@ +-- Current sql file was generated after introspecting the database +-- If you want to run this migration please uncomment this code before executing migrations +/* +CREATE TABLE "pgmigrations" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "run_on" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "admin_settings" ( + "key" text PRIMARY KEY NOT NULL, + "value" text NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "item_images" ( + "id" serial PRIMARY KEY NOT NULL, + "item_id" integer NOT NULL, + "image_path" text NOT NULL, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "shipping_addresses" ( + "id" serial PRIMARY KEY NOT NULL, + "customer_id" integer NOT NULL, + "full_name" text NOT NULL, + "address_line1" text NOT NULL, + "address_line2" text, + "city" text NOT NULL, + "state" text NOT NULL, + "postal_code" text NOT NULL, + "country" text DEFAULT 'US' NOT NULL, + "is_default" boolean DEFAULT false NOT NULL, + "usps_validated" boolean DEFAULT false NOT NULL, + "usps_standardized" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "items" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "description" text, + "price_cents" integer NOT NULL, + "status" text DEFAULT 'pending' NOT NULL, + "reserved_until" timestamp with time zone, + "sold_at" timestamp with time zone, + "paypal_order_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "category_id" integer +); +--> statement-breakpoint +CREATE TABLE "tags" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "color" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "categories" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "parent_id" integer, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "orders" ( + "id" serial PRIMARY KEY NOT NULL, + "item_id" integer, + "customer_id" integer, + "checkout_id" integer, + "processor" text NOT NULL, + "processor_order_id" text, + "amount_cents" integer, + "status" text, + "raw_event" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "cart_items" ( + "id" serial PRIMARY KEY NOT NULL, + "cart_id" integer NOT NULL, + "item_id" integer NOT NULL, + "added_at" timestamp with time zone DEFAULT now() NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "last_reminder_sent_at" timestamp with time zone, + CONSTRAINT "cart_items_item_id_key" UNIQUE("item_id") +); +--> statement-breakpoint +CREATE TABLE "carts" ( + "id" serial PRIMARY KEY NOT NULL, + "customer_id" integer NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "carts_customer_id_key" UNIQUE("customer_id") +); +--> statement-breakpoint +CREATE TABLE "customer_tokens" ( + "token" text PRIMARY KEY NOT NULL, + "customer_id" integer NOT NULL, + "kind" text NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "checkouts" ( + "id" serial PRIMARY KEY NOT NULL, + "customer_id" integer, + "shipping_address_id" integer, + "processor" text NOT NULL, + "processor_order_id" text, + "amount_cents" integer, + "status" text DEFAULT 'pending' NOT NULL, + "raw_event" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "customer_sessions" ( + "token" text PRIMARY KEY NOT NULL, + "customer_id" integer NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "customers" ( + "id" serial PRIMARY KEY NOT NULL, + "email" text NOT NULL, + "password_hash" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "marketing_consent" boolean DEFAULT false NOT NULL, + "marketing_consent_at" timestamp with time zone, + "marketing_consent_text" text, + "unsubscribe_token" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "disabled_at" timestamp with time zone, + "favorite_alerts" boolean DEFAULT false NOT NULL, + "favorite_alerts_at" timestamp with time zone, + "favorite_alerts_text" text, + "first_name" text, + "last_name" text, + CONSTRAINT "customers_email_key" UNIQUE("email"), + CONSTRAINT "customers_unsubscribe_token_key" UNIQUE("unsubscribe_token") +); +--> statement-breakpoint +CREATE TABLE "item_tags" ( + "item_id" integer NOT NULL, + "tag_id" integer NOT NULL, + CONSTRAINT "item_tags_pkey" PRIMARY KEY("item_id","tag_id") +); +--> statement-breakpoint +CREATE TABLE "favorites" ( + "customer_id" integer NOT NULL, + "item_id" integer NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "favorites_pkey" PRIMARY KEY("customer_id","item_id") +); +--> statement-breakpoint +CREATE TABLE "checkout_items" ( + "checkout_id" integer NOT NULL, + "item_id" integer NOT NULL, + "price_cents" integer NOT NULL, + CONSTRAINT "checkout_items_pkey" PRIMARY KEY("checkout_id","item_id") +); +--> statement-breakpoint +ALTER TABLE "item_images" ADD CONSTRAINT "item_images_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shipping_addresses" ADD CONSTRAINT "shipping_addresses_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "items" ADD CONSTRAINT "items_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "categories" ADD CONSTRAINT "categories_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "orders" ADD CONSTRAINT "orders_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "orders" ADD CONSTRAINT "orders_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "orders" ADD CONSTRAINT "orders_checkout_id_fkey" FOREIGN KEY ("checkout_id") REFERENCES "public"."checkouts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_cart_id_fkey" FOREIGN KEY ("cart_id") REFERENCES "public"."carts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "carts" ADD CONSTRAINT "carts_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "customer_tokens" ADD CONSTRAINT "customer_tokens_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "checkouts" ADD CONSTRAINT "checkouts_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "checkouts" ADD CONSTRAINT "checkouts_shipping_address_id_fkey" FOREIGN KEY ("shipping_address_id") REFERENCES "public"."shipping_addresses"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "customer_sessions" ADD CONSTRAINT "customer_sessions_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "item_tags" ADD CONSTRAINT "item_tags_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "item_tags" ADD CONSTRAINT "item_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "public"."tags"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "favorites" ADD CONSTRAINT "favorites_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "favorites" ADD CONSTRAINT "favorites_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "checkout_items" ADD CONSTRAINT "checkout_items_checkout_id_fkey" FOREIGN KEY ("checkout_id") REFERENCES "public"."checkouts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "checkout_items" ADD CONSTRAINT "checkout_items_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "items_category_id_idx" ON "items" USING btree ("category_id" int4_ops);--> statement-breakpoint +CREATE UNIQUE INDEX "tags_name_uniq" ON "tags" USING btree (lower(name) text_ops);--> statement-breakpoint +CREATE UNIQUE INDEX "categories_child_name_uniq" ON "categories" USING btree (parent_id text_ops,lower(name) int4_ops) WHERE (parent_id IS NOT NULL);--> statement-breakpoint +CREATE INDEX "categories_parent_id_idx" ON "categories" USING btree ("parent_id" int4_ops);--> statement-breakpoint +CREATE UNIQUE INDEX "categories_root_name_uniq" ON "categories" USING btree (lower(name) text_ops) WHERE (parent_id IS NULL);--> statement-breakpoint +CREATE INDEX "customers_disabled_at_idx" ON "customers" USING btree ("disabled_at" timestamptz_ops) WHERE (disabled_at IS NOT NULL);--> statement-breakpoint +CREATE INDEX "item_tags_tag_id_idx" ON "item_tags" USING btree ("tag_id" int4_ops);--> statement-breakpoint +CREATE INDEX "favorites_item_id_idx" ON "favorites" USING btree ("item_id" int4_ops); +*/ \ No newline at end of file diff --git a/backend/drizzle/0001_add_condition_note.sql b/backend/drizzle/0001_add_condition_note.sql new file mode 100644 index 0000000..20dccbb --- /dev/null +++ b/backend/drizzle/0001_add_condition_note.sql @@ -0,0 +1,7 @@ +DROP INDEX "tags_name_uniq";--> statement-breakpoint +DROP INDEX "categories_child_name_uniq";--> statement-breakpoint +DROP INDEX "categories_root_name_uniq";--> statement-breakpoint +ALTER TABLE "items" ADD COLUMN "condition_note" text;--> statement-breakpoint +CREATE UNIQUE INDEX "tags_name_uniq" ON "tags" USING btree (lower(name));--> statement-breakpoint +CREATE UNIQUE INDEX "categories_child_name_uniq" ON "categories" USING btree (parent_id,lower(name)) WHERE (parent_id IS NOT NULL);--> statement-breakpoint +CREATE UNIQUE INDEX "categories_root_name_uniq" ON "categories" USING btree (lower(name)) WHERE (parent_id IS NULL); \ No newline at end of file diff --git a/backend/drizzle/meta/0000_snapshot.json b/backend/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..60ef76c --- /dev/null +++ b/backend/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1384 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "prevId": "", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.pgmigrations": { + "name": "pgmigrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "run_on": { + "name": "run_on", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.admin_settings": { + "name": "admin_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.item_images": { + "name": "item_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image_path": { + "name": "image_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "item_images_item_id_fkey": { + "name": "item_images_item_id_fkey", + "tableFrom": "item_images", + "tableTo": "items", + "schemaTo": "public", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.shipping_addresses": { + "name": "shipping_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address_line1": { + "name": "address_line1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address_line2": { + "name": "address_line2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'US'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "usps_validated": { + "name": "usps_validated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "usps_standardized": { + "name": "usps_standardized", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shipping_addresses_customer_id_fkey": { + "name": "shipping_addresses_customer_id_fkey", + "tableFrom": "shipping_addresses", + "tableTo": "customers", + "schemaTo": "public", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.items": { + "name": "items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reserved_until": { + "name": "reserved_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sold_at": { + "name": "sold_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paypal_order_id": { + "name": "paypal_order_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "items_category_id_idx": { + "name": "items_category_id_idx", + "columns": [ + { + "expression": "category_id", + "asc": true, + "nulls": "last", + "opclass": "int4_ops", + "isExpression": false + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "items_category_id_fkey": { + "name": "items_category_id_fkey", + "tableFrom": "items", + "tableTo": "categories", + "schemaTo": "public", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tags_name_uniq": { + "name": "tags_name_uniq", + "columns": [ + { + "expression": "lower(name)", + "asc": true, + "nulls": "last", + "opclass": "text_ops", + "isExpression": true + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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": { + "categories_child_name_uniq": { + "name": "categories_child_name_uniq", + "columns": [ + { + "expression": "parent_id", + "asc": true, + "nulls": "last", + "opclass": "text_ops", + "isExpression": true + }, + { + "expression": "lower(name)", + "asc": true, + "nulls": "last", + "opclass": "int4_ops", + "isExpression": true + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "where": "(parent_id IS NOT NULL)", + "with": {} + }, + "categories_parent_id_idx": { + "name": "categories_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "asc": true, + "nulls": "last", + "opclass": "int4_ops", + "isExpression": false + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_root_name_uniq": { + "name": "categories_root_name_uniq", + "columns": [ + { + "expression": "lower(name)", + "asc": true, + "nulls": "last", + "opclass": "text_ops", + "isExpression": true + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "where": "(parent_id IS NULL)", + "with": {} + } + }, + "foreignKeys": { + "categories_parent_id_fkey": { + "name": "categories_parent_id_fkey", + "tableFrom": "categories", + "tableTo": "categories", + "schemaTo": "public", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checkout_id": { + "name": "checkout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "processor": { + "name": "processor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor_order_id": { + "name": "processor_order_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_event": { + "name": "raw_event", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "orders_item_id_fkey": { + "name": "orders_item_id_fkey", + "tableFrom": "orders", + "tableTo": "items", + "schemaTo": "public", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "orders_customer_id_fkey": { + "name": "orders_customer_id_fkey", + "tableFrom": "orders", + "tableTo": "customers", + "schemaTo": "public", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "orders_checkout_id_fkey": { + "name": "orders_checkout_id_fkey", + "tableFrom": "orders", + "tableTo": "checkouts", + "schemaTo": "public", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.cart_items": { + "name": "cart_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "cart_id": { + "name": "cart_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_reminder_sent_at": { + "name": "last_reminder_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cart_items_cart_id_fkey": { + "name": "cart_items_cart_id_fkey", + "tableFrom": "cart_items", + "tableTo": "carts", + "schemaTo": "public", + "columnsFrom": [ + "cart_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cart_items_item_id_fkey": { + "name": "cart_items_item_id_fkey", + "tableFrom": "cart_items", + "tableTo": "items", + "schemaTo": "public", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cart_items_item_id_key": { + "columns": [ + "item_id" + ], + "nullsNotDistinct": false, + "name": "cart_items_item_id_key" + } + }, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.carts": { + "name": "carts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "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": { + "carts_customer_id_fkey": { + "name": "carts_customer_id_fkey", + "tableFrom": "carts", + "tableTo": "customers", + "schemaTo": "public", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "carts_customer_id_key": { + "columns": [ + "customer_id" + ], + "nullsNotDistinct": false, + "name": "carts_customer_id_key" + } + }, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.customer_tokens": { + "name": "customer_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_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": {}, + "foreignKeys": { + "customer_tokens_customer_id_fkey": { + "name": "customer_tokens_customer_id_fkey", + "tableFrom": "customer_tokens", + "tableTo": "customers", + "schemaTo": "public", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.checkouts": { + "name": "checkouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "shipping_address_id": { + "name": "shipping_address_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "processor": { + "name": "processor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor_order_id": { + "name": "processor_order_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "raw_event": { + "name": "raw_event", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "checkouts_customer_id_fkey": { + "name": "checkouts_customer_id_fkey", + "tableFrom": "checkouts", + "tableTo": "customers", + "schemaTo": "public", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "checkouts_shipping_address_id_fkey": { + "name": "checkouts_shipping_address_id_fkey", + "tableFrom": "checkouts", + "tableTo": "shipping_addresses", + "schemaTo": "public", + "columnsFrom": [ + "shipping_address_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.customer_sessions": { + "name": "customer_sessions", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_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": {}, + "foreignKeys": { + "customer_sessions_customer_id_fkey": { + "name": "customer_sessions_customer_id_fkey", + "tableFrom": "customer_sessions", + "tableTo": "customers", + "schemaTo": "public", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketing_consent": { + "name": "marketing_consent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketing_consent_at": { + "name": "marketing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_consent_text": { + "name": "marketing_consent_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unsubscribe_token": { + "name": "unsubscribe_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "favorite_alerts": { + "name": "favorite_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "favorite_alerts_at": { + "name": "favorite_alerts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "favorite_alerts_text": { + "name": "favorite_alerts_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "customers_disabled_at_idx": { + "name": "customers_disabled_at_idx", + "columns": [ + { + "expression": "disabled_at", + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops", + "isExpression": false + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "where": "(disabled_at IS NOT NULL)", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "customers_email_key": { + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "customers_email_key" + }, + "customers_unsubscribe_token_key": { + "columns": [ + "unsubscribe_token" + ], + "nullsNotDistinct": false, + "name": "customers_unsubscribe_token_key" + } + }, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.item_tags": { + "name": "item_tags", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "item_tags_tag_id_idx": { + "name": "item_tags_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "asc": true, + "nulls": "last", + "opclass": "int4_ops", + "isExpression": false + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "item_tags_item_id_fkey": { + "name": "item_tags_item_id_fkey", + "tableFrom": "item_tags", + "tableTo": "items", + "schemaTo": "public", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_tags_tag_id_fkey": { + "name": "item_tags_tag_id_fkey", + "tableFrom": "item_tags", + "tableTo": "tags", + "schemaTo": "public", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_tags_pkey": { + "name": "item_tags_pkey", + "columns": [ + "item_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.favorites": { + "name": "favorites", + "schema": "", + "columns": { + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "favorites_item_id_idx": { + "name": "favorites_item_id_idx", + "columns": [ + { + "expression": "item_id", + "asc": true, + "nulls": "last", + "opclass": "int4_ops", + "isExpression": false + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "favorites_customer_id_fkey": { + "name": "favorites_customer_id_fkey", + "tableFrom": "favorites", + "tableTo": "customers", + "schemaTo": "public", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "favorites_item_id_fkey": { + "name": "favorites_item_id_fkey", + "tableFrom": "favorites", + "tableTo": "items", + "schemaTo": "public", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "favorites_pkey": { + "name": "favorites_pkey", + "columns": [ + "customer_id", + "item_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + }, + "public.checkout_items": { + "name": "checkout_items", + "schema": "", + "columns": { + "checkout_id": { + "name": "checkout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "checkout_items_checkout_id_fkey": { + "name": "checkout_items_checkout_id_fkey", + "tableFrom": "checkout_items", + "tableTo": "checkouts", + "schemaTo": "public", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "checkout_items_item_id_fkey": { + "name": "checkout_items_item_id_fkey", + "tableFrom": "checkout_items", + "tableTo": "items", + "schemaTo": "public", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "checkout_items_pkey": { + "name": "checkout_items_pkey", + "columns": [ + "checkout_id", + "item_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraints": {}, + "policies": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/0001_snapshot.json b/backend/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..86fc8a5 --- /dev/null +++ b/backend/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1363 @@ +{ + "id": "8023ec5b-dd3f-4cdb-bf4c-f8f0b81f0a33", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.admin_settings": { + "name": "admin_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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.cart_items": { + "name": "cart_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "cart_id": { + "name": "cart_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_reminder_sent_at": { + "name": "last_reminder_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cart_items_cart_id_fkey": { + "name": "cart_items_cart_id_fkey", + "tableFrom": "cart_items", + "tableTo": "carts", + "columnsFrom": [ + "cart_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cart_items_item_id_fkey": { + "name": "cart_items_item_id_fkey", + "tableFrom": "cart_items", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cart_items_item_id_key": { + "name": "cart_items_item_id_key", + "nullsNotDistinct": false, + "columns": [ + "item_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.carts": { + "name": "carts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "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": { + "carts_customer_id_fkey": { + "name": "carts_customer_id_fkey", + "tableFrom": "carts", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "carts_customer_id_key": { + "name": "carts_customer_id_key", + "nullsNotDistinct": false, + "columns": [ + "customer_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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": { + "categories_child_name_uniq": { + "name": "categories_child_name_uniq", + "columns": [ + { + "expression": "parent_id", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "lower(name)", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "(parent_id IS NOT NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_parent_id_idx": { + "name": "categories_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "categories_root_name_uniq": { + "name": "categories_root_name_uniq", + "columns": [ + { + "expression": "lower(name)", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "(parent_id IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "categories_parent_id_fkey": { + "name": "categories_parent_id_fkey", + "tableFrom": "categories", + "tableTo": "categories", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkout_items": { + "name": "checkout_items", + "schema": "", + "columns": { + "checkout_id": { + "name": "checkout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "checkout_items_checkout_id_fkey": { + "name": "checkout_items_checkout_id_fkey", + "tableFrom": "checkout_items", + "tableTo": "checkouts", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "checkout_items_item_id_fkey": { + "name": "checkout_items_item_id_fkey", + "tableFrom": "checkout_items", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "checkout_items_pkey": { + "name": "checkout_items_pkey", + "columns": [ + "checkout_id", + "item_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkouts": { + "name": "checkouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "shipping_address_id": { + "name": "shipping_address_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "processor": { + "name": "processor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor_order_id": { + "name": "processor_order_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "raw_event": { + "name": "raw_event", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "checkouts_customer_id_fkey": { + "name": "checkouts_customer_id_fkey", + "tableFrom": "checkouts", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "checkouts_shipping_address_id_fkey": { + "name": "checkouts_shipping_address_id_fkey", + "tableFrom": "checkouts", + "tableTo": "shipping_addresses", + "columnsFrom": [ + "shipping_address_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_sessions": { + "name": "customer_sessions", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_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": {}, + "foreignKeys": { + "customer_sessions_customer_id_fkey": { + "name": "customer_sessions_customer_id_fkey", + "tableFrom": "customer_sessions", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_tokens": { + "name": "customer_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_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": {}, + "foreignKeys": { + "customer_tokens_customer_id_fkey": { + "name": "customer_tokens_customer_id_fkey", + "tableFrom": "customer_tokens", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketing_consent": { + "name": "marketing_consent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketing_consent_at": { + "name": "marketing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_consent_text": { + "name": "marketing_consent_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unsubscribe_token": { + "name": "unsubscribe_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "favorite_alerts": { + "name": "favorite_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "favorite_alerts_at": { + "name": "favorite_alerts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "favorite_alerts_text": { + "name": "favorite_alerts_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "customers_disabled_at_idx": { + "name": "customers_disabled_at_idx", + "columns": [ + { + "expression": "disabled_at", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "where": "(disabled_at IS NOT NULL)", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "customers_email_key": { + "name": "customers_email_key", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "customers_unsubscribe_token_key": { + "name": "customers_unsubscribe_token_key", + "nullsNotDistinct": false, + "columns": [ + "unsubscribe_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.favorites": { + "name": "favorites", + "schema": "", + "columns": { + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "favorites_item_id_idx": { + "name": "favorites_item_id_idx", + "columns": [ + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "favorites_customer_id_fkey": { + "name": "favorites_customer_id_fkey", + "tableFrom": "favorites", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "favorites_item_id_fkey": { + "name": "favorites_item_id_fkey", + "tableFrom": "favorites", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "favorites_pkey": { + "name": "favorites_pkey", + "columns": [ + "customer_id", + "item_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item_images": { + "name": "item_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image_path": { + "name": "image_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "item_images_item_id_fkey": { + "name": "item_images_item_id_fkey", + "tableFrom": "item_images", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item_tags": { + "name": "item_tags", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "item_tags_tag_id_idx": { + "name": "item_tags_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "item_tags_item_id_fkey": { + "name": "item_tags_item_id_fkey", + "tableFrom": "item_tags", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_tags_tag_id_fkey": { + "name": "item_tags_tag_id_fkey", + "tableFrom": "item_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_tags_pkey": { + "name": "item_tags_pkey", + "columns": [ + "item_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.items": { + "name": "items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reserved_until": { + "name": "reserved_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sold_at": { + "name": "sold_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paypal_order_id": { + "name": "paypal_order_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "condition_note": { + "name": "condition_note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "items_category_id_idx": { + "name": "items_category_id_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "items_category_id_fkey": { + "name": "items_category_id_fkey", + "tableFrom": "items", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checkout_id": { + "name": "checkout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "processor": { + "name": "processor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor_order_id": { + "name": "processor_order_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_event": { + "name": "raw_event", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "orders_item_id_fkey": { + "name": "orders_item_id_fkey", + "tableFrom": "orders", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "orders_customer_id_fkey": { + "name": "orders_customer_id_fkey", + "tableFrom": "orders", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "orders_checkout_id_fkey": { + "name": "orders_checkout_id_fkey", + "tableFrom": "orders", + "tableTo": "checkouts", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pgmigrations": { + "name": "pgmigrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "run_on": { + "name": "run_on", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shipping_addresses": { + "name": "shipping_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address_line1": { + "name": "address_line1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address_line2": { + "name": "address_line2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'US'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "usps_validated": { + "name": "usps_validated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "usps_standardized": { + "name": "usps_standardized", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shipping_addresses_customer_id_fkey": { + "name": "shipping_addresses_customer_id_fkey", + "tableFrom": "shipping_addresses", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tags_name_uniq": { + "name": "tags_name_uniq", + "columns": [ + { + "expression": "lower(name)", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json new file mode 100644 index 0000000..7813ec8 --- /dev/null +++ b/backend/drizzle/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1787946791717, + "tag": "0000_sleepy_franklin_richards", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1787946975565, + "tag": "0001_add_condition_note", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/backend/drizzle/relations.ts b/backend/drizzle/relations.ts new file mode 100644 index 0000000..d2f3dcf --- /dev/null +++ b/backend/drizzle/relations.ts @@ -0,0 +1,150 @@ +import { relations } from "drizzle-orm/relations"; +import { items, itemImages, customers, shippingAddresses, categories, orders, checkouts, carts, cartItems, customerTokens, customerSessions, itemTags, tags, favorites, checkoutItems } from "./schema"; + +export const itemImagesRelations = relations(itemImages, ({one}) => ({ + item: one(items, { + fields: [itemImages.itemId], + references: [items.id] + }), +})); + +export const itemsRelations = relations(items, ({one, many}) => ({ + itemImages: many(itemImages), + category: one(categories, { + fields: [items.categoryId], + references: [categories.id] + }), + orders: many(orders), + cartItems: many(cartItems), + itemTags: many(itemTags), + favorites: many(favorites), + checkoutItems: many(checkoutItems), +})); + +export const shippingAddressesRelations = relations(shippingAddresses, ({one, many}) => ({ + customer: one(customers, { + fields: [shippingAddresses.customerId], + references: [customers.id] + }), + checkouts: many(checkouts), +})); + +export const customersRelations = relations(customers, ({many}) => ({ + shippingAddresses: many(shippingAddresses), + orders: many(orders), + carts: many(carts), + customerTokens: many(customerTokens), + checkouts: many(checkouts), + customerSessions: many(customerSessions), + favorites: many(favorites), +})); + +export const categoriesRelations = relations(categories, ({one, many}) => ({ + items: many(items), + category: one(categories, { + fields: [categories.parentId], + references: [categories.id], + relationName: "categories_parentId_categories_id" + }), + categories: many(categories, { + relationName: "categories_parentId_categories_id" + }), +})); + +export const ordersRelations = relations(orders, ({one}) => ({ + item: one(items, { + fields: [orders.itemId], + references: [items.id] + }), + customer: one(customers, { + fields: [orders.customerId], + references: [customers.id] + }), + checkout: one(checkouts, { + fields: [orders.checkoutId], + references: [checkouts.id] + }), +})); + +export const checkoutsRelations = relations(checkouts, ({one, many}) => ({ + orders: many(orders), + customer: one(customers, { + fields: [checkouts.customerId], + references: [customers.id] + }), + shippingAddress: one(shippingAddresses, { + fields: [checkouts.shippingAddressId], + references: [shippingAddresses.id] + }), + checkoutItems: many(checkoutItems), +})); + +export const cartItemsRelations = relations(cartItems, ({one}) => ({ + cart: one(carts, { + fields: [cartItems.cartId], + references: [carts.id] + }), + item: one(items, { + fields: [cartItems.itemId], + references: [items.id] + }), +})); + +export const cartsRelations = relations(carts, ({one, many}) => ({ + cartItems: many(cartItems), + customer: one(customers, { + fields: [carts.customerId], + references: [customers.id] + }), +})); + +export const customerTokensRelations = relations(customerTokens, ({one}) => ({ + customer: one(customers, { + fields: [customerTokens.customerId], + references: [customers.id] + }), +})); + +export const customerSessionsRelations = relations(customerSessions, ({one}) => ({ + customer: one(customers, { + fields: [customerSessions.customerId], + references: [customers.id] + }), +})); + +export const itemTagsRelations = relations(itemTags, ({one}) => ({ + item: one(items, { + fields: [itemTags.itemId], + references: [items.id] + }), + tag: one(tags, { + fields: [itemTags.tagId], + references: [tags.id] + }), +})); + +export const tagsRelations = relations(tags, ({many}) => ({ + itemTags: many(itemTags), +})); + +export const favoritesRelations = relations(favorites, ({one}) => ({ + customer: one(customers, { + fields: [favorites.customerId], + references: [customers.id] + }), + item: one(items, { + fields: [favorites.itemId], + references: [items.id] + }), +})); + +export const checkoutItemsRelations = relations(checkoutItems, ({one}) => ({ + checkout: one(checkouts, { + fields: [checkoutItems.checkoutId], + references: [checkouts.id] + }), + item: one(items, { + fields: [checkoutItems.itemId], + references: [items.id] + }), +})); \ No newline at end of file diff --git a/backend/drizzle/schema.ts b/backend/drizzle/schema.ts new file mode 100644 index 0000000..5985a9b --- /dev/null +++ b/backend/drizzle/schema.ts @@ -0,0 +1,289 @@ +import { pgTable, serial, varchar, timestamp, text, foreignKey, integer, boolean, jsonb, index, uniqueIndex, unique, primaryKey } from "drizzle-orm/pg-core" +import { sql } from "drizzle-orm" + + + +export const pgmigrations = pgTable("pgmigrations", { + id: serial().primaryKey().notNull(), + name: varchar({ length: 255 }).notNull(), + runOn: timestamp("run_on", { mode: 'string' }).notNull(), +}); + +export const adminSettings = pgTable("admin_settings", { + key: text().primaryKey().notNull(), + value: text().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}); + +export const itemImages = pgTable("item_images", { + id: serial().primaryKey().notNull(), + itemId: integer("item_id").notNull(), + imagePath: text("image_path").notNull(), + sortOrder: integer("sort_order").default(0).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "item_images_item_id_fkey" + }).onDelete("cascade"), +]); + +export const shippingAddresses = pgTable("shipping_addresses", { + id: serial().primaryKey().notNull(), + customerId: integer("customer_id").notNull(), + fullName: text("full_name").notNull(), + addressLine1: text("address_line1").notNull(), + addressLine2: text("address_line2"), + city: text().notNull(), + state: text().notNull(), + postalCode: text("postal_code").notNull(), + country: text().default('US').notNull(), + isDefault: boolean("is_default").default(false).notNull(), + uspsValidated: boolean("usps_validated").default(false).notNull(), + uspsStandardized: jsonb("usps_standardized"), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "shipping_addresses_customer_id_fkey" + }).onDelete("cascade"), +]); + +export const items = pgTable("items", { + id: serial().primaryKey().notNull(), + name: text().notNull(), + description: text(), + priceCents: integer("price_cents").notNull(), + status: text().default('pending').notNull(), + reservedUntil: timestamp("reserved_until", { withTimezone: true, mode: 'string' }), + soldAt: timestamp("sold_at", { withTimezone: true, mode: 'string' }), + paypalOrderId: text("paypal_order_id"), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + categoryId: integer("category_id"), +}, (table) => [ + index("items_category_id_idx").using("btree", table.categoryId.asc().nullsLast().op("int4_ops")), + foreignKey({ + columns: [table.categoryId], + foreignColumns: [categories.id], + name: "items_category_id_fkey" + }).onDelete("set null"), +]); + +export const tags = pgTable("tags", { + id: serial().primaryKey().notNull(), + name: text().notNull(), + color: text().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + uniqueIndex("tags_name_uniq").using("btree", sql`lower(name)`), +]); + +export const categories = pgTable("categories", { + id: serial().primaryKey().notNull(), + name: text().notNull(), + parentId: integer("parent_id"), + sortOrder: integer("sort_order").default(0).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + uniqueIndex("categories_child_name_uniq").using("btree", sql`parent_id`, sql`lower(name)`).where(sql`(parent_id IS NOT NULL)`), + index("categories_parent_id_idx").using("btree", table.parentId.asc().nullsLast().op("int4_ops")), + uniqueIndex("categories_root_name_uniq").using("btree", sql`lower(name)`).where(sql`(parent_id IS NULL)`), + foreignKey({ + columns: [table.parentId], + foreignColumns: [table.id], + name: "categories_parent_id_fkey" + }).onDelete("cascade"), +]); + +export const orders = pgTable("orders", { + id: serial().primaryKey().notNull(), + itemId: integer("item_id"), + customerId: integer("customer_id"), + checkoutId: integer("checkout_id"), + processor: text().notNull(), + processorOrderId: text("processor_order_id"), + amountCents: integer("amount_cents"), + status: text(), + rawEvent: jsonb("raw_event"), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "orders_item_id_fkey" + }), + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "orders_customer_id_fkey" + }).onDelete("set null"), + foreignKey({ + columns: [table.checkoutId], + foreignColumns: [checkouts.id], + name: "orders_checkout_id_fkey" + }).onDelete("set null"), +]); + +export const cartItems = pgTable("cart_items", { + id: serial().primaryKey().notNull(), + cartId: integer("cart_id").notNull(), + itemId: integer("item_id").notNull(), + addedAt: timestamp("added_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }).notNull(), + lastReminderSentAt: timestamp("last_reminder_sent_at", { withTimezone: true, mode: 'string' }), +}, (table) => [ + foreignKey({ + columns: [table.cartId], + foreignColumns: [carts.id], + name: "cart_items_cart_id_fkey" + }).onDelete("cascade"), + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "cart_items_item_id_fkey" + }).onDelete("cascade"), + unique("cart_items_item_id_key").on(table.itemId), +]); + +export const carts = pgTable("carts", { + id: serial().primaryKey().notNull(), + customerId: integer("customer_id").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "carts_customer_id_fkey" + }).onDelete("cascade"), + unique("carts_customer_id_key").on(table.customerId), +]); + +export const customerTokens = pgTable("customer_tokens", { + token: text().primaryKey().notNull(), + customerId: integer("customer_id").notNull(), + kind: text().notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "customer_tokens_customer_id_fkey" + }).onDelete("cascade"), +]); + +export const checkouts = pgTable("checkouts", { + id: serial().primaryKey().notNull(), + customerId: integer("customer_id"), + shippingAddressId: integer("shipping_address_id"), + processor: text().notNull(), + processorOrderId: text("processor_order_id"), + amountCents: integer("amount_cents"), + status: text().default('pending').notNull(), + rawEvent: jsonb("raw_event"), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "checkouts_customer_id_fkey" + }).onDelete("set null"), + foreignKey({ + columns: [table.shippingAddressId], + foreignColumns: [shippingAddresses.id], + name: "checkouts_shipping_address_id_fkey" + }).onDelete("set null"), +]); + +export const customerSessions = pgTable("customer_sessions", { + token: text().primaryKey().notNull(), + customerId: integer("customer_id").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "customer_sessions_customer_id_fkey" + }).onDelete("cascade"), +]); + +export const customers = pgTable("customers", { + id: serial().primaryKey().notNull(), + email: text().notNull(), + passwordHash: text("password_hash").notNull(), + emailVerified: boolean("email_verified").default(false).notNull(), + marketingConsent: boolean("marketing_consent").default(false).notNull(), + marketingConsentAt: timestamp("marketing_consent_at", { withTimezone: true, mode: 'string' }), + marketingConsentText: text("marketing_consent_text"), + unsubscribeToken: text("unsubscribe_token").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + disabledAt: timestamp("disabled_at", { withTimezone: true, mode: 'string' }), + favoriteAlerts: boolean("favorite_alerts").default(false).notNull(), + favoriteAlertsAt: timestamp("favorite_alerts_at", { withTimezone: true, mode: 'string' }), + favoriteAlertsText: text("favorite_alerts_text"), + firstName: text("first_name"), + lastName: text("last_name"), +}, (table) => [ + index("customers_disabled_at_idx").using("btree", table.disabledAt.asc().nullsLast().op("timestamptz_ops")).where(sql`(disabled_at IS NOT NULL)`), + unique("customers_email_key").on(table.email), + unique("customers_unsubscribe_token_key").on(table.unsubscribeToken), +]); + +export const itemTags = pgTable("item_tags", { + itemId: integer("item_id").notNull(), + tagId: integer("tag_id").notNull(), +}, (table) => [ + index("item_tags_tag_id_idx").using("btree", table.tagId.asc().nullsLast().op("int4_ops")), + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "item_tags_item_id_fkey" + }).onDelete("cascade"), + foreignKey({ + columns: [table.tagId], + foreignColumns: [tags.id], + name: "item_tags_tag_id_fkey" + }).onDelete("cascade"), + primaryKey({ columns: [table.itemId, table.tagId], name: "item_tags_pkey"}), +]); + +export const favorites = pgTable("favorites", { + customerId: integer("customer_id").notNull(), + itemId: integer("item_id").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + index("favorites_item_id_idx").using("btree", table.itemId.asc().nullsLast().op("int4_ops")), + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "favorites_customer_id_fkey" + }).onDelete("cascade"), + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "favorites_item_id_fkey" + }).onDelete("cascade"), + primaryKey({ columns: [table.customerId, table.itemId], name: "favorites_pkey"}), +]); + +export const checkoutItems = pgTable("checkout_items", { + checkoutId: integer("checkout_id").notNull(), + itemId: integer("item_id").notNull(), + priceCents: integer("price_cents").notNull(), +}, (table) => [ + foreignKey({ + columns: [table.checkoutId], + foreignColumns: [checkouts.id], + name: "checkout_items_checkout_id_fkey" + }).onDelete("cascade"), + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "checkout_items_item_id_fkey" + }).onDelete("cascade"), + primaryKey({ columns: [table.checkoutId, table.itemId], name: "checkout_items_pkey"}), +]); diff --git a/backend/package-lock.json b/backend/package-lock.json index 4653eda..a9b0d05 100755 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "@types/markdown-it": "^14.2.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.6", + "drizzle-orm": "^0.45.2", "express": "^4.19.2", "express-rate-limit": "^8.6.2", "markdown-it": "^15.0.0", @@ -22,6 +23,8 @@ }, "devDependencies": { "@eslint/js": "^9.39.5", + "@tinqerjs/pg-promise-adapter": "^0.0.27", + "@tinqerjs/tinqer": "^0.0.27", "@types/bcryptjs": "^2.4.6", "@types/cookie-parser": "^1.4.7", "@types/express": "^4.17.21", @@ -32,10 +35,12 @@ "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/supertest": "^6.0.2", + "drizzle-kit": "^0.31.10", "eslint": "^9.39.5", "eslint-plugin-sonarjs": "^4.2.0", "globals": "^17.11.0", "jest": "^29.7.0", + "pg-promise": "^12.7.1", "supertest": "^7.0.0", "ts-jest": "^29.2.4", "tsx": "^4.16.5", @@ -589,6 +594,494 @@ "dev": true, "license": "MIT" }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -1711,6 +2204,28 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -1724,6 +2239,382 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.134.0.tgz", + "integrity": "sha512-N9Us7l/X9ZC3LA6eWSzPyduvBPXV1eRyDPwM6/UWpxwwXGsatb8131+d2L8UsmyHrixnKLHBd6UeH8wangV7fw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.134.0.tgz", + "integrity": "sha512-Ic2oPZESeCaD4+9cKRqp1GMYsTO9Q3Yi9HdY2x9x75ozbnC20sybFHzeBklmaVD9PBzd8KbkmNN0gy+SVlm7zw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.134.0.tgz", + "integrity": "sha512-1z9+nVJ1Awq4CPyHAthx5zOUrg5T1zc3dWt6juxwDcuejFGbdYzWJITkS1rv4DCdSTphDU3IW71MzyLV3BjRGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.134.0.tgz", + "integrity": "sha512-MpofofaRZnxmYrY3lE8RTLHmE1KkX2q0elEeJ8sMcLbS8At76BjYSL6axssqhx29prGGDzIZ5lYFD+IXqaTzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.134.0.tgz", + "integrity": "sha512-OqnPQY27vqWAbMnHfLcF8CVOUv2cCvdlTiqyK5qz5WCbH3XOLpYVQATv8S5UrR8bbEJCAqJLsI7W6cRFXAxCoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.134.0.tgz", + "integrity": "sha512-0eqWl+PWrcwGC3b8DCB58w3QINAuZfpX2ULTGpI01GUMBc6zDKSpttWxvqPIydxuQEkGQTQRAXLLvc+vTDZbQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.134.0.tgz", + "integrity": "sha512-YToasuDmyzpyTC1ztrvkaSXz8tP+YUbx041M/4SGxaRGiyMzsKkQ869KPUTGA57A6aVsb1/DiPX8XZQQeSFkiw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.134.0.tgz", + "integrity": "sha512-qMS7NLc2o8G7LLz53wisol+O7/YbMhtaGVhlfsTVLnrraf9kLFgzpiGjtQALLbdxX8uhx0Zmd4l+vY3s1/K4Gg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.134.0.tgz", + "integrity": "sha512-jUEsnxPXhrCYxswQLYvUOxZEE5UWFMkK5kBnrcPMj7TONz1pD0yKgPmOQCAx2LPoysJ/v2Sjg4RBUVoO2VXoZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.134.0.tgz", + "integrity": "sha512-FU5xMUsXnMuWKVCGo43c6SJsnqHcsioqm32PHdDY39cIRJa/AZbo7RMYW0W5gYcNZqb8EMhELkMDwbJOFbUhtg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.134.0.tgz", + "integrity": "sha512-tPc7OAhslHVmNebho1RSEGL//7i7Nm39gbQ+gYreBYwzvDVqNwdQH3S13ccM+R1TpimQ+3bIyFMfnilJIGRtjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.134.0.tgz", + "integrity": "sha512-TtWEC4MUAHodX5a5kGsmK8g5K49V5ewWfwGrXdbw+gXWLXWXuhi+ectNELmOvYIwaqPSTAry1HNS/hfXssaPHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.134.0.tgz", + "integrity": "sha512-mF4uls8TA8SPXSDLpclJ6z9S+vaeUnNp95iUEfqwv9oVJUAE7B2j4crOj8UvByRXpbO5N+aAlJadQEyFlnXU6g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.134.0.tgz", + "integrity": "sha512-hieAQplyJeCvzqdSAxpOOGvVCBVXo/ioIBxioHfsUrQu93Et7Hy52cCG/GHEnjImVyeqEIViyyjuB0nKghxz/w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.134.0.tgz", + "integrity": "sha512-OQnJAK4sFuNSLgsh2s/K+14a3kwbmqf2yh1JiADU9XSfDuRooZYbAxmmBVPiyQ97+jBIIA1x2oPZeYNto3Ioow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.134.0.tgz", + "integrity": "sha512-RYLooe6g31q/PqNFN0NjR80IK/ARGsfLasAXL42LXonL+5Cyy9Or76rjBKQLAjERikJwbRU/sYW9Q5Tnpt1A4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.134.0.tgz", + "integrity": "sha512-h8bmHyvc0PxA811prUjfVpJlQAurOOiRbohY4QNGjCEz+L72G9CmWl28OOz3mevrYlURgUsprNuH8hDJXh1VOw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.134.0.tgz", + "integrity": "sha512-EPTfanpBMLNnSAWCDYpbJp1stmsf5x6hMsAwymf2J8ylRupIvO6FtKmHBMdc/wt43y08iz6ILlzFGIXe32/Kbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.134.0.tgz", + "integrity": "sha512-yAwetF+fTr9lTMtmqvkkWiiMXvH/yjMxGzUDG2rTL/LV1lL37eZ2enUGIlIo0gwhBIKWgabDEidtil+kiqNpgQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.134.0.tgz", + "integrity": "sha512-4VY39G5tuGlBYiH13XbWqfLcwKygQEv0iyf8vtZ5NyLAGjI8M0MiYyLhj91GkWRznr+5VC0I+5R55HUDV/Rklw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.134.0.tgz", + "integrity": "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -1761,6 +2652,37 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tinqerjs/pg-promise-adapter": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/@tinqerjs/pg-promise-adapter/-/pg-promise-adapter-0.0.27.tgz", + "integrity": "sha512-GF2KDUWmavLDCrYRhisvwGR5cah5sSuo3loSunUVTITGvYivlrYl58zOwBKMkC0WfiVyrJ5nVAJ3E/Spt6NH9A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@tinqerjs/tinqer": "^0.0.27" + } + }, + "node_modules/@tinqerjs/tinqer": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/@tinqerjs/tinqer/-/tinqer-0.0.27.tgz", + "integrity": "sha512-Rf9zNwstFD1mb8BO6RkRatPbx0rJ0VXHrS9pk8doeVv6sCTgo6wyFR/8hQPYe1LTblJUV3SFQNAfJpvsVbNgrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "oxc-parser": "0.134.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2656,6 +3578,16 @@ "dev": true, "license": "MIT" }, + "node_modules/assert-options": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.3.tgz", + "integrity": "sha512-s6v4HnA+vYSGO4eZX+F+I3gvF74wPk+m6Z1Q3w1Dsg4Pnv/R24vhKAasoMVZGvDpOOfTg1Qz4ptZnEbuy95XsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3370,6 +4302,631 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4352,6 +5909,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6158,6 +7728,44 @@ "node": ">= 0.8.0" } }, + "node_modules/oxc-parser": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.134.0.tgz", + "integrity": "sha512-Hs8fRG6A94BzMrMkGOtrUS7JQjmslfF+IvIXslf3QURzK3ud0QmFJRiYZjTe4TzAQnTfvlk4AwZnqIbrUjiE4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.134.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.134.0", + "@oxc-parser/binding-android-arm64": "0.134.0", + "@oxc-parser/binding-darwin-arm64": "0.134.0", + "@oxc-parser/binding-darwin-x64": "0.134.0", + "@oxc-parser/binding-freebsd-x64": "0.134.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.134.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.134.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.134.0", + "@oxc-parser/binding-linux-arm64-musl": "0.134.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.134.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.134.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.134.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.134.0", + "@oxc-parser/binding-linux-x64-gnu": "0.134.0", + "@oxc-parser/binding-linux-x64-musl": "0.134.0", + "@oxc-parser/binding-openharmony-arm64": "0.134.0", + "@oxc-parser/binding-wasm32-wasi": "0.134.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.134.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.134.0", + "@oxc-parser/binding-win32-x64-msvc": "0.134.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6364,6 +7972,17 @@ "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", "license": "MIT" }, + "node_modules/pg-cursor": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.22.0.tgz", + "integrity": "sha512-knzXLKqarTjOvb3qDSW0JiGsazmxwEKXrqHfWRte7XUsOYccQRafn3BLnQobWwInkzFJSyOej8y8cQRh2z3kGw==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "pg": "^8" + } + }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -6373,6 +7992,16 @@ "node": ">=4.0.0" } }, + "node_modules/pg-minify": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-1.8.0.tgz", + "integrity": "sha512-jO/oJOununpx8DzKgvSsWm61P8JjwXlaxSlbbfTBo1nvSWoo/+I6qZYaSN96jm/KDwa5d+JMQwPGgcP6HXDRow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/pg-pool": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", @@ -6382,12 +8011,45 @@ "pg": ">=8.0" } }, + "node_modules/pg-promise": { + "version": "12.7.1", + "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-12.7.1.tgz", + "integrity": "sha512-tsemfuBRsHMGaFZKMpkmCAA9iLeHXuuTq+0DucJwJyftyXk7crlMFXq7SY0AOVelIrdSL8qwdl5omfj/R8JN6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-options": "0.8.3", + "pg": "8.23.0", + "pg-minify": "1.8.0", + "spex": "4.1.0" + }, + "engines": { + "node": ">=16.0" + }, + "peerDependencies": { + "pg-query-stream": "4.17.0" + } + }, "node_modules/pg-protocol": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", "license": "MIT" }, + "node_modules/pg-query-stream": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.17.0.tgz", + "integrity": "sha512-69bug2CH/AJi1BN8r08eZ8tAGdbZZVp9kB0FG9OC2VkJPHIyj7d3Nk9V8tmporO60wqeWvhn7qtQ5oNimwlFug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "pg-cursor": "^2.22.0" + }, + "peerDependencies": { + "pg": "^8" + } + }, "node_modules/pg-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", @@ -6751,6 +8413,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -7001,6 +8673,16 @@ "source-map": "^0.6.0" } }, + "node_modules/spex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/spex/-/spex-4.1.0.tgz", + "integrity": "sha512-ktgNAQ1X9x1A3IMChM6XBDeVjhGPbLgPQ8aEzGOaUIhZTnLeJSBApvi3gXT789hee6h73N3jOeWkXDwoPbYT/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -7429,6 +9111,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tsx": { "version": "4.23.12", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", diff --git a/backend/package.json b/backend/package.json index 924433d..7b2686e 100755 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,7 @@ "@types/markdown-it": "^14.2.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.6", + "drizzle-orm": "^0.45.2", "express": "^4.19.2", "express-rate-limit": "^8.6.2", "markdown-it": "^15.0.0", @@ -37,6 +38,8 @@ }, "devDependencies": { "@eslint/js": "^9.39.5", + "@tinqerjs/pg-promise-adapter": "^0.0.27", + "@tinqerjs/tinqer": "^0.0.27", "@types/bcryptjs": "^2.4.6", "@types/cookie-parser": "^1.4.7", "@types/express": "^4.17.21", @@ -47,10 +50,12 @@ "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/supertest": "^6.0.2", + "drizzle-kit": "^0.31.10", "eslint": "^9.39.5", "eslint-plugin-sonarjs": "^4.2.0", "globals": "^17.11.0", "jest": "^29.7.0", + "pg-promise": "^12.7.1", "supertest": "^7.0.0", "ts-jest": "^29.2.4", "tsx": "^4.16.5", diff --git a/backend/src/db-drizzle/itemFilters.drizzle.ts b/backend/src/db-drizzle/itemFilters.drizzle.ts new file mode 100644 index 0000000..ac03283 --- /dev/null +++ b/backend/src/db-drizzle/itemFilters.drizzle.ts @@ -0,0 +1,74 @@ +// Spike (#216): buildItemFilterSql expressed with Drizzle. +// +// Deliberately the hardest thing in the codebase — six optional clauses composed +// at run time, a recursive CTE for the category subtree, an ANY(...::int[]) tag +// match with a count equality, and array parameters. If this cannot be said +// cleanly, nothing else in the conversion matters. + +import { SQL, and, eq, gte, lte, sql, inArray, exists } from 'drizzle-orm'; +import { items, itemTags, favorites, categories } from './schema'; + +export interface SpikeFilters { + categoryIds: number[]; + tagIds: number[]; + minPriceCents: number | null; + maxPriceCents: number | null; + status: string[] | null; + favoritesOnly: boolean; +} + +export function buildItemFilterDrizzle( + filters: SpikeFilters, + favoritesCustomerId: number | null +): SQL[] { + const clauses: SQL[] = []; + + // The recursive CTE. Drizzle's $with() builds statement-level CTEs; this one + // has to sit inside an IN (...) subquery, so it stays a sql`` template. + // + // Note what that template does with ${filters.categoryIds}: it emits a BIND + // PARAMETER, not text. That is the difference from a plain JS template + // literal, and it is the whole of the #202 invariant expressed by the type + // system rather than by a comment — there is no way to spell "interpolate + // this value as SQL text" by accident. + if (filters.categoryIds.length) { + clauses.push(sql`${items.categoryId} IN ( + WITH RECURSIVE subtree AS ( + SELECT id FROM ${categories} WHERE id = ANY(${sql.param(filters.categoryIds)}::int[]) + UNION ALL + SELECT c.id FROM ${categories} c JOIN subtree s ON c.parent_id = s.id + ) + SELECT id FROM subtree + )`); + } + + // AND, not OR: the item must carry every selected tag, so the count of + // matched rows has to equal the number requested. + if (filters.tagIds.length) { + clauses.push(sql`( + SELECT COUNT(*) FROM ${itemTags} it + WHERE it.item_id = ${items.id} AND it.tag_id = ANY(${sql.param(filters.tagIds)}::int[]) + ) = ${filters.tagIds.length}`); + } + + if (filters.minPriceCents !== null) clauses.push(gte(items.priceCents, filters.minPriceCents)); + if (filters.maxPriceCents !== null) clauses.push(lte(items.priceCents, filters.maxPriceCents)); + + // inArray replaces `= ANY($n::text[])`. Drizzle emits an IN list of binds. + if (filters.status !== null) clauses.push(inArray(items.status, filters.status)); + + if (filters.favoritesOnly) { + if (favoritesCustomerId === null) throw new Error('favorites filter requires a customer id'); + clauses.push( + exists( + sql`(SELECT 1 FROM ${favorites} f WHERE f.item_id = ${items.id} AND f.customer_id = ${favoritesCustomerId})` + ) + ); + } + + return clauses; +} + +export function combine(clauses: SQL[]): SQL | undefined { + return clauses.length ? and(...clauses) : undefined; +} diff --git a/backend/src/db-drizzle/schema.ts b/backend/src/db-drizzle/schema.ts new file mode 100644 index 0000000..8bd9138 --- /dev/null +++ b/backend/src/db-drizzle/schema.ts @@ -0,0 +1,290 @@ +import { pgTable, serial, varchar, timestamp, text, foreignKey, integer, boolean, jsonb, index, uniqueIndex, unique, primaryKey } from "drizzle-orm/pg-core" +import { sql } from "drizzle-orm" + + + +export const pgmigrations = pgTable("pgmigrations", { + id: serial().primaryKey().notNull(), + name: varchar({ length: 255 }).notNull(), + runOn: timestamp("run_on", { mode: 'string' }).notNull(), +}); + +export const adminSettings = pgTable("admin_settings", { + key: text().primaryKey().notNull(), + value: text().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}); + +export const itemImages = pgTable("item_images", { + id: serial().primaryKey().notNull(), + itemId: integer("item_id").notNull(), + imagePath: text("image_path").notNull(), + sortOrder: integer("sort_order").default(0).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "item_images_item_id_fkey" + }).onDelete("cascade"), +]); + +export const shippingAddresses = pgTable("shipping_addresses", { + id: serial().primaryKey().notNull(), + customerId: integer("customer_id").notNull(), + fullName: text("full_name").notNull(), + addressLine1: text("address_line1").notNull(), + addressLine2: text("address_line2"), + city: text().notNull(), + state: text().notNull(), + postalCode: text("postal_code").notNull(), + country: text().default('US').notNull(), + isDefault: boolean("is_default").default(false).notNull(), + uspsValidated: boolean("usps_validated").default(false).notNull(), + uspsStandardized: jsonb("usps_standardized"), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "shipping_addresses_customer_id_fkey" + }).onDelete("cascade"), +]); + +export const items = pgTable("items", { + id: serial().primaryKey().notNull(), + name: text().notNull(), + description: text(), + priceCents: integer("price_cents").notNull(), + status: text().default('pending').notNull(), + reservedUntil: timestamp("reserved_until", { withTimezone: true, mode: 'string' }), + soldAt: timestamp("sold_at", { withTimezone: true, mode: 'string' }), + paypalOrderId: text("paypal_order_id"), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + categoryId: integer("category_id"), + conditionNote: text("condition_note"), +}, (table) => [ + index("items_category_id_idx").using("btree", table.categoryId.asc().nullsLast().op("int4_ops")), + foreignKey({ + columns: [table.categoryId], + foreignColumns: [categories.id], + name: "items_category_id_fkey" + }).onDelete("set null"), +]); + +export const tags = pgTable("tags", { + id: serial().primaryKey().notNull(), + name: text().notNull(), + color: text().notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + uniqueIndex("tags_name_uniq").using("btree", sql`lower(name)`), +]); + +export const categories = pgTable("categories", { + id: serial().primaryKey().notNull(), + name: text().notNull(), + parentId: integer("parent_id"), + sortOrder: integer("sort_order").default(0).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + uniqueIndex("categories_child_name_uniq").using("btree", sql`parent_id`, sql`lower(name)`).where(sql`(parent_id IS NOT NULL)`), + index("categories_parent_id_idx").using("btree", table.parentId.asc().nullsLast().op("int4_ops")), + uniqueIndex("categories_root_name_uniq").using("btree", sql`lower(name)`).where(sql`(parent_id IS NULL)`), + foreignKey({ + columns: [table.parentId], + foreignColumns: [table.id], + name: "categories_parent_id_fkey" + }).onDelete("cascade"), +]); + +export const orders = pgTable("orders", { + id: serial().primaryKey().notNull(), + itemId: integer("item_id"), + customerId: integer("customer_id"), + checkoutId: integer("checkout_id"), + processor: text().notNull(), + processorOrderId: text("processor_order_id"), + amountCents: integer("amount_cents"), + status: text(), + rawEvent: jsonb("raw_event"), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "orders_item_id_fkey" + }), + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "orders_customer_id_fkey" + }).onDelete("set null"), + foreignKey({ + columns: [table.checkoutId], + foreignColumns: [checkouts.id], + name: "orders_checkout_id_fkey" + }).onDelete("set null"), +]); + +export const cartItems = pgTable("cart_items", { + id: serial().primaryKey().notNull(), + cartId: integer("cart_id").notNull(), + itemId: integer("item_id").notNull(), + addedAt: timestamp("added_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }).notNull(), + lastReminderSentAt: timestamp("last_reminder_sent_at", { withTimezone: true, mode: 'string' }), +}, (table) => [ + foreignKey({ + columns: [table.cartId], + foreignColumns: [carts.id], + name: "cart_items_cart_id_fkey" + }).onDelete("cascade"), + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "cart_items_item_id_fkey" + }).onDelete("cascade"), + unique("cart_items_item_id_key").on(table.itemId), +]); + +export const carts = pgTable("carts", { + id: serial().primaryKey().notNull(), + customerId: integer("customer_id").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "carts_customer_id_fkey" + }).onDelete("cascade"), + unique("carts_customer_id_key").on(table.customerId), +]); + +export const customerTokens = pgTable("customer_tokens", { + token: text().primaryKey().notNull(), + customerId: integer("customer_id").notNull(), + kind: text().notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "customer_tokens_customer_id_fkey" + }).onDelete("cascade"), +]); + +export const checkouts = pgTable("checkouts", { + id: serial().primaryKey().notNull(), + customerId: integer("customer_id"), + shippingAddressId: integer("shipping_address_id"), + processor: text().notNull(), + processorOrderId: text("processor_order_id"), + amountCents: integer("amount_cents"), + status: text().default('pending').notNull(), + rawEvent: jsonb("raw_event"), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "checkouts_customer_id_fkey" + }).onDelete("set null"), + foreignKey({ + columns: [table.shippingAddressId], + foreignColumns: [shippingAddresses.id], + name: "checkouts_shipping_address_id_fkey" + }).onDelete("set null"), +]); + +export const customerSessions = pgTable("customer_sessions", { + token: text().primaryKey().notNull(), + customerId: integer("customer_id").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "customer_sessions_customer_id_fkey" + }).onDelete("cascade"), +]); + +export const customers = pgTable("customers", { + id: serial().primaryKey().notNull(), + email: text().notNull(), + passwordHash: text("password_hash").notNull(), + emailVerified: boolean("email_verified").default(false).notNull(), + marketingConsent: boolean("marketing_consent").default(false).notNull(), + marketingConsentAt: timestamp("marketing_consent_at", { withTimezone: true, mode: 'string' }), + marketingConsentText: text("marketing_consent_text"), + unsubscribeToken: text("unsubscribe_token").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + disabledAt: timestamp("disabled_at", { withTimezone: true, mode: 'string' }), + favoriteAlerts: boolean("favorite_alerts").default(false).notNull(), + favoriteAlertsAt: timestamp("favorite_alerts_at", { withTimezone: true, mode: 'string' }), + favoriteAlertsText: text("favorite_alerts_text"), + firstName: text("first_name"), + lastName: text("last_name"), +}, (table) => [ + index("customers_disabled_at_idx").using("btree", table.disabledAt.asc().nullsLast().op("timestamptz_ops")).where(sql`(disabled_at IS NOT NULL)`), + unique("customers_email_key").on(table.email), + unique("customers_unsubscribe_token_key").on(table.unsubscribeToken), +]); + +export const itemTags = pgTable("item_tags", { + itemId: integer("item_id").notNull(), + tagId: integer("tag_id").notNull(), +}, (table) => [ + index("item_tags_tag_id_idx").using("btree", table.tagId.asc().nullsLast().op("int4_ops")), + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "item_tags_item_id_fkey" + }).onDelete("cascade"), + foreignKey({ + columns: [table.tagId], + foreignColumns: [tags.id], + name: "item_tags_tag_id_fkey" + }).onDelete("cascade"), + primaryKey({ columns: [table.itemId, table.tagId], name: "item_tags_pkey"}), +]); + +export const favorites = pgTable("favorites", { + customerId: integer("customer_id").notNull(), + itemId: integer("item_id").notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, (table) => [ + index("favorites_item_id_idx").using("btree", table.itemId.asc().nullsLast().op("int4_ops")), + foreignKey({ + columns: [table.customerId], + foreignColumns: [customers.id], + name: "favorites_customer_id_fkey" + }).onDelete("cascade"), + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "favorites_item_id_fkey" + }).onDelete("cascade"), + primaryKey({ columns: [table.customerId, table.itemId], name: "favorites_pkey"}), +]); + +export const checkoutItems = pgTable("checkout_items", { + checkoutId: integer("checkout_id").notNull(), + itemId: integer("item_id").notNull(), + priceCents: integer("price_cents").notNull(), +}, (table) => [ + foreignKey({ + columns: [table.checkoutId], + foreignColumns: [checkouts.id], + name: "checkout_items_checkout_id_fkey" + }).onDelete("cascade"), + foreignKey({ + columns: [table.itemId], + foreignColumns: [items.id], + name: "checkout_items_item_id_fkey" + }).onDelete("cascade"), + primaryKey({ columns: [table.checkoutId, table.itemId], name: "checkout_items_pkey"}), +]); diff --git a/backend/src/db-tinqer/probe.ts b/backend/src/db-tinqer/probe.ts new file mode 100644 index 0000000..2a84318 --- /dev/null +++ b/backend/src/db-tinqer/probe.ts @@ -0,0 +1,71 @@ +// Spike (#216): what Tinqer can and cannot express, recorded as runnable cases. +// +// Every case is wrapped in a function rather than evaluated at module level. +// defineSelect() parses the lambda eagerly and THROWS on a shape it cannot +// handle, so a top-level call turns an unsupported query into an import-time +// crash. That is itself a finding worth keeping: the failure is a runtime throw +// at load, not a type error, so an unsupported query compiles cleanly and takes +// the process down when the module is first required. + +import { createSchema, defineSelect } from '@tinqerjs/tinqer'; +import { toSql } from '@tinqerjs/pg-promise-adapter'; + +interface Db { + items: { id: number; name: string; price_cents: number; status: string; category_id: number | null }; + categories: { id: number; parent_id: number | null }; +} + +const schema = createSchema(); + +export type Outcome = { label: string; ok: boolean; sql?: string; error?: string }; + +function attempt(label: string, build: () => unknown, params: Record): Outcome { + try { + const r = toSql(build() as never, params as never); + return { label, ok: true, sql: r.sql }; + } catch (e) { + return { label, ok: false, error: (e as Error).message.split('\n')[0] }; + } +} + +export function runCases(): Outcome[] { + return [ + // Supported. + attempt( + 'compound condition', + () => + defineSelect(schema, (q, p: { min: number; max: number }) => + q.from('items').where((i) => i.price_cents >= p.min && i.price_cents <= p.max).select((i) => ({ id: i.id })) + ), + { min: 1, max: 2 } + ), + attempt( + 'array membership', + () => + defineSelect(schema, (q, p: { statuses: string[] }) => + q.from('items').where((i) => p.statuses.includes(i.status)).select((i) => ({ id: i.id })) + ), + { statuses: ['available', 'sold'] } + ), + + // NOT supported — and these are the two shapes buildItemFilterSql needs. + attempt( + 'ternary (clause optional at run time)', + () => + defineSelect(schema, (q, p: { apply: boolean; min: number }) => + q.from('items').where((i) => (p.apply ? i.price_cents >= p.min : true)).select((i) => ({ id: i.id })) + ), + { apply: true, min: 1 } + ), + attempt( + 'block body with if (clause optional at run time)', + () => + defineSelect(schema, (q, p: { apply: boolean; min: number }) => { + let x = q.from('items'); + if (p.apply) x = x.where((i) => i.price_cents >= p.min); + return x.select((i) => ({ id: i.id })); + }), + { apply: true, min: 1 } + ) + ]; +}