From 7010f01feffc55acc7ab064cd755ce171cac9abb Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Wed, 13 May 2026 16:23:42 +0000 Subject: [PATCH] =?UTF-8?q?chore(schema):=20canonical=20remediation=20?= =?UTF-8?q?=E2=80=94=20chitty=5Fid,=20P/L/T/E/A,=20entities=20registry,=20?= =?UTF-8?q?R2=20ACL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greenfield canonical schema for Neon project steep-cloud-28172078. The DB was empty pre-migration; no production data to preserve. Stacked on Phase 1 Hono branch since canonical docs and Worker skeleton live there. Migrations (drizzle/): - 0001_init.sql — Drop-in canonical baseline. All entity tables get nullable chitty_id varchar UNIQUE, timestamptz created_at/updated_at, soft-delete deleted_at where applicable, GIN indexes on JSONB, and a shared set_updated_at trigger. Entity-type assignments annotated per table: users=P, assets=T, evidence=T, timeline_events=E, warranties=T, insurance_policies=T, legal_cases=E, ai_analysis_results=E. Borderline calls documented in COMMENT ON TABLE with canonical rationale. - 0002_entities_registry.sql — Canonical discovery/audit index with entity_type enum containing ALL FIVE P/L/T/E/A values (never omit A). Soft references only: per-service tables do NOT FK here. - 0003_r2_object_acl.sql — Replaces GCS objectStorage.ts ACL with a Neon-backed table. principal_chitty_id is a Person (P); evidence_id / asset_id scope grants to Thing (T) artifacts. docs/migrations/phase3-users-chittyid-migration.md — 5-step plan to promote users.chitty_id to PK once ChittyAuth-only cutover completes. Document only, not executed in this PR. shared/schema.ts — Rewritten to mirror DDL. All entity_type-bearing tables carry // @canon: chittycanon://gov/governance#core-types annotations. CANONICAL_ENTITY_TYPES tuple and CHITTY_ID_PATTERN regex include all five P/L/T/E/A; never omit A. Validated on Neon preview branch br-spring-star-aky6u1mc: - All 63 DDL statements executed cleanly via run_sql_transaction - Read-back confirms 11 tables, every entity table has chitty_id + created_at, entity_type enum = {P,L,T,E,A} - npm run check: zero shared/schema.ts errors (pre-existing errors in legacy server/* and client/* are unrelated) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../phase3-users-chittyid-migration.md | 111 ++++ drizzle/0001_init.sql | 279 ++++++++++ drizzle/0002_entities_registry.sql | 40 ++ drizzle/0003_r2_object_acl.sql | 36 ++ shared/schema.ts | 527 ++++++++++-------- 5 files changed, 761 insertions(+), 232 deletions(-) create mode 100644 docs/migrations/phase3-users-chittyid-migration.md create mode 100644 drizzle/0001_init.sql create mode 100644 drizzle/0002_entities_registry.sql create mode 100644 drizzle/0003_r2_object_acl.sql diff --git a/docs/migrations/phase3-users-chittyid-migration.md b/docs/migrations/phase3-users-chittyid-migration.md new file mode 100644 index 0000000..1e334d1 --- /dev/null +++ b/docs/migrations/phase3-users-chittyid-migration.md @@ -0,0 +1,111 @@ +# Phase 3 — Migrate users.id from uuid to canonical ChittyID + +**Status:** Planned, not executed. Document only. +**Canon:** chittycanon://gov/governance#core-types +**Target:** Neon project `steep-cloud-28172078` + +## Rationale + +Phase 1 (this PR) keeps `users.id` as a uuid PK and adds nullable `users.chitty_id`. This is +backward-compatible: existing Replit-Auth-emitted user records (if any are seeded from the +legacy stack) continue to work; new ChittyAuth flows populate `chitty_id` immediately. + +Phase 3 promotes `chitty_id` to the canonical PK and rewires all FK references. It is a +breaking change at the DB layer and MUST be coordinated with all consumers: + +- Worker `worker/src/auth.ts` (claims → user lookup) +- Express `server/storage.ts` (legacy) +- Any downstream service that reads `users.id` via shared Neon access + +## Pre-conditions + +1. ChittyAuth issues `sub = chitty_id` for every authenticated principal. +2. All existing users in production have `users.chitty_id IS NOT NULL`. +3. Worker no longer reads `users.id` — only `users.chitty_id`. +4. All FK tables (`assets.user_id`, `evidence.user_id`, etc.) have a `user_chitty_id varchar` + column shadowing the uuid (added in a prior, separate migration). + +## The 5-step migration + +### Step 1 — Backfill `user_chitty_id` on all FK tables + +```sql +BEGIN; +UPDATE assets a SET user_chitty_id = u.chitty_id FROM users u WHERE a.user_id = u.id; +UPDATE evidence e SET user_chitty_id = u.chitty_id FROM users u WHERE e.user_id = u.id; +UPDATE timeline_events t SET user_chitty_id = u.chitty_id FROM users u WHERE t.user_id = u.id; +UPDATE warranties w SET user_chitty_id = u.chitty_id FROM users u WHERE w.user_id = u.id; +UPDATE insurance_policies i SET user_chitty_id = u.chitty_id FROM users u WHERE i.user_id = u.id; +UPDATE legal_cases l SET user_chitty_id = u.chitty_id FROM users u WHERE l.user_id = u.id; + +-- Validate: zero NULLs +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM assets WHERE user_chitty_id IS NULL) + OR EXISTS (SELECT 1 FROM evidence WHERE user_chitty_id IS NULL) + OR EXISTS (SELECT 1 FROM timeline_events WHERE user_chitty_id IS NULL) + OR EXISTS (SELECT 1 FROM warranties WHERE user_chitty_id IS NULL) + OR EXISTS (SELECT 1 FROM insurance_policies WHERE user_chitty_id IS NULL) + OR EXISTS (SELECT 1 FROM legal_cases WHERE user_chitty_id IS NULL) + THEN RAISE EXCEPTION 'Backfill incomplete'; END IF; +END $$; +COMMIT; +``` + +### Step 2 — Add new FK constraints alongside legacy ones + +```sql +BEGIN; +ALTER TABLE assets ADD CONSTRAINT fk_assets_user_chitty FOREIGN KEY (user_chitty_id) REFERENCES users(chitty_id); +ALTER TABLE evidence ADD CONSTRAINT fk_evidence_user_chitty FOREIGN KEY (user_chitty_id) REFERENCES users(chitty_id); +ALTER TABLE timeline_events ADD CONSTRAINT fk_timeline_user_chitty FOREIGN KEY (user_chitty_id) REFERENCES users(chitty_id); +ALTER TABLE warranties ADD CONSTRAINT fk_warranties_user_chitty FOREIGN KEY (user_chitty_id) REFERENCES users(chitty_id); +ALTER TABLE insurance_policies ADD CONSTRAINT fk_insurance_user_chitty FOREIGN KEY (user_chitty_id) REFERENCES users(chitty_id); +ALTER TABLE legal_cases ADD CONSTRAINT fk_legal_cases_user_chitty FOREIGN KEY (user_chitty_id) REFERENCES users(chitty_id); +COMMIT; +``` + +### Step 3 — Cut over Worker code + +Deploy a Worker release that reads exclusively `user_chitty_id`. Old uuid `user_id` +columns remain populated but unread. **Bake time: minimum 7 days** to catch stragglers. + +### Step 4 — Drop legacy FK + uuid columns + +```sql +BEGIN; +ALTER TABLE assets DROP CONSTRAINT assets_user_id_fkey; +ALTER TABLE assets DROP COLUMN user_id; +ALTER TABLE evidence DROP CONSTRAINT evidence_user_id_fkey; +ALTER TABLE evidence DROP COLUMN user_id; +-- ...repeat for each table +ALTER TABLE users DROP CONSTRAINT users_pkey; +ALTER TABLE users ALTER COLUMN chitty_id SET NOT NULL; +ALTER TABLE users ADD CONSTRAINT users_pkey PRIMARY KEY (chitty_id); +ALTER TABLE users DROP COLUMN id; +COMMIT; +``` + +### Step 5 — Rename `user_chitty_id` → `user_id` everywhere + +```sql +BEGIN; +ALTER TABLE assets RENAME COLUMN user_chitty_id TO user_id; +ALTER TABLE evidence RENAME COLUMN user_chitty_id TO user_id; +ALTER TABLE timeline_events RENAME COLUMN user_chitty_id TO user_id; +ALTER TABLE warranties RENAME COLUMN user_chitty_id TO user_id; +ALTER TABLE insurance_policies RENAME COLUMN user_chitty_id TO user_id; +ALTER TABLE legal_cases RENAME COLUMN user_chitty_id TO user_id; +COMMIT; +``` + +## Rollback + +Any step is reversible until Step 4 commits. After Step 4, restore from the pre-step-4 +Neon branch (Neon's point-in-time restore retains 7 days by default; verify before +executing). + +## Validation + +After Step 5, every row in every per-service table MUST have `user_id` matching +`/^[0-9]{2}-[A-Z]-[0-9]{3}-[0-9]{4}-[PLTEA]-[0-9]{4}-[A-Z]-[0-9]$/` (the canonical +ChittyID regex including all five P/L/T/E/A types). diff --git a/drizzle/0001_init.sql b/drizzle/0001_init.sql new file mode 100644 index 0000000..b4fe416 --- /dev/null +++ b/drizzle/0001_init.sql @@ -0,0 +1,279 @@ +-- Migration 0001 — Initial canonical schema for ChittyAssets +-- Target: Neon project steep-cloud-28172078 (greenfield) +-- Canon: chittycanon://gov/governance#core-types (P/L/T/E/A) +-- +-- Entity-type assignments (annotated on app-layer entities table in 0002): +-- users -> P (Person): natural actor with agency +-- assets -> T (Thing): object without agency, asset/artifact +-- evidence -> T (Thing): document/artifact attached to an asset +-- timeline_events -> E (Event): occurrence in time +-- warranties -> T (Thing): contract-as-artifact +-- insurance_policies -> T (Thing): policy-document-as-artifact +-- legal_cases -> E (Event): proceeding with docket/status progression +-- ai_analysis_results -> E (Event): analysis run in time +-- +-- All ChittyID-bearing rows use chitty_id varchar in canonical format +-- VV-G-LLL-SSSS-T-YM-C-X. Phase 3 (see docs/migrations/phase3-users-chittyid-migration.md) +-- migrates users.id from uuid to chitty_id; this migration keeps uuid PKs for +-- per-service tables so a backward-compatible deploy is possible. + +BEGIN; + +-- ===================================================================== +-- Sessions (Replit Auth legacy; retained until ChittyAuth-only cutover) +-- ===================================================================== +CREATE TABLE IF NOT EXISTS sessions ( + sid varchar PRIMARY KEY, + sess jsonb NOT NULL, + expire timestamp NOT NULL +); +CREATE INDEX IF NOT EXISTS "IDX_session_expire" ON sessions (expire); +COMMENT ON TABLE sessions IS 'Express session store. Legacy; removed after ChittyAuth-only cutover.'; + +-- ===================================================================== +-- Users — Person (P) +-- ===================================================================== +CREATE TABLE IF NOT EXISTS users ( + id varchar PRIMARY KEY DEFAULT gen_random_uuid(), + chitty_id varchar UNIQUE, -- canonical Person ChittyID, populated by ChittyAuth + email varchar UNIQUE, + first_name varchar, + last_name varchar, + profile_image_url varchar, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +COMMENT ON TABLE users IS 'Person (P) — natural actor with agency. Canon: chittycanon://gov/governance#core-types'; +COMMENT ON COLUMN users.chitty_id IS 'Canonical ChittyID (Person, T=P). Format VV-G-LLL-SSSS-P-YM-C-X. Phase 3 promotes this to PK.'; +COMMENT ON COLUMN users.deleted_at IS 'GDPR soft-delete; audit trail preserved in ChittyLedger.'; + +-- ===================================================================== +-- Enums +-- ===================================================================== +CREATE TYPE asset_type AS ENUM ( + 'real_estate','vehicle','artwork','jewelry','electronics', + 'documents','business_assets','intellectual_property','other' +); +CREATE TYPE asset_status AS ENUM ( + 'active','disposed','lost','stolen','in_dispute','under_review' +); +CREATE TYPE verification_status AS ENUM ( + 'pending','verified','rejected','expired' +); +CREATE TYPE chitty_chain_status AS ENUM ( + 'draft','frozen','minted','settled','disputed' +); +CREATE TYPE evidence_type AS ENUM ( + 'receipt','contract','photo','video','insurance_document', + 'warranty','maintenance_record','legal_filing','correspondence','other' +); +CREATE TYPE timeline_event_type AS ENUM ( + 'acquisition','modification','maintenance','insurance_update', + 'valuation_change','location_change','status_change','evidence_added','other' +); +CREATE TYPE legal_case_status AS ENUM ( + 'active','settled','dismissed','pending','on_appeal' +); + +-- ===================================================================== +-- Assets — Thing (T) +-- ===================================================================== +CREATE TABLE IF NOT EXISTS assets ( + id varchar PRIMARY KEY DEFAULT gen_random_uuid(), + chitty_id varchar UNIQUE, -- canonical Thing ChittyID + chitty_id_v2 varchar UNIQUE, -- future Mod-97 Base32 + user_id varchar NOT NULL REFERENCES users(id), + name text NOT NULL, + description text, + asset_type asset_type NOT NULL, + status asset_status DEFAULT 'active', + purchase_price numeric(12,2), + current_value numeric(12,2), + purchase_date timestamptz, + location text, + serial_number varchar, + model varchar, + manufacturer varchar, + condition varchar, + trust_score numeric(3,1) DEFAULT 0.0, + blockchain_hash varchar, + block_number varchar, + ipfs_hash varchar, + freeze_timestamp timestamptz, + settlement_timestamp timestamptz, + minting_fee numeric(8,6), + verification_status verification_status DEFAULT 'pending', + chitty_chain_status chitty_chain_status DEFAULT 'draft', + tags text[], + metadata jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +CREATE INDEX IF NOT EXISTS idx_assets_user_id ON assets (user_id); +CREATE INDEX IF NOT EXISTS idx_assets_chitty_id ON assets (chitty_id); +CREATE INDEX IF NOT EXISTS idx_assets_metadata_gin ON assets USING GIN (metadata); +COMMENT ON TABLE assets IS 'Thing (T) — object without agency. The central artifact of ChittyAssets. Canon: chittycanon://gov/governance#core-types'; +COMMENT ON COLUMN assets.chitty_id IS 'Canonical ChittyID (Thing, T=T). Format VV-G-LLL-SSSS-T-YM-C-X.'; + +-- ===================================================================== +-- Evidence — Thing (T) +-- ===================================================================== +CREATE TABLE IF NOT EXISTS evidence ( + id varchar PRIMARY KEY DEFAULT gen_random_uuid(), + chitty_id varchar UNIQUE, + asset_id varchar NOT NULL REFERENCES assets(id), + user_id varchar NOT NULL REFERENCES users(id), + name text NOT NULL, + evidence_type evidence_type NOT NULL, + file_path text, + file_size integer, + mime_type varchar, + extracted_data jsonb, + ai_analysis jsonb, + blockchain_hash varchar, + verification_status verification_status DEFAULT 'pending', + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); +CREATE INDEX IF NOT EXISTS idx_evidence_asset_id ON evidence (asset_id); +CREATE INDEX IF NOT EXISTS idx_evidence_user_id ON evidence (user_id); +CREATE INDEX IF NOT EXISTS idx_evidence_extracted_data_gin ON evidence USING GIN (extracted_data); +COMMENT ON TABLE evidence IS 'Thing (T) — document/artifact attached to an asset. Canon: chittycanon://gov/governance#core-types'; + +-- ===================================================================== +-- Timeline events — Event (E) +-- ===================================================================== +CREATE TABLE IF NOT EXISTS timeline_events ( + id varchar PRIMARY KEY DEFAULT gen_random_uuid(), + chitty_id varchar UNIQUE, + asset_id varchar NOT NULL REFERENCES assets(id), + user_id varchar NOT NULL REFERENCES users(id), + event_type timeline_event_type NOT NULL, + title text NOT NULL, + description text, + event_date timestamptz NOT NULL, + related_evidence_id varchar REFERENCES evidence(id), + metadata jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_timeline_events_asset_id ON timeline_events (asset_id); +CREATE INDEX IF NOT EXISTS idx_timeline_events_event_date ON timeline_events (event_date); +COMMENT ON TABLE timeline_events IS 'Event (E) — occurrence in time against an asset. Append-only. Canon: chittycanon://gov/governance#core-types'; + +-- ===================================================================== +-- Warranties — Thing (T) +-- ===================================================================== +CREATE TABLE IF NOT EXISTS warranties ( + id varchar PRIMARY KEY DEFAULT gen_random_uuid(), + chitty_id varchar UNIQUE, + asset_id varchar NOT NULL REFERENCES assets(id), + user_id varchar NOT NULL REFERENCES users(id), + provider text NOT NULL, + type varchar NOT NULL, + start_date timestamptz NOT NULL, + end_date timestamptz NOT NULL, + coverage text, + terms text, + cost numeric(10,2), + is_active boolean DEFAULT true, + notification_sent boolean DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_warranties_asset_id ON warranties (asset_id); +CREATE INDEX IF NOT EXISTS idx_warranties_end_date ON warranties (end_date); +COMMENT ON TABLE warranties IS 'Thing (T) — contract-as-artifact granting coverage. The contract document is the Thing; coverage events are timeline_events. Canon: chittycanon://gov/governance#core-types'; + +-- ===================================================================== +-- Insurance policies — Thing (T) +-- ===================================================================== +CREATE TABLE IF NOT EXISTS insurance_policies ( + id varchar PRIMARY KEY DEFAULT gen_random_uuid(), + chitty_id varchar UNIQUE, + asset_id varchar NOT NULL REFERENCES assets(id), + user_id varchar NOT NULL REFERENCES users(id), + provider text NOT NULL, + policy_number varchar NOT NULL, + type varchar NOT NULL, + coverage_amount numeric(12,2), + premium numeric(10,2), + deductible numeric(10,2), + start_date timestamptz NOT NULL, + end_date timestamptz NOT NULL, + is_active boolean DEFAULT true, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_insurance_policies_asset_id ON insurance_policies (asset_id); +CREATE INDEX IF NOT EXISTS idx_insurance_policies_policy_number ON insurance_policies (policy_number); +COMMENT ON TABLE insurance_policies IS 'Thing (T) — policy document as artifact. Claims/coverage events are recorded as timeline_events. Canon: chittycanon://gov/governance#core-types'; + +-- ===================================================================== +-- Legal cases — Event (E) +-- ===================================================================== +CREATE TABLE IF NOT EXISTS legal_cases ( + id varchar PRIMARY KEY DEFAULT gen_random_uuid(), + chitty_id varchar UNIQUE, + user_id varchar NOT NULL REFERENCES users(id), + case_number varchar, + title text NOT NULL, + description text, + status legal_case_status DEFAULT 'active', + court text, + judge text, + filing_date timestamptz, + next_hearing timestamptz, + related_assets text[], + attorneys jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_legal_cases_user_id ON legal_cases (user_id); +CREATE INDEX IF NOT EXISTS idx_legal_cases_status ON legal_cases (status); +COMMENT ON TABLE legal_cases IS 'Event (E) — legal proceeding with docket/status progression. Canon: chittycanon://gov/governance#core-types'; + +-- ===================================================================== +-- AI analysis results — Event (E) +-- ===================================================================== +CREATE TABLE IF NOT EXISTS ai_analysis_results ( + id varchar PRIMARY KEY DEFAULT gen_random_uuid(), + chitty_id varchar UNIQUE, + evidence_id varchar NOT NULL REFERENCES evidence(id), + analysis_type varchar NOT NULL, + confidence numeric(3,2), + results jsonb NOT NULL, + processing_time integer, + model_used varchar, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_ai_analysis_evidence_id ON ai_analysis_results (evidence_id); +CREATE INDEX IF NOT EXISTS idx_ai_analysis_results_gin ON ai_analysis_results USING GIN (results); +COMMENT ON TABLE ai_analysis_results IS 'Event (E) — AI analysis run in time against an evidence artifact. Append-only. Canon: chittycanon://gov/governance#core-types'; + +-- ===================================================================== +-- updated_at trigger +-- ===================================================================== +CREATE OR REPLACE FUNCTION set_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DO $$ +DECLARE t text; +BEGIN + FOREACH t IN ARRAY ARRAY['users','assets','evidence','warranties','insurance_policies','legal_cases'] + LOOP + EXECUTE format( + 'DROP TRIGGER IF EXISTS trg_%1$s_updated_at ON %1$s; + CREATE TRIGGER trg_%1$s_updated_at BEFORE UPDATE ON %1$s + FOR EACH ROW EXECUTE FUNCTION set_updated_at();', t); + END LOOP; +END $$; + +COMMIT; diff --git a/drizzle/0002_entities_registry.sql b/drizzle/0002_entities_registry.sql new file mode 100644 index 0000000..505460f --- /dev/null +++ b/drizzle/0002_entities_registry.sql @@ -0,0 +1,40 @@ +-- Migration 0002 — Canonical entities registry +-- Soft references only: per-service tables do NOT FK to this registry. +-- The registry is a discovery/audit index, populated by app-layer triggers +-- or batch reconciliation. Schema authority is per-table. +-- Canon: chittycanon://gov/governance#core-types + +BEGIN; + +CREATE TYPE entity_type AS ENUM ('P','L','T','E','A'); +COMMENT ON TYPE entity_type IS + 'Canonical ChittyOS entity types per chittycanon://gov/governance#core-types: ' + 'P=Person, L=Location, T=Thing, E=Event, A=Authority. All five MUST be present; never omit A.'; + +CREATE TABLE IF NOT EXISTS entities ( + chitty_id varchar PRIMARY KEY, + entity_type entity_type NOT NULL, + source_table varchar NOT NULL, + source_id varchar NOT NULL, + display_name text, + metadata jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz, + UNIQUE (source_table, source_id) +); +CREATE INDEX IF NOT EXISTS idx_entities_type ON entities (entity_type); +CREATE INDEX IF NOT EXISTS idx_entities_source ON entities (source_table, source_id); +CREATE INDEX IF NOT EXISTS idx_entities_metadata_gin ON entities USING GIN (metadata); + +COMMENT ON TABLE entities IS + 'Canonical discovery/audit index of all ChittyID-bearing entities in this service. ' + 'Soft-reference only: per-service tables do NOT FK here. Populated by app layer.'; +COMMENT ON COLUMN entities.entity_type IS + '@canon: chittycanon://gov/governance#core-types — one of P/L/T/E/A. Never include "Entity" as a value (circular).'; + +CREATE TRIGGER trg_entities_updated_at +BEFORE UPDATE ON entities +FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +COMMIT; diff --git a/drizzle/0003_r2_object_acl.sql b/drizzle/0003_r2_object_acl.sql new file mode 100644 index 0000000..465494f --- /dev/null +++ b/drizzle/0003_r2_object_acl.sql @@ -0,0 +1,36 @@ +-- Migration 0003 — R2 object ACL +-- Replaces Google Cloud Storage objectStorage.ts ACL with a Neon-backed table. +-- File storage path is the R2 object key; ACL governs read/write per ChittyID. +-- Canon: chittycanon://gov/governance#core-types +-- This table itself is operational metadata (not an entity); rows describe +-- access grants from Person (P) principals onto Thing (T) artifacts. + +BEGIN; + +CREATE TYPE r2_acl_permission AS ENUM ('read','write','owner'); + +CREATE TABLE IF NOT EXISTS r2_object_acl ( + id varchar PRIMARY KEY DEFAULT gen_random_uuid(), + bucket varchar NOT NULL, + object_key text NOT NULL, + principal_chitty_id varchar NOT NULL, -- Person (P) ChittyID — soft ref to users.chitty_id + permission r2_acl_permission NOT NULL, + granted_by_chitty_id varchar, -- Person (P) ChittyID — granting principal + evidence_id varchar REFERENCES evidence(id), -- nullable: scoped to an evidence artifact + asset_id varchar REFERENCES assets(id), -- nullable: scoped to an asset + expires_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + revoked_at timestamptz, + UNIQUE (bucket, object_key, principal_chitty_id, permission) +); +CREATE INDEX IF NOT EXISTS idx_r2_acl_principal ON r2_object_acl (principal_chitty_id); +CREATE INDEX IF NOT EXISTS idx_r2_acl_object ON r2_object_acl (bucket, object_key); +CREATE INDEX IF NOT EXISTS idx_r2_acl_evidence ON r2_object_acl (evidence_id); +CREATE INDEX IF NOT EXISTS idx_r2_acl_asset ON r2_object_acl (asset_id); + +COMMENT ON TABLE r2_object_acl IS + 'Access control for R2 objects. Replaces GCS ACL. principal_chitty_id is a Person (P) ChittyID; ' + 'evidence_id/asset_id scope the grant to a Thing (T) artifact. Soft ref to users.chitty_id (no FK ' + 'because users.chitty_id is nullable during Phase 3 transition).'; + +COMMIT; diff --git a/shared/schema.ts b/shared/schema.ts index cec7041..96eacfb 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -1,4 +1,12 @@ -import { sql } from 'drizzle-orm'; +// @canon: chittycanon://gov/governance#core-types — P/L/T/E/A entity types +// +// ChittyAssets canonical Drizzle schema for Neon project steep-cloud-28172078. +// Mirrors drizzle/0001_init.sql, drizzle/0002_entities_registry.sql, +// drizzle/0003_r2_object_acl.sql. Annotations on each table indicate its +// canonical entity type. See docs/migrations/phase3-users-chittyid-migration.md +// for the planned promotion of users.chitty_id to PK. + +import { sql, relations } from 'drizzle-orm'; import { index, jsonb, @@ -6,204 +14,291 @@ import { timestamp, varchar, text, - decimal, + numeric, integer, boolean, pgEnum, -} from "drizzle-orm/pg-core"; -import { createInsertSchema } from "drizzle-zod"; -import { z } from "zod"; -import { relations } from "drizzle-orm"; - -// Session storage table (required for Replit Auth) + uniqueIndex, +} from 'drizzle-orm/pg-core'; +import { createInsertSchema } from 'drizzle-zod'; +import { z } from 'zod'; + +// ===================================================================== +// Sessions — legacy Replit Auth store, removed at ChittyAuth-only cutover. +// ===================================================================== export const sessions = pgTable( - "sessions", + 'sessions', { - sid: varchar("sid").primaryKey(), - sess: jsonb("sess").notNull(), - expire: timestamp("expire").notNull(), + sid: varchar('sid').primaryKey(), + sess: jsonb('sess').notNull(), + expire: timestamp('expire').notNull(), }, - (table) => [index("IDX_session_expire").on(table.expire)], + (table) => [index('IDX_session_expire').on(table.expire)], ); -// User storage table (required for Replit Auth) -export const users = pgTable("users", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - email: varchar("email").unique(), - firstName: varchar("first_name"), - lastName: varchar("last_name"), - profileImageUrl: varchar("profile_image_url"), - createdAt: timestamp("created_at").defaultNow(), - updatedAt: timestamp("updated_at").defaultNow(), +// ===================================================================== +// @canon: chittycanon://gov/governance#core-types — Person (P) +// Users are natural actors with agency. Phase 3 promotes chitty_id to PK. +// ===================================================================== +export const users = pgTable('users', { + id: varchar('id').primaryKey().default(sql`gen_random_uuid()`), + /** Canonical Person ChittyID. Format VV-G-LLL-SSSS-P-YM-C-X. Nullable during Phase 1; PK in Phase 3. */ + chittyId: varchar('chitty_id').unique(), + email: varchar('email').unique(), + firstName: varchar('first_name'), + lastName: varchar('last_name'), + profileImageUrl: varchar('profile_image_url'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + /** GDPR soft-delete; ChittyLedger preserves the audit trail. */ + deletedAt: timestamp('deleted_at', { withTimezone: true }), }); -// Asset categories and types +// ===================================================================== +// Enums +// ===================================================================== export const assetTypeEnum = pgEnum('asset_type', [ - 'real_estate', 'vehicle', 'artwork', 'jewelry', 'electronics', - 'documents', 'business_assets', 'intellectual_property', 'other' + 'real_estate', 'vehicle', 'artwork', 'jewelry', 'electronics', + 'documents', 'business_assets', 'intellectual_property', 'other', ]); export const assetStatusEnum = pgEnum('asset_status', [ - 'active', 'disposed', 'lost', 'stolen', 'in_dispute', 'under_review' + 'active', 'disposed', 'lost', 'stolen', 'in_dispute', 'under_review', ]); export const verificationStatusEnum = pgEnum('verification_status', [ - 'pending', 'verified', 'rejected', 'expired' + 'pending', 'verified', 'rejected', 'expired', ]); export const chittyChainStatusEnum = pgEnum('chitty_chain_status', [ - 'draft', 'frozen', 'minted', 'settled', 'disputed' + 'draft', 'frozen', 'minted', 'settled', 'disputed', ]); -// Assets table - core asset tracking with ChittyChain integration -export const assets = pgTable("assets", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - chittyId: varchar("chitty_id").unique(), // ChittyChain UUID v7 identifier - chittyIdV2: varchar("chitty_id_v2").unique(), // Future Mod-97 Base32 identifier (TTTTTTTTT-NN-VVV-SSSS-CC) - userId: varchar("user_id").notNull().references(() => users.id), - name: text("name").notNull(), - description: text("description"), - assetType: assetTypeEnum("asset_type").notNull(), - status: assetStatusEnum("status").default("active"), - purchasePrice: decimal("purchase_price", { precision: 12, scale: 2 }), - currentValue: decimal("current_value", { precision: 12, scale: 2 }), - purchaseDate: timestamp("purchase_date"), - location: text("location"), - serialNumber: varchar("serial_number"), - model: varchar("model"), - manufacturer: varchar("manufacturer"), - condition: varchar("condition"), - trustScore: decimal("trust_score", { precision: 3, scale: 1 }).default("0.0"), - - // ChittyChain blockchain integration - blockchainHash: varchar("blockchain_hash"), - blockNumber: varchar("block_number"), - ipfsHash: varchar("ipfs_hash"), // IPFS CID for off-chain data - freezeTimestamp: timestamp("freeze_timestamp"), // 7-day freeze period start - settlementTimestamp: timestamp("settlement_timestamp"), // On-chain settlement completion - mintingFee: decimal("minting_fee", { precision: 8, scale: 6 }), // Fee paid in CHITTY tokens - - verificationStatus: verificationStatusEnum("verification_status").default("pending"), - chittyChainStatus: chittyChainStatusEnum("chitty_chain_status").default("draft"), - tags: text("tags").array(), - metadata: jsonb("metadata"), - createdAt: timestamp("created_at").defaultNow(), - updatedAt: timestamp("updated_at").defaultNow(), -}); - -// Evidence and documentation export const evidenceTypeEnum = pgEnum('evidence_type', [ - 'receipt', 'contract', 'photo', 'video', 'insurance_document', - 'warranty', 'maintenance_record', 'legal_filing', 'correspondence', 'other' + 'receipt', 'contract', 'photo', 'video', 'insurance_document', + 'warranty', 'maintenance_record', 'legal_filing', 'correspondence', 'other', ]); -export const evidence = pgTable("evidence", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - assetId: varchar("asset_id").notNull().references(() => assets.id), - userId: varchar("user_id").notNull().references(() => users.id), - name: text("name").notNull(), - evidenceType: evidenceTypeEnum("evidence_type").notNull(), - filePath: text("file_path"), - fileSize: integer("file_size"), - mimeType: varchar("mime_type"), - extractedData: jsonb("extracted_data"), - aiAnalysis: jsonb("ai_analysis"), - blockchainHash: varchar("blockchain_hash"), - verificationStatus: verificationStatusEnum("verification_status").default("pending"), - createdAt: timestamp("created_at").defaultNow(), - updatedAt: timestamp("updated_at").defaultNow(), -}); - -// Asset timeline events export const timelineEventTypeEnum = pgEnum('timeline_event_type', [ - 'acquisition', 'modification', 'maintenance', 'insurance_update', - 'valuation_change', 'location_change', 'status_change', 'evidence_added', 'other' + 'acquisition', 'modification', 'maintenance', 'insurance_update', + 'valuation_change', 'location_change', 'status_change', 'evidence_added', 'other', ]); -export const timelineEvents = pgTable("timeline_events", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - assetId: varchar("asset_id").notNull().references(() => assets.id), - userId: varchar("user_id").notNull().references(() => users.id), - eventType: timelineEventTypeEnum("event_type").notNull(), - title: text("title").notNull(), - description: text("description"), - eventDate: timestamp("event_date").notNull(), - relatedEvidenceId: varchar("related_evidence_id").references(() => evidence.id), - metadata: jsonb("metadata"), - createdAt: timestamp("created_at").defaultNow(), +export const legalCaseStatusEnum = pgEnum('legal_case_status', [ + 'active', 'settled', 'dismissed', 'pending', 'on_appeal', +]); + +// ===================================================================== +// @canon: chittycanon://gov/governance#core-types — Thing (T) +// Central artifact of ChittyAssets: an object without agency. +// ===================================================================== +export const assets = pgTable('assets', { + id: varchar('id').primaryKey().default(sql`gen_random_uuid()`), + /** Canonical Thing ChittyID. Format VV-G-LLL-SSSS-T-YM-C-X. */ + chittyId: varchar('chitty_id').unique(), + chittyIdV2: varchar('chitty_id_v2').unique(), + userId: varchar('user_id').notNull().references(() => users.id), + name: text('name').notNull(), + description: text('description'), + assetType: assetTypeEnum('asset_type').notNull(), + status: assetStatusEnum('status').default('active'), + purchasePrice: numeric('purchase_price', { precision: 12, scale: 2 }), + currentValue: numeric('current_value', { precision: 12, scale: 2 }), + purchaseDate: timestamp('purchase_date', { withTimezone: true }), + location: text('location'), + serialNumber: varchar('serial_number'), + model: varchar('model'), + manufacturer: varchar('manufacturer'), + condition: varchar('condition'), + trustScore: numeric('trust_score', { precision: 3, scale: 1 }).default('0.0'), + blockchainHash: varchar('blockchain_hash'), + blockNumber: varchar('block_number'), + ipfsHash: varchar('ipfs_hash'), + freezeTimestamp: timestamp('freeze_timestamp', { withTimezone: true }), + settlementTimestamp: timestamp('settlement_timestamp', { withTimezone: true }), + mintingFee: numeric('minting_fee', { precision: 8, scale: 6 }), + verificationStatus: verificationStatusEnum('verification_status').default('pending'), + chittyChainStatus: chittyChainStatusEnum('chitty_chain_status').default('draft'), + tags: text('tags').array(), + metadata: jsonb('metadata'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + deletedAt: timestamp('deleted_at', { withTimezone: true }), }); -// Warranties and service contracts -export const warranties = pgTable("warranties", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - assetId: varchar("asset_id").notNull().references(() => assets.id), - userId: varchar("user_id").notNull().references(() => users.id), - provider: text("provider").notNull(), - type: varchar("type").notNull(), // extended, manufacturer, service_contract - startDate: timestamp("start_date").notNull(), - endDate: timestamp("end_date").notNull(), - coverage: text("coverage"), - terms: text("terms"), - cost: decimal("cost", { precision: 10, scale: 2 }), - isActive: boolean("is_active").default(true), - notificationSent: boolean("notification_sent").default(false), - createdAt: timestamp("created_at").defaultNow(), - updatedAt: timestamp("updated_at").defaultNow(), +// ===================================================================== +// @canon: chittycanon://gov/governance#core-types — Thing (T) +// Evidence is a document/artifact attached to an asset. +// ===================================================================== +export const evidence = pgTable('evidence', { + id: varchar('id').primaryKey().default(sql`gen_random_uuid()`), + chittyId: varchar('chitty_id').unique(), + assetId: varchar('asset_id').notNull().references(() => assets.id), + userId: varchar('user_id').notNull().references(() => users.id), + name: text('name').notNull(), + evidenceType: evidenceTypeEnum('evidence_type').notNull(), + filePath: text('file_path'), + fileSize: integer('file_size'), + mimeType: varchar('mime_type'), + extractedData: jsonb('extracted_data'), + aiAnalysis: jsonb('ai_analysis'), + blockchainHash: varchar('blockchain_hash'), + verificationStatus: verificationStatusEnum('verification_status').default('pending'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + deletedAt: timestamp('deleted_at', { withTimezone: true }), }); -// Insurance policies -export const insurancePolicies = pgTable("insurance_policies", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - assetId: varchar("asset_id").notNull().references(() => assets.id), - userId: varchar("user_id").notNull().references(() => users.id), - provider: text("provider").notNull(), - policyNumber: varchar("policy_number").notNull(), - type: varchar("type").notNull(), // comprehensive, liability, etc. - coverageAmount: decimal("coverage_amount", { precision: 12, scale: 2 }), - premium: decimal("premium", { precision: 10, scale: 2 }), - deductible: decimal("deductible", { precision: 10, scale: 2 }), - startDate: timestamp("start_date").notNull(), - endDate: timestamp("end_date").notNull(), - isActive: boolean("is_active").default(true), - createdAt: timestamp("created_at").defaultNow(), - updatedAt: timestamp("updated_at").defaultNow(), +// ===================================================================== +// @canon: chittycanon://gov/governance#core-types — Event (E) +// Append-only occurrence in time against an asset. +// ===================================================================== +export const timelineEvents = pgTable('timeline_events', { + id: varchar('id').primaryKey().default(sql`gen_random_uuid()`), + chittyId: varchar('chitty_id').unique(), + assetId: varchar('asset_id').notNull().references(() => assets.id), + userId: varchar('user_id').notNull().references(() => users.id), + eventType: timelineEventTypeEnum('event_type').notNull(), + title: text('title').notNull(), + description: text('description'), + eventDate: timestamp('event_date', { withTimezone: true }).notNull(), + relatedEvidenceId: varchar('related_evidence_id').references(() => evidence.id), + metadata: jsonb('metadata'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }); -// Legal cases and disputes -export const legalCaseStatusEnum = pgEnum('legal_case_status', [ - 'active', 'settled', 'dismissed', 'pending', 'on_appeal' -]); +// ===================================================================== +// @canon: chittycanon://gov/governance#core-types — Thing (T) +// Warranty contract as artifact. Coverage events are timeline_events. +// ===================================================================== +export const warranties = pgTable('warranties', { + id: varchar('id').primaryKey().default(sql`gen_random_uuid()`), + chittyId: varchar('chitty_id').unique(), + assetId: varchar('asset_id').notNull().references(() => assets.id), + userId: varchar('user_id').notNull().references(() => users.id), + provider: text('provider').notNull(), + type: varchar('type').notNull(), + startDate: timestamp('start_date', { withTimezone: true }).notNull(), + endDate: timestamp('end_date', { withTimezone: true }).notNull(), + coverage: text('coverage'), + terms: text('terms'), + cost: numeric('cost', { precision: 10, scale: 2 }), + isActive: boolean('is_active').default(true), + notificationSent: boolean('notification_sent').default(false), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); + +// ===================================================================== +// @canon: chittycanon://gov/governance#core-types — Thing (T) +// Policy document as artifact. Claim events are timeline_events. +// ===================================================================== +export const insurancePolicies = pgTable('insurance_policies', { + id: varchar('id').primaryKey().default(sql`gen_random_uuid()`), + chittyId: varchar('chitty_id').unique(), + assetId: varchar('asset_id').notNull().references(() => assets.id), + userId: varchar('user_id').notNull().references(() => users.id), + provider: text('provider').notNull(), + policyNumber: varchar('policy_number').notNull(), + type: varchar('type').notNull(), + coverageAmount: numeric('coverage_amount', { precision: 12, scale: 2 }), + premium: numeric('premium', { precision: 10, scale: 2 }), + deductible: numeric('deductible', { precision: 10, scale: 2 }), + startDate: timestamp('start_date', { withTimezone: true }).notNull(), + endDate: timestamp('end_date', { withTimezone: true }).notNull(), + isActive: boolean('is_active').default(true), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); + +// ===================================================================== +// @canon: chittycanon://gov/governance#core-types — Event (E) +// Legal proceeding with docket/status progression. +// ===================================================================== +export const legalCases = pgTable('legal_cases', { + id: varchar('id').primaryKey().default(sql`gen_random_uuid()`), + chittyId: varchar('chitty_id').unique(), + userId: varchar('user_id').notNull().references(() => users.id), + caseNumber: varchar('case_number'), + title: text('title').notNull(), + description: text('description'), + status: legalCaseStatusEnum('status').default('active'), + court: text('court'), + judge: text('judge'), + filingDate: timestamp('filing_date', { withTimezone: true }), + nextHearing: timestamp('next_hearing', { withTimezone: true }), + relatedAssets: text('related_assets').array(), + attorneys: jsonb('attorneys'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); -export const legalCases = pgTable("legal_cases", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - userId: varchar("user_id").notNull().references(() => users.id), - caseNumber: varchar("case_number"), - title: text("title").notNull(), - description: text("description"), - status: legalCaseStatusEnum("status").default("active"), - court: text("court"), - judge: text("judge"), - filingDate: timestamp("filing_date"), - nextHearing: timestamp("next_hearing"), - relatedAssets: text("related_assets").array(), - attorneys: jsonb("attorneys"), - createdAt: timestamp("created_at").defaultNow(), - updatedAt: timestamp("updated_at").defaultNow(), +// ===================================================================== +// @canon: chittycanon://gov/governance#core-types — Event (E) +// AI analysis run in time against an evidence artifact. Append-only. +// ===================================================================== +export const aiAnalysisResults = pgTable('ai_analysis_results', { + id: varchar('id').primaryKey().default(sql`gen_random_uuid()`), + chittyId: varchar('chitty_id').unique(), + evidenceId: varchar('evidence_id').notNull().references(() => evidence.id), + analysisType: varchar('analysis_type').notNull(), + confidence: numeric('confidence', { precision: 3, scale: 2 }), + results: jsonb('results').notNull(), + processingTime: integer('processing_time'), + modelUsed: varchar('model_used'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }); -// AI analysis results -export const aiAnalysisResults = pgTable("ai_analysis_results", { - id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), - evidenceId: varchar("evidence_id").notNull().references(() => evidence.id), - analysisType: varchar("analysis_type").notNull(), // ocr, vision, valuation, etc. - confidence: decimal("confidence", { precision: 3, scale: 2 }), - results: jsonb("results").notNull(), - processingTime: integer("processing_time"), // in milliseconds - modelUsed: varchar("model_used"), - createdAt: timestamp("created_at").defaultNow(), +// ===================================================================== +// Canonical entity types enum — all five P/L/T/E/A must be present. +// @canon: chittycanon://gov/governance#core-types +// ===================================================================== +export const entityTypeEnum = pgEnum('entity_type', ['P', 'L', 'T', 'E', 'A']); + +// ===================================================================== +// Entities registry — soft-reference index. No FKs from per-service tables. +// ===================================================================== +export const entities = pgTable( + 'entities', + { + chittyId: varchar('chitty_id').primaryKey(), + /** @canon: chittycanon://gov/governance#core-types — one of P/L/T/E/A. */ + entityType: entityTypeEnum('entity_type').notNull(), + sourceTable: varchar('source_table').notNull(), + sourceId: varchar('source_id').notNull(), + displayName: text('display_name'), + metadata: jsonb('metadata'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + deletedAt: timestamp('deleted_at', { withTimezone: true }), + }, + (table) => [uniqueIndex('uq_entities_source').on(table.sourceTable, table.sourceId)], +); + +// ===================================================================== +// R2 object ACL — operational metadata (not a canonical entity). +// principal_chitty_id is a Person (P); evidence_id / asset_id are Things (T). +// ===================================================================== +export const r2AclPermissionEnum = pgEnum('r2_acl_permission', ['read', 'write', 'owner']); + +export const r2ObjectAcl = pgTable('r2_object_acl', { + id: varchar('id').primaryKey().default(sql`gen_random_uuid()`), + bucket: varchar('bucket').notNull(), + objectKey: text('object_key').notNull(), + /** Person (P) ChittyID — soft ref to users.chitty_id. */ + principalChittyId: varchar('principal_chitty_id').notNull(), + permission: r2AclPermissionEnum('permission').notNull(), + grantedByChittyId: varchar('granted_by_chitty_id'), + evidenceId: varchar('evidence_id').references(() => evidence.id), + assetId: varchar('asset_id').references(() => assets.id), + expiresAt: timestamp('expires_at', { withTimezone: true }), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + revokedAt: timestamp('revoked_at', { withTimezone: true }), }); +// ===================================================================== // Relations +// ===================================================================== export const usersRelations = relations(users, ({ many }) => ({ assets: many(assets), evidence: many(evidence), @@ -214,10 +309,7 @@ export const usersRelations = relations(users, ({ many }) => ({ })); export const assetsRelations = relations(assets, ({ one, many }) => ({ - user: one(users, { - fields: [assets.userId], - references: [users.id], - }), + user: one(users, { fields: [assets.userId], references: [users.id] }), evidence: many(evidence), timelineEvents: many(timelineEvents), warranties: many(warranties), @@ -225,27 +317,15 @@ export const assetsRelations = relations(assets, ({ one, many }) => ({ })); export const evidenceRelations = relations(evidence, ({ one, many }) => ({ - asset: one(assets, { - fields: [evidence.assetId], - references: [assets.id], - }), - user: one(users, { - fields: [evidence.userId], - references: [users.id], - }), + asset: one(assets, { fields: [evidence.assetId], references: [assets.id] }), + user: one(users, { fields: [evidence.userId], references: [users.id] }), aiAnalysisResults: many(aiAnalysisResults), timelineEvents: many(timelineEvents), })); export const timelineEventsRelations = relations(timelineEvents, ({ one }) => ({ - asset: one(assets, { - fields: [timelineEvents.assetId], - references: [assets.id], - }), - user: one(users, { - fields: [timelineEvents.userId], - references: [users.id], - }), + asset: one(assets, { fields: [timelineEvents.assetId], references: [assets.id] }), + user: one(users, { fields: [timelineEvents.userId], references: [users.id] }), relatedEvidence: one(evidence, { fields: [timelineEvents.relatedEvidenceId], references: [evidence.id], @@ -253,89 +333,60 @@ export const timelineEventsRelations = relations(timelineEvents, ({ one }) => ({ })); export const warrantiesRelations = relations(warranties, ({ one }) => ({ - asset: one(assets, { - fields: [warranties.assetId], - references: [assets.id], - }), - user: one(users, { - fields: [warranties.userId], - references: [users.id], - }), + asset: one(assets, { fields: [warranties.assetId], references: [assets.id] }), + user: one(users, { fields: [warranties.userId], references: [users.id] }), })); export const insurancePoliciesRelations = relations(insurancePolicies, ({ one }) => ({ - asset: one(assets, { - fields: [insurancePolicies.assetId], - references: [assets.id], - }), - user: one(users, { - fields: [insurancePolicies.userId], - references: [users.id], - }), + asset: one(assets, { fields: [insurancePolicies.assetId], references: [assets.id] }), + user: one(users, { fields: [insurancePolicies.userId], references: [users.id] }), })); export const legalCasesRelations = relations(legalCases, ({ one }) => ({ - user: one(users, { - fields: [legalCases.userId], - references: [users.id], - }), + user: one(users, { fields: [legalCases.userId], references: [users.id] }), })); export const aiAnalysisResultsRelations = relations(aiAnalysisResults, ({ one }) => ({ - evidence: one(evidence, { - fields: [aiAnalysisResults.evidenceId], - references: [evidence.id], - }), + evidence: one(evidence, { fields: [aiAnalysisResults.evidenceId], references: [evidence.id] }), })); +// ===================================================================== // Insert schemas +// ===================================================================== export const insertUserSchema = createInsertSchema(users).omit({ - id: true, - createdAt: true, - updatedAt: true, + id: true, createdAt: true, updatedAt: true, }); - export const insertAssetSchema = createInsertSchema(assets).omit({ - id: true, - createdAt: true, - updatedAt: true, + id: true, createdAt: true, updatedAt: true, }); - export const insertEvidenceSchema = createInsertSchema(evidence).omit({ - id: true, - createdAt: true, - updatedAt: true, + id: true, createdAt: true, updatedAt: true, }); - export const insertTimelineEventSchema = createInsertSchema(timelineEvents).omit({ - id: true, - createdAt: true, + id: true, createdAt: true, }); - export const insertWarrantySchema = createInsertSchema(warranties).omit({ - id: true, - createdAt: true, - updatedAt: true, + id: true, createdAt: true, updatedAt: true, }); - export const insertInsurancePolicySchema = createInsertSchema(insurancePolicies).omit({ - id: true, - createdAt: true, - updatedAt: true, + id: true, createdAt: true, updatedAt: true, }); - export const insertLegalCaseSchema = createInsertSchema(legalCases).omit({ - id: true, - createdAt: true, - updatedAt: true, + id: true, createdAt: true, updatedAt: true, }); - export const insertAiAnalysisResultSchema = createInsertSchema(aiAnalysisResults).omit({ - id: true, - createdAt: true, + id: true, createdAt: true, +}); +export const insertEntitySchema = createInsertSchema(entities).omit({ + createdAt: true, updatedAt: true, +}); +export const insertR2ObjectAclSchema = createInsertSchema(r2ObjectAcl).omit({ + id: true, createdAt: true, }); +// ===================================================================== // Types +// ===================================================================== export type UpsertUser = typeof users.$inferInsert; export type User = typeof users.$inferSelect; export type InsertAsset = z.infer; @@ -352,3 +403,15 @@ export type InsertLegalCase = z.infer; export type LegalCase = typeof legalCases.$inferSelect; export type InsertAiAnalysisResult = z.infer; export type AiAnalysisResult = typeof aiAnalysisResults.$inferSelect; +export type InsertEntity = z.infer; +export type Entity = typeof entities.$inferSelect; +export type InsertR2ObjectAcl = z.infer; +export type R2ObjectAcl = typeof r2ObjectAcl.$inferSelect; + +// Canonical entity-type tuple — all five P/L/T/E/A. Never omit A. +// @canon: chittycanon://gov/governance#core-types +export const CANONICAL_ENTITY_TYPES = ['P', 'L', 'T', 'E', 'A'] as const; +export type CanonicalEntityType = (typeof CANONICAL_ENTITY_TYPES)[number]; + +// Canonical ChittyID regex — VV-G-LLL-SSSS-T-YM-C-X where T is one of P/L/T/E/A. +export const CHITTY_ID_PATTERN = /^[0-9]{2}-[A-Z]-[0-9]{3}-[0-9]{4}-[PLTEA]-[0-9]{4}-[A-Z]-[0-9]$/;