From 5754b7ca25504dd8ee45d2197debaf14538c3cb6 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Fri, 2 Oct 2026 21:38:30 -0400 Subject: [PATCH] Step 5.3: add database schema and migrations Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/ci.yml | 15 + .prettierignore | 1 + DECISIONS.md | 13 + apps/server/drizzle.config.ts | 8 + apps/server/drizzle/0000_init.sql | 109 +++ apps/server/drizzle/meta/0000_snapshot.json | 842 ++++++++++++++++++++ apps/server/drizzle/meta/_journal.json | 13 + apps/server/package.json | 8 +- apps/server/src/db/client.ts | 16 + apps/server/src/db/migrate-cli.ts | 15 + apps/server/src/db/migrate.ts | 15 + apps/server/src/db/schema.ts | 174 ++++ apps/server/test/migrations.test.ts | 52 ++ apps/server/vitest.config.ts | 8 + memory-bank/architecture.md | 197 +++++ memory-bank/progress.md | 2 +- pnpm-lock.yaml | 783 ++++++++++++++++++ 17 files changed, 2269 insertions(+), 2 deletions(-) create mode 100644 apps/server/drizzle.config.ts create mode 100644 apps/server/drizzle/0000_init.sql create mode 100644 apps/server/drizzle/meta/0000_snapshot.json create mode 100644 apps/server/drizzle/meta/_journal.json create mode 100644 apps/server/src/db/client.ts create mode 100644 apps/server/src/db/migrate-cli.ts create mode 100644 apps/server/src/db/migrate.ts create mode 100644 apps/server/src/db/schema.ts create mode 100644 apps/server/test/migrations.test.ts create mode 100644 apps/server/vitest.config.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 20b0ce1..f5b0159 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -7,6 +7,21 @@ on: jobs: check: runs-on: ubuntu-latest + services: + # Test database for server integration tests. + postgres: + image: postgres:18 + env: + POSTGRES_USER: deadhead + POSTGRES_PASSWORD: deadhead + POSTGRES_DB: deadhead_test + options: >- + --health-cmd "pg_isready -U deadhead -d deadhead_test" + --health-interval 2s + --health-timeout 5s + --health-retries 30 + env: + TEST_DATABASE_URL: postgres://deadhead:deadhead@postgres:5432/deadhead_test steps: - uses: actions/checkout@v7 - uses: pnpm/action-setup@v6 diff --git a/.prettierignore b/.prettierignore index 51378bf..27708d1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ pnpm-lock.yaml memory-bank/ .pnpm-store/ +apps/server/drizzle/ diff --git a/DECISIONS.md b/DECISIONS.md index 150b42d..b4f5672 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -212,3 +212,16 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **The injectable clock arrives now, not in Step 7.2.** `buildApp(config, { clock })` takes a `Clock` (`() => RealTime`) and defaults to `Date.now`. Every endpoint will read "now" through it (decision 0.5.18). Step 7.2 still adds the test-only clock-offset endpoint. - **Game time uses the default `content.config`**, via sim `realToGame`. + +## Step 5.3 + +- **Every game table references `players.id`.** Players and companies are one-to-one; `companies` holds credits and is the row that money-changing endpoints lock (Step 6.14). +- **Times are `bigint` Unix ms** (Drizzle `mode: "number"`), like sim and the API. World seeds are `bigint` because unsigned 32-bit values overflow `integer`. +- **Status enums use `in_flight`** in the database, as the plan words it; the API uses `in-flight`, so the server maps between them. +- **Dispatches store the full sim `DispatchRecord` as JSONB**, plus the columns queries need (`status`, `arrival_at`, `resolved_at`, `acknowledged_at`, `tutorial`, `ship_id`, `pilot_id`). The applied `Resolution` is kept as JSONB for the summary. A tutorial dispatch has `ship_id` null, because the loaner is not a ship row. +- **Pilot skills are a JSONB `{ skillId: level }` map**, plus a `skill_points_spent` counter. Level is derived from XP, not stored. +- **Contract boards store their contracts as JSONB**, keyed by player, kind (`bay` / `tutorial`), location (the bay or the loaner's location) and period. +- **Auth tables are not here yet;** Better Auth's tables and the link from players to users arrive in Step 5.5. +- **Migrations are generated SQL** (`drizzle-kit generate`) committed in `apps/server/drizzle/`, which Prettier ignores. They are applied with the drizzle-orm migrator (`db:migrate`, or `runMigrations(url)` in tests). +- **Server test files run one at a time** (`apps/server/vitest.config.ts`) because integration tests share one test database. +- **CI runs a `postgres:18` service container** and points `TEST_DATABASE_URL` at it; locally the default is the compose database `deadhead_test`. diff --git a/apps/server/drizzle.config.ts b/apps/server/drizzle.config.ts new file mode 100644 index 0000000..86bbf34 --- /dev/null +++ b/apps/server/drizzle.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "drizzle-kit"; + +// `pnpm --filter @deadhead/server db:generate` writes SQL migrations to ./drizzle. +export default defineConfig({ + dialect: "postgresql", + schema: "./src/db/schema.ts", + out: "./drizzle", +}); diff --git a/apps/server/drizzle/0000_init.sql b/apps/server/drizzle/0000_init.sql new file mode 100644 index 0000000..4f924c7 --- /dev/null +++ b/apps/server/drizzle/0000_init.sql @@ -0,0 +1,109 @@ +CREATE TYPE "public"."activity_status" AS ENUM('idle', 'in_flight');--> statement-breakpoint +CREATE TYPE "public"."board_kind" AS ENUM('bay', 'tutorial');--> statement-breakpoint +CREATE TYPE "public"."dispatch_status" AS ENUM('in_flight', 'resolved');--> statement-breakpoint +CREATE TABLE "accepted_contracts" ( + "player_id" text NOT NULL, + "contract_id" text NOT NULL, + "period_index" integer NOT NULL, + "dispatch_id" text NOT NULL, + "accepted_at" bigint NOT NULL, + CONSTRAINT "accepted_contracts_player_id_contract_id_pk" PRIMARY KEY("player_id","contract_id") +); +--> statement-breakpoint +CREATE TABLE "bays" ( + "id" text PRIMARY KEY NOT NULL, + "player_id" text NOT NULL, + "location_id" text NOT NULL, + "created_at" bigint NOT NULL +); +--> statement-breakpoint +CREATE TABLE "companies" ( + "id" text PRIMARY KEY NOT NULL, + "player_id" text NOT NULL, + "name" text NOT NULL, + "credits" bigint NOT NULL, + CONSTRAINT "companies_player_id_unique" UNIQUE("player_id") +); +--> statement-breakpoint +CREATE TABLE "contract_boards" ( + "id" text PRIMARY KEY NOT NULL, + "player_id" text NOT NULL, + "kind" "board_kind" NOT NULL, + "location_id" text NOT NULL, + "period_index" integer NOT NULL, + "contracts" jsonb NOT NULL, + "created_at" bigint NOT NULL +); +--> statement-breakpoint +CREATE TABLE "discovered_locations" ( + "player_id" text NOT NULL, + "location_id" text NOT NULL, + "discovered_at" bigint NOT NULL, + CONSTRAINT "discovered_locations_player_id_location_id_pk" PRIMARY KEY("player_id","location_id") +); +--> statement-breakpoint +CREATE TABLE "dispatches" ( + "id" text PRIMARY KEY NOT NULL, + "player_id" text NOT NULL, + "ship_id" text, + "pilot_id" text NOT NULL, + "tutorial" boolean NOT NULL, + "record" jsonb NOT NULL, + "arrival_at" bigint NOT NULL, + "status" "dispatch_status" DEFAULT 'in_flight' NOT NULL, + "resolution" jsonb, + "resolved_at" bigint, + "acknowledged_at" bigint +); +--> statement-breakpoint +CREATE TABLE "pilots" ( + "id" text PRIMARY KEY NOT NULL, + "player_id" text NOT NULL, + "name" text NOT NULL, + "is_avatar" boolean DEFAULT false NOT NULL, + "xp" integer DEFAULT 0 NOT NULL, + "skills" jsonb DEFAULT '{}'::jsonb NOT NULL, + "skill_points_spent" integer DEFAULT 0 NOT NULL, + "assigned_ship_id" text, + "location_id" text, + "status" "activity_status" DEFAULT 'idle' NOT NULL, + "created_at" bigint NOT NULL, + CONSTRAINT "pilots_assigned_ship_id_unique" UNIQUE("assigned_ship_id") +); +--> statement-breakpoint +CREATE TABLE "players" ( + "id" text PRIMARY KEY NOT NULL, + "world_seed" bigint NOT NULL, + "tutorial_active" boolean DEFAULT true NOT NULL, + "loaner_location_id" text, + "created_at" bigint NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ships" ( + "id" text PRIMARY KEY NOT NULL, + "player_id" text NOT NULL, + "model_id" text NOT NULL, + "name" text NOT NULL, + "status" "activity_status" DEFAULT 'idle' NOT NULL, + "location_id" text, + "created_at" bigint NOT NULL +); +--> statement-breakpoint +ALTER TABLE "accepted_contracts" ADD CONSTRAINT "accepted_contracts_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "accepted_contracts" ADD CONSTRAINT "accepted_contracts_dispatch_id_dispatches_id_fk" FOREIGN KEY ("dispatch_id") REFERENCES "public"."dispatches"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bays" ADD CONSTRAINT "bays_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "companies" ADD CONSTRAINT "companies_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "contract_boards" ADD CONSTRAINT "contract_boards_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "discovered_locations" ADD CONSTRAINT "discovered_locations_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "dispatches" ADD CONSTRAINT "dispatches_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "dispatches" ADD CONSTRAINT "dispatches_ship_id_ships_id_fk" FOREIGN KEY ("ship_id") REFERENCES "public"."ships"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "dispatches" ADD CONSTRAINT "dispatches_pilot_id_pilots_id_fk" FOREIGN KEY ("pilot_id") REFERENCES "public"."pilots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pilots" ADD CONSTRAINT "pilots_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pilots" ADD CONSTRAINT "pilots_assigned_ship_id_ships_id_fk" FOREIGN KEY ("assigned_ship_id") REFERENCES "public"."ships"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ships" ADD CONSTRAINT "ships_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "bays_player_location_idx" ON "bays" USING btree ("player_id","location_id");--> statement-breakpoint +CREATE UNIQUE INDEX "contract_boards_unique_idx" ON "contract_boards" USING btree ("player_id","kind","location_id","period_index");--> statement-breakpoint +CREATE INDEX "dispatches_due_idx" ON "dispatches" USING btree ("status","arrival_at");--> statement-breakpoint +CREATE INDEX "dispatches_player_status_idx" ON "dispatches" USING btree ("player_id","status");--> statement-breakpoint +CREATE INDEX "pilots_player_idx" ON "pilots" USING btree ("player_id");--> statement-breakpoint +CREATE INDEX "ships_player_idx" ON "ships" USING btree ("player_id"); \ No newline at end of file diff --git a/apps/server/drizzle/meta/0000_snapshot.json b/apps/server/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..bd3b42c --- /dev/null +++ b/apps/server/drizzle/meta/0000_snapshot.json @@ -0,0 +1,842 @@ +{ + "id": "1390d91d-5047-43d3-984f-06f82cb52ab5", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accepted_contracts": { + "name": "accepted_contracts", + "schema": "", + "columns": { + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contract_id": { + "name": "contract_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_index": { + "name": "period_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dispatch_id": { + "name": "dispatch_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "accepted_contracts_player_id_players_id_fk": { + "name": "accepted_contracts_player_id_players_id_fk", + "tableFrom": "accepted_contracts", + "tableTo": "players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "accepted_contracts_dispatch_id_dispatches_id_fk": { + "name": "accepted_contracts_dispatch_id_dispatches_id_fk", + "tableFrom": "accepted_contracts", + "tableTo": "dispatches", + "columnsFrom": [ + "dispatch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "accepted_contracts_player_id_contract_id_pk": { + "name": "accepted_contracts_player_id_contract_id_pk", + "columns": [ + "player_id", + "contract_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bays": { + "name": "bays", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "bays_player_location_idx": { + "name": "bays_player_location_idx", + "columns": [ + { + "expression": "player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bays_player_id_players_id_fk": { + "name": "bays_player_id_players_id_fk", + "tableFrom": "bays", + "tableTo": "players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "companies_player_id_players_id_fk": { + "name": "companies_player_id_players_id_fk", + "tableFrom": "companies", + "tableTo": "players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "companies_player_id_unique": { + "name": "companies_player_id_unique", + "nullsNotDistinct": false, + "columns": [ + "player_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contract_boards": { + "name": "contract_boards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "board_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_index": { + "name": "period_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "contracts": { + "name": "contracts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "contract_boards_unique_idx": { + "name": "contract_boards_unique_idx", + "columns": [ + { + "expression": "player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contract_boards_player_id_players_id_fk": { + "name": "contract_boards_player_id_players_id_fk", + "tableFrom": "contract_boards", + "tableTo": "players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discovered_locations": { + "name": "discovered_locations", + "schema": "", + "columns": { + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discovered_at": { + "name": "discovered_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "discovered_locations_player_id_players_id_fk": { + "name": "discovered_locations_player_id_players_id_fk", + "tableFrom": "discovered_locations", + "tableTo": "players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discovered_locations_player_id_location_id_pk": { + "name": "discovered_locations_player_id_location_id_pk", + "columns": [ + "player_id", + "location_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dispatches": { + "name": "dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ship_id": { + "name": "ship_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pilot_id": { + "name": "pilot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tutorial": { + "name": "tutorial", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "record": { + "name": "record", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "arrival_at": { + "name": "arrival_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "dispatch_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'in_flight'" + }, + "resolution": { + "name": "resolution", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "dispatches_due_idx": { + "name": "dispatches_due_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "arrival_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dispatches_player_status_idx": { + "name": "dispatches_player_status_idx", + "columns": [ + { + "expression": "player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dispatches_player_id_players_id_fk": { + "name": "dispatches_player_id_players_id_fk", + "tableFrom": "dispatches", + "tableTo": "players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dispatches_ship_id_ships_id_fk": { + "name": "dispatches_ship_id_ships_id_fk", + "tableFrom": "dispatches", + "tableTo": "ships", + "columnsFrom": [ + "ship_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "dispatches_pilot_id_pilots_id_fk": { + "name": "dispatches_pilot_id_pilots_id_fk", + "tableFrom": "dispatches", + "tableTo": "pilots", + "columnsFrom": [ + "pilot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pilots": { + "name": "pilots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_avatar": { + "name": "is_avatar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "xp": { + "name": "xp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skill_points_spent": { + "name": "skill_points_spent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "assigned_ship_id": { + "name": "assigned_ship_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "activity_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pilots_player_idx": { + "name": "pilots_player_idx", + "columns": [ + { + "expression": "player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pilots_player_id_players_id_fk": { + "name": "pilots_player_id_players_id_fk", + "tableFrom": "pilots", + "tableTo": "players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pilots_assigned_ship_id_ships_id_fk": { + "name": "pilots_assigned_ship_id_ships_id_fk", + "tableFrom": "pilots", + "tableTo": "ships", + "columnsFrom": [ + "assigned_ship_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pilots_assigned_ship_id_unique": { + "name": "pilots_assigned_ship_id_unique", + "nullsNotDistinct": false, + "columns": [ + "assigned_ship_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.players": { + "name": "players", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "world_seed": { + "name": "world_seed", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tutorial_active": { + "name": "tutorial_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "loaner_location_id": { + "name": "loaner_location_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ships": { + "name": "ships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "activity_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ships_player_idx": { + "name": "ships_player_idx", + "columns": [ + { + "expression": "player_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ships_player_id_players_id_fk": { + "name": "ships_player_id_players_id_fk", + "tableFrom": "ships", + "tableTo": "players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.activity_status": { + "name": "activity_status", + "schema": "public", + "values": [ + "idle", + "in_flight" + ] + }, + "public.board_kind": { + "name": "board_kind", + "schema": "public", + "values": [ + "bay", + "tutorial" + ] + }, + "public.dispatch_status": { + "name": "dispatch_status", + "schema": "public", + "values": [ + "in_flight", + "resolved" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/drizzle/meta/_journal.json b/apps/server/drizzle/meta/_journal.json new file mode 100644 index 0000000..e5b7d73 --- /dev/null +++ b/apps/server/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1790991444530, + "tag": "0000_init", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 41855f0..a996f00 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -8,16 +8,22 @@ "scripts": { "dev": "tsx watch src/main.ts", "start": "tsx src/main.ts", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "db:generate": "drizzle-kit generate", + "db:migrate": "tsx src/db/migrate-cli.ts" }, "dependencies": { "@deadhead/content": "workspace:*", "@deadhead/schema": "workspace:*", "@deadhead/sim": "workspace:*", + "drizzle-orm": "^0.45.3", "fastify": "^5.12.5", + "pg": "^8.23.1", "zod": "^4.6.5" }, "devDependencies": { + "@types/pg": "^8.23.1", + "drizzle-kit": "^0.31.11", "tsx": "^4.23.15" } } diff --git a/apps/server/src/db/client.ts b/apps/server/src/db/client.ts new file mode 100644 index 0000000..d4cde16 --- /dev/null +++ b/apps/server/src/db/client.ts @@ -0,0 +1,16 @@ +import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; +import pg from "pg"; +import * as schema from "./schema"; + +export type Db = NodePgDatabase; + +export interface Database { + db: Db; + pool: pg.Pool; + close(): Promise; +} + +export function createDatabase(url: string): Database { + const pool = new pg.Pool({ connectionString: url }); + return { db: drizzle(pool, { schema }), pool, close: () => pool.end() }; +} diff --git a/apps/server/src/db/migrate-cli.ts b/apps/server/src/db/migrate-cli.ts new file mode 100644 index 0000000..c283073 --- /dev/null +++ b/apps/server/src/db/migrate-cli.ts @@ -0,0 +1,15 @@ +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { runMigrations } from "./migrate"; + +// `pnpm --filter @deadhead/server db:migrate` migrates DATABASE_URL. +const envFile = fileURLToPath(new URL("../../../../.env", import.meta.url)); +if (existsSync(envFile)) process.loadEnvFile(envFile); + +const url = process.env.DATABASE_URL; +if (!url) { + console.error("DATABASE_URL is not set"); + process.exit(1); +} +await runMigrations(url); +console.log("Migrations applied"); diff --git a/apps/server/src/db/migrate.ts b/apps/server/src/db/migrate.ts new file mode 100644 index 0000000..e228d51 --- /dev/null +++ b/apps/server/src/db/migrate.ts @@ -0,0 +1,15 @@ +import { fileURLToPath } from "node:url"; +import { migrate } from "drizzle-orm/node-postgres/migrator"; +import { createDatabase } from "./client"; + +export const MIGRATIONS_FOLDER = fileURLToPath(new URL("../../drizzle", import.meta.url)); + +/** Applies pending migrations. Already-applied ones are skipped, so it is safe to rerun. */ +export async function runMigrations(url: string): Promise { + const database = createDatabase(url); + try { + await migrate(database.db, { migrationsFolder: MIGRATIONS_FOLDER }); + } finally { + await database.close(); + } +} diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts new file mode 100644 index 0000000..ba16d3c --- /dev/null +++ b/apps/server/src/db/schema.ts @@ -0,0 +1,174 @@ +import type { Contract, DispatchRecord, Resolution } from "@deadhead/sim"; +import { + bigint, + boolean, + index, + integer, + jsonb, + pgEnum, + pgTable, + primaryKey, + text, + uniqueIndex, +} from "drizzle-orm/pg-core"; + +// All times are Unix milliseconds (bigint), matching sim and the API. +const time = (name: string) => bigint(name, { mode: "number" }); + +export const activityStatus = pgEnum("activity_status", ["idle", "in_flight"]); +export const dispatchStatus = pgEnum("dispatch_status", ["in_flight", "resolved"]); +export const boardKind = pgEnum("board_kind", ["bay", "tutorial"]); + +export const players = pgTable("players", { + id: text("id").primaryKey(), + /** 32-bit unsigned seed for boards, dispatches and names (decision 0.5.10). */ + worldSeed: bigint("world_seed", { mode: "number" }).notNull(), + tutorialActive: boolean("tutorial_active").notNull().default(true), + /** Where the tutorial loaner is (decision 0.5.26); null once the tutorial ends. */ + loanerLocationId: text("loaner_location_id"), + createdAt: time("created_at").notNull(), +}); + +export const companies = pgTable("companies", { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .unique() + .references(() => players.id, { onDelete: "cascade" }), + name: text("name").notNull(), + /** Row-locked by every endpoint that changes money or fleet state (Step 6.14). */ + credits: bigint("credits", { mode: "number" }).notNull(), +}); + +export const ships = pgTable( + "ships", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + modelId: text("model_id").notNull(), + name: text("name").notNull(), + status: activityStatus("status").notNull().default("idle"), + /** Null while in flight. */ + locationId: text("location_id"), + createdAt: time("created_at").notNull(), + }, + (t) => [index("ships_player_idx").on(t.playerId)], +); + +export const pilots = pgTable( + "pilots", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + name: text("name").notNull(), + isAvatar: boolean("is_avatar").notNull().default(false), + xp: integer("xp").notNull().default(0), + skills: jsonb("skills").$type>().notNull().default({}), + skillPointsSpent: integer("skill_points_spent").notNull().default(0), + /** One pilot per ship (decision 0.5.2). */ + assignedShipId: text("assigned_ship_id") + .unique() + .references(() => ships.id, { onDelete: "set null" }), + /** Only unassigned pilots store a location (decision 0.5.27). */ + locationId: text("location_id"), + status: activityStatus("status").notNull().default("idle"), + createdAt: time("created_at").notNull(), + }, + (t) => [index("pilots_player_idx").on(t.playerId)], +); + +export const bays = pgTable( + "bays", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + locationId: text("location_id").notNull(), + createdAt: time("created_at").notNull(), + }, + (t) => [uniqueIndex("bays_player_location_idx").on(t.playerId, t.locationId)], +); + +export const discoveredLocations = pgTable( + "discovered_locations", + { + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + locationId: text("location_id").notNull(), + discoveredAt: time("discovered_at").notNull(), + }, + (t) => [primaryKey({ columns: [t.playerId, t.locationId] })], +); + +/** + * One stored board per bay per period, and one tutorial board per period per + * loaner location (decisions 0.5.5 and 0.5.21). `locationId` is the bay's or + * the loaner's location. + */ +export const contractBoards = pgTable( + "contract_boards", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + kind: boardKind("kind").notNull(), + locationId: text("location_id").notNull(), + periodIndex: integer("period_index").notNull(), + contracts: jsonb("contracts").$type().notNull(), + createdAt: time("created_at").notNull(), + }, + (t) => [ + uniqueIndex("contract_boards_unique_idx").on(t.playerId, t.kind, t.locationId, t.periodIndex), + ], +); + +export const dispatches = pgTable( + "dispatches", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + /** Null for a tutorial run (the loaner is not a ship row). */ + shipId: text("ship_id").references(() => ships.id, { onDelete: "set null" }), + pilotId: text("pilot_id") + .notNull() + .references(() => pilots.id, { onDelete: "cascade" }), + tutorial: boolean("tutorial").notNull(), + /** The full sim record, including a snapshot of the contract. */ + record: jsonb("record").$type().notNull(), + arrivalAt: time("arrival_at").notNull(), + status: dispatchStatus("status").notNull().default("in_flight"), + /** Applied effects, kept for the "while you were away" summary. */ + resolution: jsonb("resolution").$type(), + resolvedAt: time("resolved_at"), + acknowledgedAt: time("acknowledged_at"), + }, + (t) => [ + index("dispatches_due_idx").on(t.status, t.arrivalAt), + index("dispatches_player_status_idx").on(t.playerId, t.status), + ], +); + +export const acceptedContracts = pgTable( + "accepted_contracts", + { + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + contractId: text("contract_id").notNull(), + periodIndex: integer("period_index").notNull(), + dispatchId: text("dispatch_id") + .notNull() + .references(() => dispatches.id, { onDelete: "cascade" }), + acceptedAt: time("accepted_at").notNull(), + }, + (t) => [primaryKey({ columns: [t.playerId, t.contractId] })], +); diff --git a/apps/server/test/migrations.test.ts b/apps/server/test/migrations.test.ts new file mode 100644 index 0000000..cd01aaf --- /dev/null +++ b/apps/server/test/migrations.test.ts @@ -0,0 +1,52 @@ +import pg from "pg"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { runMigrations } from "../src/db/migrate"; +import { testConfig } from "./helpers"; + +const url = testConfig().databaseUrl; +const client = new pg.Client({ connectionString: url }); + +const tables = async () => + ( + await client.query<{ table_name: string }>( + "select table_name from information_schema.tables where table_schema = 'public' order by table_name", + ) + ).rows.map((r) => r.table_name); + +const appliedMigrations = async () => + Number( + (await client.query<{ n: string }>("select count(*) as n from drizzle.__drizzle_migrations")) + .rows[0]!.n, + ); + +describe("database migrations", () => { + beforeAll(async () => { + await client.connect(); + // Start from an empty database. + await client.query("drop schema if exists drizzle cascade"); + await client.query("drop schema public cascade"); + await client.query("create schema public"); + }); + afterAll(() => client.end()); + + it("creates every table on an empty database", async () => { + await runMigrations(url); + expect(await tables()).toEqual([ + "accepted_contracts", + "bays", + "companies", + "contract_boards", + "discovered_locations", + "dispatches", + "pilots", + "players", + "ships", + ]); + }); + + it("is a no-op when run a second time", async () => { + const before = { tables: await tables(), migrations: await appliedMigrations() }; + await runMigrations(url); + expect({ tables: await tables(), migrations: await appliedMigrations() }).toEqual(before); + }); +}); diff --git a/apps/server/vitest.config.ts b/apps/server/vitest.config.ts new file mode 100644 index 0000000..1d4c5f8 --- /dev/null +++ b/apps/server/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineProject } from "vitest/config"; + +// Integration tests share one test database, so test files run one at a time. +export default defineProject({ + test: { + fileParallelism: false, + }, +}); diff --git a/memory-bank/architecture.md b/memory-bank/architecture.md index 7763150..e3878b8 100644 --- a/memory-bank/architecture.md +++ b/memory-bank/architecture.md @@ -58,3 +58,200 @@ The `routes` catalog is the single source for both server and client. Each entry | `unassignPilot` | `POST /api/pilots/:pilotId/unassign` | `pilotId` | `{ pilot }` | yes | | `spendSkillPoint` | `POST /api/pilots/:pilotId/skills` | `pilotId`, `skillId` | `{ pilot }` | yes | | `acknowledgeSummaries` | `POST /api/summaries/acknowledge` | `dispatchIds` | `{ acknowledged }` | yes | + +## Database (`apps/server/src/db`) + +Postgres via Drizzle (`pg` driver). Migrations are SQL files in `apps/server/drizzle/`, generated with `pnpm --filter @deadhead/server db:generate` and applied with `db:migrate` (uses `DATABASE_URL`; rerunning is a no-op). Every game table hangs off `players.id`; times are bigint Unix ms. + +| Table | Purpose | Key constraints | +|---|---|---| +| `players` | World seed, tutorial flag, loaner location | pk `id` | +| `companies` | Name and credits (the row locked for money changes) | unique `player_id` | +| `ships` | Owned ships: model, name, status, location (null in flight) | — | +| `pilots` | XP, skills (jsonb), points spent, assignment, location (unassigned only), status | unique `assigned_ship_id` (one pilot per ship) | +| `bays` | Leased bays | unique (`player_id`, `location_id`) | +| `discovered_locations` | Discovered location ids | pk (`player_id`, `location_id`) | +| `contract_boards` | Stored bay boards and tutorial boards per period (jsonb contracts) | unique (`player_id`, `kind`, `location_id`, `period_index`) | +| `dispatches` | Full sim record (jsonb, with contract snapshot), status, arrival, resolution (jsonb), resolved-at, acknowledged-at | index (`status`, `arrival_at`), (`player_id`, `status`) | +| `accepted_contracts` | Contracts taken from a board, linked to their dispatch | pk (`player_id`, `contract_id`) | + +### Drizzle schema + +Copied from `apps/server/src/db/schema.ts`; update both together. + +```ts +import type { Contract, DispatchRecord, Resolution } from "@deadhead/sim"; +import { + bigint, + boolean, + index, + integer, + jsonb, + pgEnum, + pgTable, + primaryKey, + text, + uniqueIndex, +} from "drizzle-orm/pg-core"; + +// All times are Unix milliseconds (bigint), matching sim and the API. +const time = (name: string) => bigint(name, { mode: "number" }); + +export const activityStatus = pgEnum("activity_status", ["idle", "in_flight"]); +export const dispatchStatus = pgEnum("dispatch_status", ["in_flight", "resolved"]); +export const boardKind = pgEnum("board_kind", ["bay", "tutorial"]); + +export const players = pgTable("players", { + id: text("id").primaryKey(), + /** 32-bit unsigned seed for boards, dispatches and names (decision 0.5.10). */ + worldSeed: bigint("world_seed", { mode: "number" }).notNull(), + tutorialActive: boolean("tutorial_active").notNull().default(true), + /** Where the tutorial loaner is (decision 0.5.26); null once the tutorial ends. */ + loanerLocationId: text("loaner_location_id"), + createdAt: time("created_at").notNull(), +}); + +export const companies = pgTable("companies", { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .unique() + .references(() => players.id, { onDelete: "cascade" }), + name: text("name").notNull(), + /** Row-locked by every endpoint that changes money or fleet state (Step 6.14). */ + credits: bigint("credits", { mode: "number" }).notNull(), +}); + +export const ships = pgTable( + "ships", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + modelId: text("model_id").notNull(), + name: text("name").notNull(), + status: activityStatus("status").notNull().default("idle"), + /** Null while in flight. */ + locationId: text("location_id"), + createdAt: time("created_at").notNull(), + }, + (t) => [index("ships_player_idx").on(t.playerId)], +); + +export const pilots = pgTable( + "pilots", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + name: text("name").notNull(), + isAvatar: boolean("is_avatar").notNull().default(false), + xp: integer("xp").notNull().default(0), + skills: jsonb("skills").$type>().notNull().default({}), + skillPointsSpent: integer("skill_points_spent").notNull().default(0), + /** One pilot per ship (decision 0.5.2). */ + assignedShipId: text("assigned_ship_id") + .unique() + .references(() => ships.id, { onDelete: "set null" }), + /** Only unassigned pilots store a location (decision 0.5.27). */ + locationId: text("location_id"), + status: activityStatus("status").notNull().default("idle"), + createdAt: time("created_at").notNull(), + }, + (t) => [index("pilots_player_idx").on(t.playerId)], +); + +export const bays = pgTable( + "bays", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + locationId: text("location_id").notNull(), + createdAt: time("created_at").notNull(), + }, + (t) => [uniqueIndex("bays_player_location_idx").on(t.playerId, t.locationId)], +); + +export const discoveredLocations = pgTable( + "discovered_locations", + { + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + locationId: text("location_id").notNull(), + discoveredAt: time("discovered_at").notNull(), + }, + (t) => [primaryKey({ columns: [t.playerId, t.locationId] })], +); + +/** + * One stored board per bay per period, and one tutorial board per period per + * loaner location (decisions 0.5.5 and 0.5.21). `locationId` is the bay's or + * the loaner's location. + */ +export const contractBoards = pgTable( + "contract_boards", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + kind: boardKind("kind").notNull(), + locationId: text("location_id").notNull(), + periodIndex: integer("period_index").notNull(), + contracts: jsonb("contracts").$type().notNull(), + createdAt: time("created_at").notNull(), + }, + (t) => [ + uniqueIndex("contract_boards_unique_idx").on(t.playerId, t.kind, t.locationId, t.periodIndex), + ], +); + +export const dispatches = pgTable( + "dispatches", + { + id: text("id").primaryKey(), + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + /** Null for a tutorial run (the loaner is not a ship row). */ + shipId: text("ship_id").references(() => ships.id, { onDelete: "set null" }), + pilotId: text("pilot_id") + .notNull() + .references(() => pilots.id, { onDelete: "cascade" }), + tutorial: boolean("tutorial").notNull(), + /** The full sim record, including a snapshot of the contract. */ + record: jsonb("record").$type().notNull(), + arrivalAt: time("arrival_at").notNull(), + status: dispatchStatus("status").notNull().default("in_flight"), + /** Applied effects, kept for the "while you were away" summary. */ + resolution: jsonb("resolution").$type(), + resolvedAt: time("resolved_at"), + acknowledgedAt: time("acknowledged_at"), + }, + (t) => [ + index("dispatches_due_idx").on(t.status, t.arrivalAt), + index("dispatches_player_status_idx").on(t.playerId, t.status), + ], +); + +export const acceptedContracts = pgTable( + "accepted_contracts", + { + playerId: text("player_id") + .notNull() + .references(() => players.id, { onDelete: "cascade" }), + contractId: text("contract_id").notNull(), + periodIndex: integer("period_index").notNull(), + dispatchId: text("dispatch_id") + .notNull() + .references(() => dispatches.id, { onDelete: "cascade" }), + acceptedAt: time("accepted_at").notNull(), + }, + (t) => [primaryKey({ columns: [t.playerId, t.contractId] })], +); +``` diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 89884ff..4f259e3 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -47,7 +47,7 @@ - [x] **Step 5.1 — Fastify skeleton.** `apps/server`: Zod-validated env config (`src/config.ts`), `buildApp` with pino JSON logging and `GET /api/health` (`src/app.ts`), entry point `src/main.ts` (loads repo-root `.env` if present). `pnpm --filter @deadhead/server dev|start` run it with tsx. Integration test starts the app on an ephemeral port and calls health over HTTP. - [x] **Step 5.2 — Server time endpoint.** `GET /api/time` returns `{ realTime, gameTime }` from an injectable `Clock` (`src/clock.ts`, passed via `buildApp(config, { clock })`). -- [ ] Step 5.3 — Database schema and migrations +- [x] **Step 5.3 — Database schema and migrations.** Drizzle schema (`apps/server/src/db/schema.ts`, copied into `architecture.md`), first migration `drizzle/0000_init.sql`, `db:generate` / `db:migrate` scripts. Migration test runs against an emptied test database twice. CI now has a Postgres 18 service; server tests run one file at a time. - [ ] Step 5.4 — Test database harness - [ ] Step 5.5 — Guest accounts - [ ] Step 5.6 — Request validation from the route catalog diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a666e08..b712be6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,13 +50,25 @@ importers: '@deadhead/sim': specifier: workspace:* version: link:../../packages/sim + drizzle-orm: + specifier: ^0.45.3 + version: 0.45.3(@types/pg@8.23.1)(pg@8.23.1) fastify: specifier: ^5.12.5 version: 5.12.5 + pg: + specifier: ^8.23.1 + version: 8.23.1 zod: specifier: ^4.6.5 version: 4.6.5 devDependencies: + '@types/pg': + specifier: ^8.23.1 + version: 8.23.1 + drizzle-kit: + specifier: ^0.31.11 + version: 0.31.11 tsx: specifier: ^4.23.15 version: 4.23.15 @@ -116,156 +128,455 @@ packages: '@cacheable/utils@2.5.0': resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.hirok.io' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.28.2': resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.28.2': resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.28.2': resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.28.2': resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.28.2': resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.28.2': resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.28.2': resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.28.2': resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.28.2': resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.28.2': resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.28.2': resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.28.2': resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.28.2': resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.28.2': resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.28.2': resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.28.2': resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.28.2': resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.28.2': resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} engines: {node: '>=18'} @@ -491,6 +802,9 @@ packages: '@types/node@24.19.1': resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==} + '@types/pg@8.23.1': + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} + '@typescript-eslint/eslint-plugin@8.71.0': resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -633,6 +947,9 @@ packages: resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + cacheable@2.5.0: resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} @@ -668,9 +985,118 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + drizzle-kit@0.31.11: + resolution: {integrity: sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg==} + hasBin: true + + drizzle-orm@0.45.3: + resolution: {integrity: sha512-CAloER21cDdcgZ1OmjrZX82EeRNsdP+y0onX/eyoYDaYzki/adrAa91lzM5jWh7zZPK4qUbw/6LGv9J+A+uizA==} + 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' + '@netlify/db': '>=0.4.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' + prisma: '*' + 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 + '@netlify/db': + 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 + es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} @@ -796,6 +1222,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -993,6 +1422,40 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + pg-cloudflare@1.4.1: + resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==} + + pg-connection-string@2.14.1: + resolution: {integrity: sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.1: + resolution: {integrity: sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.1: + resolution: {integrity: sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1014,6 +1477,22 @@ packages: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1047,6 +1526,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + ret@0.5.0: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} @@ -1097,6 +1579,13 @@ packages: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -1259,6 +1748,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -1295,81 +1788,237 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 + '@drizzle-team/brocli@0.10.2': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.3 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.28.2': optional: true + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm64@0.28.2': optional: true + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-arm@0.28.2': optional: true + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/android-x64@0.28.2': optional: true + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.28.2': optional: true + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.28.2': optional: true + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.28.2': optional: true + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.28.2': optional: true + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.28.2': optional: true + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-arm@0.28.2': optional: true + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-ia32@0.28.2': optional: true + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-loong64@0.28.2': optional: true + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.28.2': optional: true + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.28.2': optional: true + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.28.2': optional: true + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-s390x@0.28.2': optional: true + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.28.2': optional: true + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.28.2': optional: true + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.28.2': optional: true + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.28.2': optional: true + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.28.2': optional: true + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/openharmony-arm64@0.28.2': optional: true + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.28.2': optional: true + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.28.2': optional: true + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-ia32@0.28.2': optional: true + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.28.2': optional: true @@ -1531,6 +2180,12 @@ snapshots: dependencies: undici-types: 7.24.6 + '@types/pg@8.23.1': + dependencies: + '@types/node': 24.19.1 + pg-protocol: 1.16.1 + pg-types: 2.2.0 + '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0)(typescript@6.0.3))(eslint@10.12.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -1700,6 +2355,8 @@ snapshots: dependencies: balanced-match: 4.0.4 + buffer-from@1.1.2: {} + cacheable@2.5.0: dependencies: '@cacheable/memory': 2.2.0 @@ -1728,8 +2385,74 @@ snapshots: detect-libc@2.1.2: {} + drizzle-kit@0.31.11: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.23.15 + + drizzle-orm@0.45.3(@types/pg@8.23.1)(pg@8.23.1): + optionalDependencies: + '@types/pg': 8.23.1 + pg: 8.23.1 + es-module-lexer@2.3.2: {} + esbuild@0.18.20: + 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 + + esbuild@0.25.12: + 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 + esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -1912,6 +2635,10 @@ snapshots: fsevents@2.3.3: optional: true + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -2067,6 +2794,41 @@ snapshots: path-key@3.1.1: {} + pg-cloudflare@1.4.1: + optional: true + + pg-connection-string@2.14.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.1): + dependencies: + pg: 8.23.1 + + pg-protocol@1.16.1: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.1: + dependencies: + pg-connection-string: 2.14.1 + pg-pool: 3.14.0(pg@8.23.1) + pg-protocol: 1.16.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@4.0.7: {} @@ -2097,6 +2859,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.2 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prettier@3.9.9: {} @@ -2117,6 +2889,8 @@ snapshots: require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} + ret@0.5.0: {} reusify@1.1.0: {} @@ -2168,6 +2942,13 @@ snapshots: source-map-js@1.2.2: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + split2@4.2.0: {} std-env@4.3.0: {} @@ -2265,6 +3046,8 @@ snapshots: word-wrap@1.2.5: {} + xtend@4.0.2: {} + yocto-queue@0.1.0: {} zod@4.6.5: {}