diff --git a/apps/gateway/src/__tests__/hierarchy-route-inventory.test.ts b/apps/gateway/src/__tests__/hierarchy-route-inventory.test.ts new file mode 100644 index 00000000..86c9ecb6 --- /dev/null +++ b/apps/gateway/src/__tests__/hierarchy-route-inventory.test.ts @@ -0,0 +1,106 @@ +import { RequestMethod, type Type } from '@nestjs/common'; +import { describe, expect, it } from 'vitest'; +import { AppModule } from '../app.module.js'; +import { HierarchyModule } from '../hierarchy/hierarchy.module.js'; + +/** + * Hierarchy route-inventory baseline (contract 1 §6.3(a)). + * + * M4-1b-i ships the audit event + outbox machinery with NO mutation routes: + * the hierarchy command family (controllers + DTOs) lands in M4-1b-ii once + * contract 2 merges. This witness enumerates every route the AppModule graph + * declares and pins that baseline, so a hierarchy route appearing before its + * command-family witnesses exist fails here first. When M4-1b-ii lands, this + * baseline is replaced by an exact inventory of the command family. + */ + +interface RouteEntry { + method: string; + path: string; + controller: string; +} + +/** Module-metadata entry: a module class or a DynamicModule-shaped object. */ +type ModuleEntry = + | Type + | { module: Type; imports?: unknown[]; controllers?: Type[] }; + +function collectControllers(root: ModuleEntry): Type[] { + const visited = new Set(); + const controllers: Type[] = []; + const walk = (entry: ModuleEntry | undefined | null): void => { + if (!entry || visited.has(entry)) return; + visited.add(entry); + const moduleClass = typeof entry === 'function' ? entry : entry.module; + // Entries with no resolvable class (forwardRef wrappers, async dynamic + // modules) carry no decorator metadata to read here. + if (typeof moduleClass !== 'function') return; + if (visited.has(moduleClass) && typeof entry !== 'function') return; + visited.add(moduleClass); + // 'controllers' / 'imports' are the metadata keys the @Module decorator writes. + const declared = (Reflect.getMetadata('controllers', moduleClass) ?? []) as Type[]; + controllers.push(...declared); + if (typeof entry !== 'function' && entry.controllers) controllers.push(...entry.controllers); + const imports = [ + ...((Reflect.getMetadata('imports', moduleClass) ?? []) as ModuleEntry[]), + ...(typeof entry !== 'function' ? ((entry.imports ?? []) as ModuleEntry[]) : []), + ]; + for (const imported of imports) walk(imported); + }; + walk(root); + return controllers; +} + +function routesOf(controller: Type): RouteEntry[] { + // 'path' on the class is the @Controller prefix; 'path'/'method' on a + // handler are written by the @Get/@Post/... route decorators. + const base = (Reflect.getMetadata('path', controller) ?? '') as string | string[]; + const bases = Array.isArray(base) ? base : [base]; + const routes: RouteEntry[] = []; + const prototype = controller.prototype as Record; + for (const name of Object.getOwnPropertyNames(prototype)) { + if (name === 'constructor') continue; + const handler = Object.getOwnPropertyDescriptor(prototype, name)?.value; + if (typeof handler !== 'function') continue; + const method = Reflect.getMetadata('method', handler) as number | undefined; + if (method === undefined) continue; + const sub = (Reflect.getMetadata('path', handler) ?? '/') as string; + for (const prefix of bases) { + const path = `/${prefix}/${sub}`.replace(/\/+/g, '/').replace(/(.)\/$/, '$1'); + routes.push({ + method: RequestMethod[method] ?? String(method), + path, + controller: controller.name, + }); + } + } + return routes; +} + +describe('hierarchy route-inventory baseline (§6.3(a))', () => { + const inventory = collectControllers(AppModule).flatMap(routesOf); + + it('control: the enumeration sees the known route surface', () => { + const paths = inventory.map((r) => `${r.method} ${r.path}`); + expect(paths).toContain('GET /health'); + expect(paths).toContain('POST /api/workspaces'); + expect(paths).toContain('GET /api/teams'); + expect(inventory.length).toBeGreaterThan(20); + }); + + it('declares zero hierarchy mutation routes before M4-1b-ii', () => { + const hierarchyRoutes = inventory.filter((r) => + /hierarch|compan|estate|platform[-_]?project/i.test(r.path), + ); + expect( + hierarchyRoutes, + 'a hierarchy route landed without replacing the §6.3(a) baseline with a command-family inventory', + ).toEqual([]); + }); + + it('HierarchyModule itself declares no controllers', () => { + expect((Reflect.getMetadata('controllers', HierarchyModule) ?? []) as unknown[]).toEqual([]); + const hierarchyControllers = collectControllers(HierarchyModule); + expect(hierarchyControllers).toEqual([]); + }); +}); diff --git a/apps/gateway/src/app.module.ts b/apps/gateway/src/app.module.ts index 7c2388a8..380b5e63 100644 --- a/apps/gateway/src/app.module.ts +++ b/apps/gateway/src/app.module.ts @@ -24,6 +24,7 @@ import { GCModule } from './gc/gc.module.js'; import { HarnessModule } from './harness/harness.module.js'; import { ReloadModule } from './reload/reload.module.js'; import { WorkspaceModule } from './workspace/workspace.module.js'; +import { HierarchyModule } from './hierarchy/hierarchy.module.js'; import { QueueModule } from './queue/queue.module.js'; import { FederationModule } from './federation/federation.module.js'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; @@ -65,6 +66,7 @@ const federationEnabled = loadConfig(resolveGatewayConfigPath()).tier === 'feder QueueModule, ReloadModule, WorkspaceModule, + HierarchyModule, ...(federationEnabled ? [FederationModule] : []), ], controllers: [HealthController], diff --git a/apps/gateway/src/hierarchy/hierarchy-audit.integration.test.ts b/apps/gateway/src/hierarchy/hierarchy-audit.integration.test.ts new file mode 100644 index 00000000..2b12e2f5 --- /dev/null +++ b/apps/gateway/src/hierarchy/hierarchy-audit.integration.test.ts @@ -0,0 +1,247 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { randomUUID } from 'node:crypto'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { Test, type TestingModule } from '@nestjs/testing'; +import { + companies, + createPgliteDb, + eq, + estates, + hierarchyAuditEvents, + hierarchyOutbox, + platformProjects, + runPgliteMigrations, + type DbHandle, +} from '@mosaicstack/db'; +import { DB } from '../database/database.module.js'; +import { + HierarchyAuditIdempotencyConflictError, + HierarchyAuditRepository, + HierarchyNodeNotFoundError, + type AppendHierarchyEventInput, +} from './hierarchy-audit.repository.js'; + +/** + * Repository-level §6.4 witnesses for the hierarchy audit machinery + * (contract 1 §5.2, REQ-AUD-001): same-transaction atomicity of state + + * event + outbox, rollback leaving no residue, idempotent replay, snapshot + * parent chains, events surviving target deletion, per-target ordering, and + * the outbox claim/complete/release CAS. The schema-level constraints are + * witnessed in packages/db/src/hierarchy-audit.witness.test.ts. + */ +describe('hierarchy audit repository integration', (): void => { + let dataDir: string; + let handle: DbHandle; + let moduleRef: TestingModule; + let repo: HierarchyAuditRepository; + + const input = ( + overrides: Partial = {}, + ): AppendHierarchyEventInput => ({ + actorId: 'user-actor', + verb: 'create', + targetKind: 'company', + targetId: randomUUID(), + targetSnapshot: { id: 'x', slug: 'x', name: 'x', parentChain: [] }, + correlationId: 'corr-1', + idempotencyKey: `key-${randomUUID()}`, + ...overrides, + }); + + beforeAll(async (): Promise => { + dataDir = await mkdtemp(join(tmpdir(), 'mosaic-gateway-hierarchy-audit-')); + handle = createPgliteDb(dataDir); + await runPgliteMigrations(handle); + moduleRef = await Test.createTestingModule({ + providers: [HierarchyAuditRepository, { provide: DB, useValue: handle.db }], + }).compile(); + repo = moduleRef.get(HierarchyAuditRepository); + }); + + afterAll(async (): Promise => { + await moduleRef.close(); + await handle.close(); + await rm(dataDir, { recursive: true, force: true }); + }); + + it('commits state, event, and outbox record atomically in one transaction', async () => { + const companyId = randomUUID(); + const key = `key-${randomUUID()}`; + await handle.db.transaction(async (tx) => { + await tx.insert(companies).values({ id: companyId, name: 'Atomic Co', slug: 'atomic-co' }); + const snapshot = await repo.snapshot(tx, 'company', companyId); + const result = await repo.append(tx, { + ...input({ targetId: companyId, idempotencyKey: key }), + targetSnapshot: { ...snapshot }, + }); + expect(result.replayed).toBe(false); + expect(result.event.idempotencyKey).toBe(key); + }); + const events = await handle.db + .select() + .from(hierarchyAuditEvents) + .where(eq(hierarchyAuditEvents.idempotencyKey, key)); + expect(events).toHaveLength(1); + const outbox = await handle.db + .select() + .from(hierarchyOutbox) + .where(eq(hierarchyOutbox.eventId, events[0]!.id)); + expect(outbox).toHaveLength(1); + expect(outbox[0]).toMatchObject({ + status: 'pending', + idempotencyKey: key, + correlationId: 'corr-1', + }); + }); + + it('a rolled-back transaction leaves no state, no event, and no outbox record', async () => { + const companyId = randomUUID(); + const key = `key-${randomUUID()}`; + await expect( + handle.db.transaction(async (tx) => { + await tx.insert(companies).values({ id: companyId, name: 'Doomed Co', slug: 'doomed-co' }); + await repo.append(tx, input({ targetId: companyId, idempotencyKey: key })); + throw new Error('deliberate rollback'); + }), + ).rejects.toThrow('deliberate rollback'); + const [companyRows, eventRows, outboxRows] = await Promise.all([ + handle.db.select().from(companies).where(eq(companies.id, companyId)), + handle.db + .select() + .from(hierarchyAuditEvents) + .where(eq(hierarchyAuditEvents.idempotencyKey, key)), + handle.db.select().from(hierarchyOutbox).where(eq(hierarchyOutbox.idempotencyKey, key)), + ]); + expect(companyRows).toHaveLength(0); + expect(eventRows).toHaveLength(0); + expect(outboxRows).toHaveLength(0); + }); + + it('replays a duplicate idempotency key without inserting a second event or outbox record', async () => { + const first = input(); + const original = await handle.db.transaction(async (tx) => repo.append(tx, first)); + const replay = await handle.db.transaction(async (tx) => repo.append(tx, first)); + expect(original.replayed).toBe(false); + expect(replay.replayed).toBe(true); + expect(replay.event.id).toBe(original.event.id); + const outbox = await handle.db + .select() + .from(hierarchyOutbox) + .where(eq(hierarchyOutbox.eventId, original.event.id)); + expect(outbox).toHaveLength(1); + }); + + it('throws on a duplicate idempotency key carrying different event content', async () => { + const first = input(); + await handle.db.transaction(async (tx) => repo.append(tx, first)); + await expect( + handle.db.transaction(async (tx) => + repo.append(tx, { ...first, verb: 'rename', targetId: randomUUID() }), + ), + ).rejects.toThrow(HierarchyAuditIdempotencyConflictError); + }); + + it('throws on a duplicate idempotency key whose transfer destination differs', async () => { + const from = { kind: 'company' as const, id: randomUUID(), slug: 'src-co' }; + const to = { kind: 'company' as const, id: randomUUID(), slug: 'dst-co' }; + const first = input({ + verb: 'transfer', + targetKind: 'estate', + transferFrom: from, + transferTo: to, + }); + const original = await handle.db.transaction(async (tx) => repo.append(tx, first)); + expect(original.replayed).toBe(false); + // Identical retry replays; a retry re-routed to a different destination must conflict. + const replay = await handle.db.transaction(async (tx) => repo.append(tx, first)); + expect(replay.replayed).toBe(true); + await expect( + handle.db.transaction(async (tx) => + repo.append(tx, { ...first, transferTo: { ...to, id: randomUUID() } }), + ), + ).rejects.toThrow(HierarchyAuditIdempotencyConflictError); + }); + + it('builds root-first parent chains and rejects unknown nodes', async () => { + const companyId = randomUUID(); + const estateId = randomUUID(); + const projectId = randomUUID(); + await handle.db.transaction(async (tx) => { + await tx.insert(companies).values({ id: companyId, name: 'Chain Co', slug: 'chain-co' }); + await tx + .insert(estates) + .values({ id: estateId, name: 'Chain Estate', slug: 'chain-estate', companyId }); + await tx + .insert(platformProjects) + .values({ id: projectId, name: 'Chain Project', slug: 'chain-project', estateId }); + }); + const snapshot = await repo.snapshot(handle.db, 'platform_project', projectId); + expect(snapshot).toMatchObject({ id: projectId, slug: 'chain-project', name: 'Chain Project' }); + expect(snapshot.parentChain).toEqual([ + { kind: 'company', id: companyId, slug: 'chain-co' }, + { kind: 'estate', id: estateId, slug: 'chain-estate' }, + ]); + await expect(repo.snapshot(handle.db, 'estate', randomUUID())).rejects.toThrow( + HierarchyNodeNotFoundError, + ); + }); + + it('keeps events readable, in per-target seq order, after the target row is deleted', async () => { + const companyId = randomUUID(); + await handle.db.transaction(async (tx) => { + await tx.insert(companies).values({ id: companyId, name: 'Mortal Co', slug: 'mortal-co' }); + const snapshot = await repo.snapshot(tx, 'company', companyId); + await repo.append(tx, input({ targetId: companyId, targetSnapshot: { ...snapshot } })); + }); + await handle.db.transaction(async (tx) => { + const snapshot = await repo.snapshot(tx, 'company', companyId); + await repo.append(tx, { + ...input({ verb: 'delete', targetId: companyId }), + targetSnapshot: { ...snapshot }, + }); + await tx.delete(companies).where(eq(companies.id, companyId)); + }); + const events = await repo.eventsForTarget(companyId); + expect(events.map((e) => e.verb)).toEqual(['create', 'delete']); + expect(events[1]!.seq).toBeGreaterThan(events[0]!.seq); + expect((events[1]!.targetSnapshot as { id: string }).id).toBe(companyId); + }); + + it('claims the oldest pending outbox record exactly once, completes and releases by CAS', async () => { + // Drain records left pending by earlier cases so ordering is deterministic. + for (;;) { + const drained = await repo.claimPendingOutbox(); + if (!drained) break; + await repo.completeOutbox(drained.id); + } + const older = await handle.db.transaction(async (tx) => repo.append(tx, input())); + const newer = await handle.db.transaction(async (tx) => repo.append(tx, input())); + + const claimed = await repo.claimPendingOutbox(); + expect(claimed).not.toBeNull(); + expect(claimed!.eventId).toBe(older.event.id); + expect(claimed!.status).toBe('processing'); + + // Delivery fails: release returns it to pending and it is claimable again. + await repo.releaseOutbox(claimed!.id); + const reclaimed = await repo.claimPendingOutbox(); + expect(reclaimed!.id).toBe(claimed!.id); + + await repo.completeOutbox(reclaimed!.id); + const done = await handle.db + .select() + .from(hierarchyOutbox) + .where(eq(hierarchyOutbox.id, reclaimed!.id)); + expect(done[0]!.status).toBe('delivered'); + expect(done[0]!.deliveredAt).not.toBeNull(); + // completeOutbox is CAS-guarded on 'processing': completing again is a no-op. + await repo.completeOutbox(reclaimed!.id); + + const second = await repo.claimPendingOutbox(); + expect(second!.eventId).toBe(newer.event.id); + await repo.completeOutbox(second!.id); + expect(await repo.claimPendingOutbox()).toBeNull(); + }); +}); diff --git a/apps/gateway/src/hierarchy/hierarchy-audit.repository.ts b/apps/gateway/src/hierarchy/hierarchy-audit.repository.ts new file mode 100644 index 00000000..8929b76b --- /dev/null +++ b/apps/gateway/src/hierarchy/hierarchy-audit.repository.ts @@ -0,0 +1,274 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + and, + asc, + companies, + eq, + estates, + hierarchyAuditEvents, + hierarchyOutbox, + platformProjects, + type Db, + type HIERARCHY_AUDIT_TARGET_KINDS, + type HIERARCHY_AUDIT_VERBS, +} from '@mosaicstack/db'; +import { DB } from '../database/database.module.js'; + +/** + * Hierarchy audit event + outbox machinery (contract 1 §5.2). + * + * Every hierarchy mutation writes its semantic audit event AND the event's + * outbox record on the caller's transaction, so state, event, and outbox + * commit or roll back together. Events reference their target by an + * immutable snapshot (id, slug, parent chain at event time), never by a + * foreign key into the class tables — append-only events survive the + * deletion of their target. This module exposes no update or delete path + * for events: append-only is a property of the code surface, witnessed by + * the integration tests. + * + * This is NOT a class-table writer: it touches only the audit/outbox + * tables, so it does not appear on the writer-coverage allowlist. The + * hierarchy command repositories (M4-1b-ii) are the allowlisted writers and + * call into this on their own transactions. + */ + +export type HierarchyAuditVerb = (typeof HIERARCHY_AUDIT_VERBS)[number]; +export type HierarchyTargetKind = (typeof HIERARCHY_AUDIT_TARGET_KINDS)[number]; +export type HierarchyNodeKind = Exclude; + +export interface ParentChainEntry { + readonly kind: HierarchyNodeKind; + readonly id: string; + readonly slug: string; +} + +/** Immutable node snapshot at event time; parentChain is root-first. */ +export interface HierarchyNodeSnapshot { + readonly id: string; + readonly slug: string; + readonly name: string; + readonly parentChain: readonly ParentChainEntry[]; +} + +export interface AppendHierarchyEventInput { + readonly actorId: string; + readonly verb: HierarchyAuditVerb; + readonly targetKind: HierarchyTargetKind; + readonly targetId: string; + /** Node events: HierarchyNodeSnapshot. Grant events: subject/target/role snapshot (contract 2 §4.4). */ + readonly targetSnapshot: Record; + /** Present exactly on transfers (CHECK-enforced): source/destination parent { kind, id, slug }. */ + readonly transferFrom?: ParentChainEntry; + readonly transferTo?: ParentChainEntry; + readonly correlationId: string; + /** Prior event in the causal chain (e.g. the delete event causing cascaded grant_revoke events). */ + readonly causationId?: string; + readonly idempotencyKey: string; +} + +export type HierarchyAuditEventRow = typeof hierarchyAuditEvents.$inferSelect; +export type HierarchyOutboxRow = typeof hierarchyOutbox.$inferSelect; + +export interface AppendHierarchyEventResult { + readonly event: HierarchyAuditEventRow; + /** True when the idempotency key had already committed an identical event (REQ-AUD-001 duplicate suppression). */ + readonly replayed: boolean; +} + +type Tx = Pick; + +export class HierarchyAuditIdempotencyConflictError extends Error { + constructor(idempotencyKey: string) { + super( + `hierarchy audit idempotency key ${idempotencyKey} already exists with different event content`, + ); + this.name = 'HierarchyAuditIdempotencyConflictError'; + } +} + +export class HierarchyNodeNotFoundError extends Error { + constructor(kind: HierarchyNodeKind, id: string) { + super(`hierarchy node not found: ${kind} ${id}`); + this.name = 'HierarchyNodeNotFoundError'; + } +} + +/** + * Append one audit event and its outbox record on the caller's transaction. + * A duplicate idempotency key with identical semantic content returns the + * prior event (replayed: true) without inserting anything; a duplicate key + * with different content throws. + */ +export async function appendHierarchyEvent( + tx: Tx, + input: AppendHierarchyEventInput, +): Promise { + const inserted = await tx + .insert(hierarchyAuditEvents) + .values({ + actorId: input.actorId, + verb: input.verb, + targetKind: input.targetKind, + targetId: input.targetId, + targetSnapshot: input.targetSnapshot, + transferFrom: input.transferFrom ?? null, + transferTo: input.transferTo ?? null, + correlationId: input.correlationId, + causationId: input.causationId ?? null, + idempotencyKey: input.idempotencyKey, + }) + .onConflictDoNothing() + .returning(); + const event = inserted[0]; + if (event) { + await tx.insert(hierarchyOutbox).values({ + eventId: event.id, + idempotencyKey: input.idempotencyKey, + correlationId: input.correlationId, + }); + return { event, replayed: false }; + } + + const prior = await tx + .select() + .from(hierarchyAuditEvents) + .where(eq(hierarchyAuditEvents.idempotencyKey, input.idempotencyKey)) + .limit(1); + const existing = prior[0]; + if (!existing || !sameEvent(existing, input)) { + throw new HierarchyAuditIdempotencyConflictError(input.idempotencyKey); + } + // Event and outbox committed atomically the first time, so the outbox + // record already exists; a replay inserts nothing. + return { event: existing, replayed: true }; +} + +/** Key-order-independent serialization: jsonb does not preserve key order. */ +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`; + if (value !== null && typeof value === 'object') { + const record = value as Record; + const body = Object.keys(record) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`) + .join(','); + return `{${body}}`; + } + return JSON.stringify(value); +} + +function sameEvent(row: HierarchyAuditEventRow, input: AppendHierarchyEventInput): boolean { + return ( + row.actorId === input.actorId && + row.verb === input.verb && + row.targetKind === input.targetKind && + row.targetId === input.targetId && + row.correlationId === input.correlationId && + (row.causationId ?? null) === (input.causationId ?? null) && + canonicalJson(row.targetSnapshot) === canonicalJson(input.targetSnapshot) && + // Transfer source/destination are semantic content (§5.2): a retry with a + // different destination must conflict, never silently replay. + canonicalJson(row.transferFrom ?? null) === canonicalJson(input.transferFrom ?? null) && + canonicalJson(row.transferTo ?? null) === canonicalJson(input.transferTo ?? null) + ); +} + +/** + * Build the immutable snapshot for a node: its row plus the parent chain up + * to the company root, root-first, read on the caller's transaction so the + * snapshot is consistent with the mutation it audits. + */ +export async function buildNodeSnapshot( + tx: Tx, + kind: HierarchyNodeKind, + id: string, +): Promise { + if (kind === 'company') { + const rows = await tx.select().from(companies).where(eq(companies.id, id)).limit(1); + const row = rows[0]; + if (!row) throw new HierarchyNodeNotFoundError(kind, id); + return { id: row.id, slug: row.slug, name: row.name, parentChain: [] }; + } + if (kind === 'estate') { + const rows = await tx.select().from(estates).where(eq(estates.id, id)).limit(1); + const row = rows[0]; + if (!row) throw new HierarchyNodeNotFoundError(kind, id); + const parent = await buildNodeSnapshot(tx, 'company', row.companyId); + return { + id: row.id, + slug: row.slug, + name: row.name, + parentChain: [...parent.parentChain, { kind: 'company', id: parent.id, slug: parent.slug }], + }; + } + const rows = await tx.select().from(platformProjects).where(eq(platformProjects.id, id)).limit(1); + const row = rows[0]; + if (!row) throw new HierarchyNodeNotFoundError(kind, id); + const parent = await buildNodeSnapshot(tx, 'estate', row.estateId); + return { + id: row.id, + slug: row.slug, + name: row.name, + parentChain: [...parent.parentChain, { kind: 'estate', id: parent.id, slug: parent.slug }], + }; +} + +@Injectable() +export class HierarchyAuditRepository { + constructor(@Inject(DB) private readonly db: Db) {} + + /** Compose an event+outbox append into a caller-owned transaction. */ + append(tx: Tx, input: AppendHierarchyEventInput): Promise { + return appendHierarchyEvent(tx, input); + } + + snapshot(tx: Tx, kind: HierarchyNodeKind, id: string): Promise { + return buildNodeSnapshot(tx, kind, id); + } + + /** Per-target ordered event history (REQ-AUD-001 per-target ordering; read-only). */ + async eventsForTarget(targetId: string): Promise { + return this.db + .select() + .from(hierarchyAuditEvents) + .where(eq(hierarchyAuditEvents.targetId, targetId)) + .orderBy(asc(hierarchyAuditEvents.seq)); + } + + /** + * Claim the oldest pending outbox record (claim-by-CAS: the UPDATE is + * guarded on status so a lost race returns null and the caller retries). + */ + async claimPendingOutbox(): Promise { + const candidates = await this.db + .select() + .from(hierarchyOutbox) + .where(eq(hierarchyOutbox.status, 'pending')) + .orderBy(asc(hierarchyOutbox.createdAt)) + .limit(1); + const candidate = candidates[0]; + if (!candidate) return null; + const claimed = await this.db + .update(hierarchyOutbox) + .set({ status: 'processing', updatedAt: new Date() }) + .where(and(eq(hierarchyOutbox.id, candidate.id), eq(hierarchyOutbox.status, 'pending'))) + .returning(); + return claimed[0] ?? null; + } + + async completeOutbox(id: string): Promise { + const now = new Date(); + await this.db + .update(hierarchyOutbox) + .set({ status: 'delivered', deliveredAt: now, updatedAt: now }) + .where(and(eq(hierarchyOutbox.id, id), eq(hierarchyOutbox.status, 'processing'))); + } + + /** Return a claimed record to pending (delivery failed; it stays replayable). */ + async releaseOutbox(id: string): Promise { + await this.db + .update(hierarchyOutbox) + .set({ status: 'pending', updatedAt: new Date() }) + .where(and(eq(hierarchyOutbox.id, id), eq(hierarchyOutbox.status, 'processing'))); + } +} diff --git a/apps/gateway/src/hierarchy/hierarchy.module.ts b/apps/gateway/src/hierarchy/hierarchy.module.ts new file mode 100644 index 00000000..71ac8e49 --- /dev/null +++ b/apps/gateway/src/hierarchy/hierarchy.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { HierarchyAuditRepository } from './hierarchy-audit.repository.js'; + +/** + * Hierarchy (tenancy/authorization structure) feature module. + * + * M4-1b-i ships the audit event + outbox machinery only (contract 1 §5.2). + * The hierarchy command family — controllers, DTOs, and the allowlisted + * class-table repositories — lands in M4-1b-ii once contract 2 (RBAC grant + * model) merges; until then this module exposes no routes, which the + * route-inventory witness asserts. + */ +@Module({ + providers: [HierarchyAuditRepository], + exports: [HierarchyAuditRepository], +}) +export class HierarchyModule {} diff --git a/packages/db/drizzle/0019_volatile_killraven.sql b/packages/db/drizzle/0019_volatile_killraven.sql new file mode 100644 index 00000000..e47049ee --- /dev/null +++ b/packages/db/drizzle/0019_volatile_killraven.sql @@ -0,0 +1,40 @@ +CREATE TYPE "public"."hierarchy_outbox_status" AS ENUM('pending', 'processing', 'delivered');--> statement-breakpoint +CREATE TABLE "hierarchy_audit_events" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "seq" bigint GENERATED ALWAYS AS IDENTITY (sequence name "hierarchy_audit_events_seq_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "actor_id" text NOT NULL, + "verb" text NOT NULL, + "target_kind" text NOT NULL, + "target_id" uuid NOT NULL, + "target_snapshot" jsonb NOT NULL, + "transfer_from" jsonb, + "transfer_to" jsonb, + "correlation_id" text NOT NULL, + "causation_id" uuid, + "idempotency_key" text NOT NULL, + "occurred_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "hierarchy_audit_events_verb_check" CHECK (verb IN ('create', 'rename', 'transfer', 'delete', 'grant_create', 'grant_change', 'grant_revoke')), + CONSTRAINT "hierarchy_audit_events_target_kind_check" CHECK (target_kind IN ('company', 'estate', 'platform_project', 'grant')), + CONSTRAINT "hierarchy_audit_events_transfer_check" CHECK ((verb = 'transfer') = (transfer_from IS NOT NULL AND transfer_to IS NOT NULL)) +); +--> statement-breakpoint +CREATE TABLE "hierarchy_outbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "event_id" uuid NOT NULL, + "idempotency_key" text NOT NULL, + "correlation_id" text NOT NULL, + "status" "hierarchy_outbox_status" DEFAULT 'pending' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "delivered_at" timestamp with time zone +); +--> statement-breakpoint +ALTER TABLE "hierarchy_audit_events" ADD CONSTRAINT "hierarchy_audit_events_causation_id_hierarchy_audit_events_id_fk" FOREIGN KEY ("causation_id") REFERENCES "public"."hierarchy_audit_events"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hierarchy_outbox" ADD CONSTRAINT "hierarchy_outbox_event_id_hierarchy_audit_events_id_fk" FOREIGN KEY ("event_id") REFERENCES "public"."hierarchy_audit_events"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "hierarchy_audit_events_idempotency_idx" ON "hierarchy_audit_events" USING btree ("idempotency_key");--> statement-breakpoint +CREATE UNIQUE INDEX "hierarchy_audit_events_seq_idx" ON "hierarchy_audit_events" USING btree ("seq");--> statement-breakpoint +CREATE INDEX "hierarchy_audit_events_target_seq_idx" ON "hierarchy_audit_events" USING btree ("target_id","seq");--> statement-breakpoint +CREATE INDEX "hierarchy_audit_events_correlation_idx" ON "hierarchy_audit_events" USING btree ("correlation_id");--> statement-breakpoint +CREATE UNIQUE INDEX "hierarchy_outbox_event_idx" ON "hierarchy_outbox" USING btree ("event_id");--> statement-breakpoint +CREATE UNIQUE INDEX "hierarchy_outbox_idempotency_idx" ON "hierarchy_outbox" USING btree ("idempotency_key");--> statement-breakpoint +CREATE INDEX "hierarchy_outbox_status_created_idx" ON "hierarchy_outbox" USING btree ("status","created_at"); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0019_snapshot.json b/packages/db/drizzle/meta/0019_snapshot.json new file mode 100644 index 00000000..63299c3a --- /dev/null +++ b/packages/db/drizzle/meta/0019_snapshot.json @@ -0,0 +1,5373 @@ +{ + "id": "81949b09-c995-41b6-b35e-40d73ae85975", + "prevId": "12c270b3-4f57-4f30-bcfd-6adddca4263a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_tokens": { + "name": "admin_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_tokens_user_id_idx": { + "name": "admin_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_tokens_hash_idx": { + "name": "admin_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_tokens_user_id_users_id_fk": { + "name": "admin_tokens_user_id_users_id_fk", + "tableFrom": "admin_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_logs": { + "name": "agent_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'hot'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "summarized_at": { + "name": "summarized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_logs_session_tier_idx": { + "name": "agent_logs_session_tier_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_user_id_idx": { + "name": "agent_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_tier_created_at_idx": { + "name": "agent_logs_tier_created_at_idx", + "columns": [ + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_logs_user_id_users_id_fk": { + "name": "agent_logs_user_id_users_id_fk", + "tableFrom": "agent_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_project_id_idx": { + "name": "agents_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_owner_id_idx": { + "name": "agents_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_is_system_idx": { + "name": "agents_is_system_idx", + "columns": [ + { + "expression": "is_system", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_project_id_projects_id_fk": { + "name": "agents_project_id_projects_id_fk", + "tableFrom": "agents", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_owner_id_users_id_fk": { + "name": "agents_owner_id_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appreciations": { + "name": "appreciations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "from_user": { + "name": "from_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_user": { + "name": "to_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backlog": { + "name": "backlog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "backlog_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_ttl_seconds": { + "name": "claim_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance": { + "name": "acceptance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "backlog_status_priority_idx": { + "name": "backlog_status_priority_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_status_claimed_at_idx": { + "name": "backlog_status_claimed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_idempotency_key_idx": { + "name": "backlog_idempotency_key_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "companies_slug_unique": { + "name": "companies_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_lease_audit_log": { + "name": "connector_lease_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logical_agent_id": { + "name": "logical_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_id": { + "name": "binding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lease_id": { + "name": "lease_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_epoch": { + "name": "lease_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connector_lease_audit_binding_occurred_idx": { + "name": "connector_lease_audit_binding_occurred_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "logical_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connector_lease_audit_correlation_idx": { + "name": "connector_lease_audit_correlation_idx", + "columns": [ + { + "expression": "correlation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conversations_user_archived_idx": { + "name": "conversations_user_archived_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_project_id_idx": { + "name": "conversations_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_agent_id_idx": { + "name": "conversations_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_user_id_users_id_fk": { + "name": "conversations_user_id_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_project_id_projects_id_fk": { + "name": "conversations_project_id_projects_id_fk", + "tableFrom": "conversations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_agent_id_agents_id_fk": { + "name": "conversations_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.estates": { + "name": "estates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "estates_company_id_companies_id_fk": { + "name": "estates_company_id_companies_id_fk", + "tableFrom": "estates", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "estates_company_slug_uniq": { + "name": "estates_company_slug_uniq", + "nullsNotDistinct": false, + "columns": [ + "company_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "events_type_idx": { + "name": "events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_date_idx": { + "name": "events_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_audit_log": { + "name": "federation_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "verb": { + "name": "verb", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "denied_reason": { + "name": "denied_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "query_hash": { + "name": "query_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bytes_out": { + "name": "bytes_out", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_audit_log_peer_created_at_idx": { + "name": "federation_audit_log_peer_created_at_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_subject_created_at_idx": { + "name": "federation_audit_log_subject_created_at_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_created_at_idx": { + "name": "federation_audit_log_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_audit_log_peer_id_federation_peers_id_fk": { + "name": "federation_audit_log_peer_id_federation_peers_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_subject_user_id_users_id_fk": { + "name": "federation_audit_log_subject_user_id_users_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_grant_id_federation_grants_id_fk": { + "name": "federation_audit_log_grant_id_federation_grants_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_enrollment_tokens": { + "name": "federation_enrollment_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_enrollment_tokens_grant_id_federation_grants_id_fk": { + "name": "federation_enrollment_tokens_grant_id_federation_grants_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_enrollment_tokens_peer_id_federation_peers_id_fk": { + "name": "federation_enrollment_tokens_peer_id_federation_peers_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_grants": { + "name": "federation_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "grant_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_grants_subject_status_idx": { + "name": "federation_grants_subject_status_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_grants_peer_status_idx": { + "name": "federation_grants_peer_status_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_grants_subject_user_id_users_id_fk": { + "name": "federation_grants_subject_user_id_users_id_fk", + "tableFrom": "federation_grants", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_grants_peer_id_federation_peers_id_fk": { + "name": "federation_grants_peer_id_federation_peers_id_fk", + "tableFrom": "federation_grants", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_peers": { + "name": "federation_peers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "common_name": { + "name": "common_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_serial": { + "name": "cert_serial", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_not_after": { + "name": "cert_not_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "client_key_pem": { + "name": "client_key_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "peer_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "endpoint_url": { + "name": "endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_peers_cert_serial_idx": { + "name": "federation_peers_cert_serial_idx", + "columns": [ + { + "expression": "cert_serial", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_peers_state_idx": { + "name": "federation_peers_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_peers_common_name_unique": { + "name": "federation_peers_common_name_unique", + "nullsNotDistinct": false, + "columns": [ + "common_name" + ] + }, + "federation_peers_cert_serial_unique": { + "name": "federation_peers_cert_serial_unique", + "nullsNotDistinct": false, + "columns": [ + "cert_serial" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hierarchy_audit_events": { + "name": "hierarchy_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "hierarchy_audit_events_seq_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verb": { + "name": "verb", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_snapshot": { + "name": "target_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "transfer_from": { + "name": "transfer_from", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "transfer_to": { + "name": "transfer_to", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "causation_id": { + "name": "causation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hierarchy_audit_events_idempotency_idx": { + "name": "hierarchy_audit_events_idempotency_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_audit_events_seq_idx": { + "name": "hierarchy_audit_events_seq_idx", + "columns": [ + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_audit_events_target_seq_idx": { + "name": "hierarchy_audit_events_target_seq_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_audit_events_correlation_idx": { + "name": "hierarchy_audit_events_correlation_idx", + "columns": [ + { + "expression": "correlation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hierarchy_audit_events_causation_id_hierarchy_audit_events_id_fk": { + "name": "hierarchy_audit_events_causation_id_hierarchy_audit_events_id_fk", + "tableFrom": "hierarchy_audit_events", + "tableTo": "hierarchy_audit_events", + "columnsFrom": [ + "causation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "hierarchy_audit_events_verb_check": { + "name": "hierarchy_audit_events_verb_check", + "value": "verb IN ('create', 'rename', 'transfer', 'delete', 'grant_create', 'grant_change', 'grant_revoke')" + }, + "hierarchy_audit_events_target_kind_check": { + "name": "hierarchy_audit_events_target_kind_check", + "value": "target_kind IN ('company', 'estate', 'platform_project', 'grant')" + }, + "hierarchy_audit_events_transfer_check": { + "name": "hierarchy_audit_events_transfer_check", + "value": "(verb = 'transfer') = (transfer_from IS NOT NULL AND transfer_to IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.hierarchy_grants": { + "name": "hierarchy_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "estate_id": { + "name": "estate_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hierarchy_grants_company_id_idx": { + "name": "hierarchy_grants_company_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_estate_id_idx": { + "name": "hierarchy_grants_estate_id_idx", + "columns": [ + { + "expression": "estate_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_platform_project_id_idx": { + "name": "hierarchy_grants_platform_project_id_idx", + "columns": [ + { + "expression": "platform_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_user_id_idx": { + "name": "hierarchy_grants_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_team_id_idx": { + "name": "hierarchy_grants_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_grants_granted_by_idx": { + "name": "hierarchy_grants_granted_by_idx", + "columns": [ + { + "expression": "granted_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hierarchy_grants_user_id_users_id_fk": { + "name": "hierarchy_grants_user_id_users_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "hierarchy_grants_team_id_teams_id_fk": { + "name": "hierarchy_grants_team_id_teams_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "hierarchy_grants_company_id_companies_id_fk": { + "name": "hierarchy_grants_company_id_companies_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hierarchy_grants_estate_id_estates_id_fk": { + "name": "hierarchy_grants_estate_id_estates_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "estates", + "columnsFrom": [ + "estate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hierarchy_grants_platform_project_id_platform_projects_id_fk": { + "name": "hierarchy_grants_platform_project_id_platform_projects_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "platform_projects", + "columnsFrom": [ + "platform_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hierarchy_grants_granted_by_users_id_fk": { + "name": "hierarchy_grants_granted_by_users_id_fk", + "tableFrom": "hierarchy_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hierarchy_grants_subject_target_role_uniq": { + "name": "hierarchy_grants_subject_target_role_uniq", + "nullsNotDistinct": true, + "columns": [ + "user_id", + "team_id", + "company_id", + "estate_id", + "platform_project_id", + "role" + ] + } + }, + "policies": {}, + "checkConstraints": { + "hierarchy_grants_subject_check": { + "name": "hierarchy_grants_subject_check", + "value": "num_nonnulls(user_id, team_id) = 1" + }, + "hierarchy_grants_target_check": { + "name": "hierarchy_grants_target_check", + "value": "num_nonnulls(company_id, estate_id, platform_project_id) = 1" + } + }, + "isRLSEnabled": false + }, + "public.hierarchy_outbox": { + "name": "hierarchy_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "hierarchy_outbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "hierarchy_outbox_event_idx": { + "name": "hierarchy_outbox_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_outbox_idempotency_idx": { + "name": "hierarchy_outbox_idempotency_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hierarchy_outbox_status_created_idx": { + "name": "hierarchy_outbox_status_created_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hierarchy_outbox_event_id_hierarchy_audit_events_id_fk": { + "name": "hierarchy_outbox_event_id_hierarchy_audit_events_id_fk", + "tableFrom": "hierarchy_outbox", + "tableTo": "hierarchy_audit_events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.insights": { + "name": "insights", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "relevance_score": { + "name": "relevance_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decayed_at": { + "name": "decayed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "insights_user_id_idx": { + "name": "insights_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_category_idx": { + "name": "insights_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_relevance_idx": { + "name": "insights_relevance_idx", + "columns": [ + { + "expression": "relevance_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insights_user_id_users_id_fk": { + "name": "insights_user_id_users_id_fk", + "tableFrom": "insights", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_checkpoints": { + "name": "interaction_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_digest": { + "name": "content_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "compaction_epoch": { + "name": "compaction_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_checkpoints_session_idempotency_idx": { + "name": "interaction_checkpoints_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_checkpoints_session_epoch_idx": { + "name": "interaction_checkpoints_session_epoch_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "compaction_epoch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_checkpoints_session_id_interaction_sessions_id_fk": { + "name": "interaction_checkpoints_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_checkpoints", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_handoffs": { + "name": "interaction_handoffs", + "schema": "", + "columns": { + "handoff_id": { + "name": "handoff_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_handoff_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_handoffs_session_status_idx": { + "name": "interaction_handoffs_session_status_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_handoffs_session_id_interaction_sessions_id_fk": { + "name": "interaction_handoffs_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_handoffs", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_inbox": { + "name": "interaction_inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_digest": { + "name": "content_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_inbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_inbox_session_idempotency_idx": { + "name": "interaction_inbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_inbox_session_status_created_idx": { + "name": "interaction_inbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_inbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_inbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_inbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_outbox": { + "name": "interaction_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_digest": { + "name": "content_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_outbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_outbox_session_idempotency_idx": { + "name": "interaction_outbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_outbox_session_status_created_idx": { + "name": "interaction_outbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_outbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_outbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_outbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_sessions": { + "name": "interaction_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_session_id": { + "name": "runtime_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "interaction_sessions_owner_id_users_id_fk": { + "name": "interaction_sessions_owner_id_users_id_fk", + "tableFrom": "interaction_sessions", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.logical_agent_connector_leases": { + "name": "logical_agent_connector_leases", + "schema": "", + "columns": { + "lease_id": { + "name": "lease_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logical_agent_id": { + "name": "logical_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_id": { + "name": "binding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "lease_epoch": { + "name": "lease_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "acquired_at": { + "name": "acquired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "logical_agent_connector_lease_binding_idx": { + "name": "logical_agent_connector_lease_binding_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "logical_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "binding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "logical_agent_connector_lease_expiry_idx": { + "name": "logical_agent_connector_lease_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "logical_agent_connector_lease_connector_idx": { + "name": "logical_agent_connector_lease_connector_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_conversation_id_idx": { + "name": "messages_conversation_id_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mission_tasks": { + "name": "mission_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr": { + "name": "pr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mission_tasks_mission_id_idx": { + "name": "mission_tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_task_id_idx": { + "name": "mission_tasks_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_user_id_idx": { + "name": "mission_tasks_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_status_idx": { + "name": "mission_tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mission_tasks_mission_id_missions_id_fk": { + "name": "mission_tasks_mission_id_missions_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mission_tasks_task_id_tasks_id_fk": { + "name": "mission_tasks_task_id_tasks_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mission_tasks_user_id_users_id_fk": { + "name": "mission_tasks_user_id_users_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions": { + "name": "missions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planning'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "milestones": { + "name": "milestones", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "missions_project_id_idx": { + "name": "missions_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_user_id_idx": { + "name": "missions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_project_id_projects_id_fk": { + "name": "missions_project_id_projects_id_fk", + "tableFrom": "missions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_user_id_users_id_fk": { + "name": "missions_user_id_users_id_fk", + "tableFrom": "missions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_projects": { + "name": "platform_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estate_id": { + "name": "estate_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "platform_projects_estate_id_estates_id_fk": { + "name": "platform_projects_estate_id_estates_id_fk", + "tableFrom": "platform_projects", + "tableTo": "estates", + "columnsFrom": [ + "estate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "platform_projects_estate_slug_uniq": { + "name": "platform_projects_estate_slug_uniq", + "nullsNotDistinct": false, + "columns": [ + "estate_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preferences": { + "name": "preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mutable": { + "name": "mutable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "preferences_user_id_idx": { + "name": "preferences_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "preferences_user_key_idx": { + "name": "preferences_user_key_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "preferences_user_id_users_id_fk": { + "name": "preferences_user_id_users_id_fk", + "tableFrom": "preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_credentials": { + "name": "provider_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_credentials_user_provider_idx": { + "name": "provider_credentials_user_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_credentials_user_id_idx": { + "name": "provider_credentials_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_credentials_user_id_users_id_fk": { + "name": "provider_credentials_user_id_users_id_fk", + "tableFrom": "provider_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routing_rules": { + "name": "routing_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routing_rules_scope_priority_idx": { + "name": "routing_rules_scope_priority_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_user_id_idx": { + "name": "routing_rules_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_enabled_idx": { + "name": "routing_rules_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routing_rules_user_id_users_id_fk": { + "name": "routing_rules_user_id_users_id_fk", + "tableFrom": "routing_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "installed_by": { + "name": "installed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_enabled_idx": { + "name": "skills_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_installed_by_users_id_fk": { + "name": "skills_installed_by_users_id_fk", + "tableFrom": "skills", + "tableTo": "users", + "columnsFrom": [ + "installed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "skills_name_unique": { + "name": "skills_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summarization_jobs": { + "name": "summarization_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "logs_processed": { + "name": "logs_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "insights_created": { + "name": "insights_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "summarization_jobs_status_idx": { + "name": "summarization_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee": { + "name": "assignee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_project_id_idx": { + "name": "tasks_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_mission_id_idx": { + "name": "tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_mission_id_missions_id_fk": { + "name": "tasks_mission_id_missions_id_fk", + "tableFrom": "tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_team_user_idx": { + "name": "team_members_team_user_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_invited_by_users_id_fk": { + "name": "team_members_invited_by_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "teams_manager_id_users_id_fk": { + "name": "teams_manager_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspaces_platform_project_id_platform_projects_id_fk": { + "name": "workspaces_platform_project_id_platform_projects_id_fk", + "tableFrom": "workspaces", + "tableTo": "platform_projects", + "columnsFrom": [ + "platform_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspaces_platform_project_slug_uniq": { + "name": "workspaces_platform_project_slug_uniq", + "nullsNotDistinct": false, + "columns": [ + "platform_project_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.backlog_status": { + "name": "backlog_status", + "schema": "public", + "values": [ + "ready", + "claimed", + "blocked", + "done" + ] + }, + "public.grant_status": { + "name": "grant_status", + "schema": "public", + "values": [ + "pending", + "active", + "revoked", + "expired" + ] + }, + "public.hierarchy_outbox_status": { + "name": "hierarchy_outbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "delivered" + ] + }, + "public.interaction_handoff_status": { + "name": "interaction_handoff_status", + "schema": "public", + "values": [ + "pending", + "accepted" + ] + }, + "public.interaction_inbox_status": { + "name": "interaction_inbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed" + ] + }, + "public.interaction_outbox_status": { + "name": "interaction_outbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "delivered" + ] + }, + "public.peer_state": { + "name": "peer_state", + "schema": "public", + "values": [ + "pending", + "active", + "suspended", + "revoked" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index b47130a1..c09e4691 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -134,6 +134,13 @@ "when": 1787862158838, "tag": "0018_clean_cobalt_man", "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1787880918208, + "tag": "0019_volatile_killraven", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/hierarchy-audit.witness.test.ts b/packages/db/src/hierarchy-audit.witness.test.ts new file mode 100644 index 00000000..4b6543e5 --- /dev/null +++ b/packages/db/src/hierarchy-audit.witness.test.ts @@ -0,0 +1,363 @@ +/** + * Hierarchy audit event + outbox schema witnesses — contract 1 §5.2 and the + * schema-level half of §6.4. + * + * Witnesses the guarantees the tables themselves carry: verb/target-kind/ + * transfer CHECK constraints, idempotency uniqueness (REQ-AUD-001 duplicate + * suppression at the database level), monotonic append order (`seq`), + * deletion-safe linkage (no foreign key from the events table into any class + * table — events survive the deletion of their target), the causation + * self-FK, and the outbox's FK/uniqueness/status shape. The repository-level + * half of §6.4 (same-transaction atomicity, rollback, replay) is witnessed in + * apps/gateway/src/hierarchy/hierarchy-audit.integration.test.ts. + * + * Two legs run the same witness body: + * - PGlite (WASM Postgres): always runs. + * - Real PostgreSQL (§6.8): runs when DATABASE_URL is set — the binding + * witness; CI migrates ci-postgres before `pnpm test`. + */ +import { randomUUID } from 'node:crypto'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { sql } from 'drizzle-orm'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { createDb } from './client.js'; +import { createPgliteDb } from './client-pglite.js'; +import { runPgliteMigrations } from './migrate.js'; +import { companies, hierarchyAuditEvents, hierarchyOutbox } from './schema.js'; + +type AnyDb = { + db: { + insert: (t: unknown) => { values: (v: unknown) => Promise }; + execute: (q: unknown) => Promise<{ rows?: unknown[] } | unknown[]>; + }; + close: () => Promise; +}; + +/** Match a constraint failure anywhere along drizzle's cause chain. */ +async function expectViolation(p: Promise, re: RegExp, label = ''): Promise { + let err: unknown; + try { + await p; + } catch (e) { + err = e; + } + expect(err, label || 'expected the statement to be refused').toBeDefined(); + const messages: string[] = []; + let cur: unknown = err; + while (cur instanceof Error) { + messages.push(cur.message); + cur = (cur as { cause?: unknown }).cause; + } + expect(messages.join(' | '), label).toMatch(re); +} + +function rows(res: { rows?: unknown[] } | unknown[]): Record[] { + return (Array.isArray(res) ? res : (res.rows ?? [])) as Record[]; +} + +/** Unique per-run prefix so real-PG runs never collide and clean up safely. */ +const T = `hier-a-${randomUUID().slice(0, 8)}`; + +/** Minimal valid event row; overrides compose the negative cases. */ +type EventInsert = typeof hierarchyAuditEvents.$inferInsert; + +function eventRow(overrides: Partial = {}): EventInsert { + return { + actorId: `${T}-actor`, + verb: 'create', + targetKind: 'company', + targetId: randomUUID(), + targetSnapshot: { id: 'x', slug: 'x', name: 'x', parentChain: [] }, + correlationId: `${T}-corr`, + idempotencyKey: `${T}-${randomUUID()}`, + ...overrides, + }; +} + +function witnessSuite(getHandle: () => AnyDb): void { + const db = () => getHandle().db as unknown as ReturnType['db']; + + afterAll(async () => { + const d = db(); + await d.execute(sql`DELETE FROM hierarchy_outbox WHERE idempotency_key LIKE ${T + '%'}`); + // Caused events first: the causation self-FK is RESTRICT. + await d.execute( + sql`DELETE FROM hierarchy_audit_events WHERE idempotency_key LIKE ${T + '%'} AND causation_id IS NOT NULL`, + ); + await d.execute(sql`DELETE FROM hierarchy_audit_events WHERE idempotency_key LIKE ${T + '%'}`); + await d.execute(sql`DELETE FROM companies WHERE slug LIKE ${T + '%'}`); + }); + + // ── CHECK constraints ────────────────────────────────────────────────────── + + it('accepts every declared verb and refuses an undeclared one', async () => { + for (const verb of [ + 'create', + 'rename', + 'delete', + 'grant_create', + 'grant_change', + 'grant_revoke', + ]) { + await db().insert(hierarchyAuditEvents).values(eventRow({ verb })); + } + await expectViolation( + db() + .insert(hierarchyAuditEvents) + .values(eventRow({ verb: 'update' })), + /verb_check|violates check/i, + 'undeclared verb must be refused', + ); + }); + + it('refuses an undeclared target kind', async () => { + await expectViolation( + db() + .insert(hierarchyAuditEvents) + .values(eventRow({ targetKind: 'workspace' })), + /target_kind_check|violates check/i, + 'workspace is not an audited target kind (workspace mutation is SOT-side)', + ); + }); + + it('requires transfer snapshots exactly on transfers', async () => { + const parent = { kind: 'company', id: randomUUID(), slug: 'p' }; + await db() + .insert(hierarchyAuditEvents) + .values( + eventRow({ + verb: 'transfer', + targetKind: 'estate', + transferFrom: parent, + transferTo: { ...parent, id: randomUUID() }, + }), + ); + await expectViolation( + db() + .insert(hierarchyAuditEvents) + .values(eventRow({ verb: 'transfer' })), + /transfer_check|violates check/i, + 'transfer without source/destination snapshots must be refused', + ); + await expectViolation( + db() + .insert(hierarchyAuditEvents) + .values(eventRow({ verb: 'transfer', transferFrom: parent })), + /transfer_check|violates check/i, + 'transfer with only the source snapshot must be refused', + ); + await expectViolation( + db() + .insert(hierarchyAuditEvents) + .values(eventRow({ verb: 'create', transferFrom: parent, transferTo: parent })), + /transfer_check|violates check/i, + 'non-transfer with transfer snapshots must be refused', + ); + }); + + // ── Idempotency and ordering ─────────────────────────────────────────────── + + it('refuses a duplicate idempotency key', async () => { + const key = `${T}-dup-${randomUUID()}`; + await db() + .insert(hierarchyAuditEvents) + .values(eventRow({ idempotencyKey: key })); + await expectViolation( + db() + .insert(hierarchyAuditEvents) + .values(eventRow({ idempotencyKey: key })), + /duplicate key|unique/i, + ); + }); + + it('assigns strictly increasing seq in insert order for one target', async () => { + const targetId = randomUUID(); + const k1 = `${T}-seq-1-${randomUUID()}`; + const k2 = `${T}-seq-2-${randomUUID()}`; + await db() + .insert(hierarchyAuditEvents) + .values(eventRow({ targetId, idempotencyKey: k1 })); + await db() + .insert(hierarchyAuditEvents) + .values(eventRow({ targetId, verb: 'rename', idempotencyKey: k2 })); + const res = rows( + await db().execute( + sql`SELECT idempotency_key, seq FROM hierarchy_audit_events WHERE target_id = ${targetId} ORDER BY seq ASC`, + ), + ); + expect(res.map((r) => r['idempotency_key'])).toEqual([k1, k2]); + expect(Number(res[1]!['seq'])).toBeGreaterThan(Number(res[0]!['seq'])); + }); + + // ── Deletion-safe linkage (§5.2) ─────────────────────────────────────────── + + it('has no foreign key into any class table, and events survive target deletion', async () => { + const fks = rows( + await db().execute(sql` + SELECT ccu.table_name AS referenced_table + FROM information_schema.table_constraints tc + JOIN information_schema.constraint_column_usage ccu + ON ccu.constraint_name = tc.constraint_name AND ccu.constraint_schema = tc.constraint_schema + WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = 'hierarchy_audit_events' + `), + ); + // The causation self-FK is the ONLY foreign key on the events table. + expect([...new Set(fks.map((r) => r['referenced_table']))]).toEqual(['hierarchy_audit_events']); + + const companyId = randomUUID(); + await db() + .insert(companies) + .values({ id: companyId, name: 'Doomed', slug: `${T}-doomed` }); + const key = `${T}-survive-${randomUUID()}`; + await db() + .insert(hierarchyAuditEvents) + .values( + eventRow({ + verb: 'delete', + targetId: companyId, + targetSnapshot: { id: companyId, slug: `${T}-doomed`, name: 'Doomed', parentChain: [] }, + idempotencyKey: key, + }), + ); + await db().execute(sql`DELETE FROM companies WHERE id = ${companyId}`); + const after = rows( + await db().execute( + sql`SELECT target_snapshot FROM hierarchy_audit_events WHERE idempotency_key = ${key}`, + ), + ); + expect(after).toHaveLength(1); + expect((after[0]!['target_snapshot'] as { id: string }).id).toBe(companyId); + }); + + it('enforces the causation self-FK and RESTRICTs deleting a cause', async () => { + await expectViolation( + db() + .insert(hierarchyAuditEvents) + .values(eventRow({ causationId: randomUUID() })), + /foreign key/i, + 'causation must reference an existing event', + ); + const causeKey = `${T}-cause-${randomUUID()}`; + await db() + .insert(hierarchyAuditEvents) + .values(eventRow({ verb: 'delete', idempotencyKey: causeKey })); + const cause = rows( + await db().execute( + sql`SELECT id FROM hierarchy_audit_events WHERE idempotency_key = ${causeKey}`, + ), + )[0]!; + await db() + .insert(hierarchyAuditEvents) + .values( + eventRow({ verb: 'grant_revoke', targetKind: 'grant', causationId: cause['id'] as string }), + ); + await expectViolation( + db().execute(sql`DELETE FROM hierarchy_audit_events WHERE id = ${cause['id'] as string}`), + /foreign key/i, + 'a cause with dependent events must not be deletable', + ); + }); + + // ── Outbox shape ─────────────────────────────────────────────────────────── + + it('outbox rows require an existing event, one outbox row per event, unique idempotency', async () => { + await expectViolation( + db() + .insert(hierarchyOutbox) + .values({ + eventId: randomUUID(), + idempotencyKey: `${T}-ob-${randomUUID()}`, + correlationId: `${T}-corr`, + }), + /foreign key/i, + 'outbox must reference an existing event', + ); + const key = `${T}-ob-${randomUUID()}`; + await db() + .insert(hierarchyAuditEvents) + .values(eventRow({ idempotencyKey: key })); + const event = rows( + await db().execute(sql`SELECT id FROM hierarchy_audit_events WHERE idempotency_key = ${key}`), + )[0]!; + const eventId = event['id'] as string; + await db() + .insert(hierarchyOutbox) + .values({ eventId, idempotencyKey: key, correlationId: `${T}-corr` }); + await expectViolation( + db() + .insert(hierarchyOutbox) + .values({ eventId, idempotencyKey: `${T}-ob2-${randomUUID()}`, correlationId: `${T}-c` }), + /duplicate key|unique/i, + 'one outbox record per event', + ); + await expectViolation( + db().execute( + sql`INSERT INTO hierarchy_outbox (event_id, idempotency_key, correlation_id, status) + VALUES (${eventId}, ${`${T}-ob3-${randomUUID()}`}, 'c', 'failed')`, + ), + /invalid input value for enum|22P02/i, + 'status outside pending/processing/delivered must be refused', + ); + }); + + it('outbox FK RESTRICTs event deletion while the outbox row exists', async () => { + const key = `${T}-obr-${randomUUID()}`; + await db() + .insert(hierarchyAuditEvents) + .values(eventRow({ idempotencyKey: key })); + const event = rows( + await db().execute(sql`SELECT id FROM hierarchy_audit_events WHERE idempotency_key = ${key}`), + )[0]!; + await db() + .insert(hierarchyOutbox) + .values({ + eventId: event['id'] as string, + idempotencyKey: key, + correlationId: `${T}-corr`, + }); + await expectViolation( + db().execute(sql`DELETE FROM hierarchy_audit_events WHERE id = ${event['id'] as string}`), + /foreign key/i, + ); + }); +} + +// ── Leg 1: PGlite (always runs — local witness signal) ─────────────────────── + +describe('hierarchy audit witnesses — PGlite', () => { + let dir: string; + let handle: ReturnType; + + beforeAll(async () => { + dir = mkdtempSync(join(tmpdir(), 'hier-audit-witness-')); + handle = createPgliteDb(dir); + await runPgliteMigrations(handle); + }); + + afterAll(async () => { + await handle.close(); + rmSync(dir, { recursive: true, force: true }); + }); + + witnessSuite(() => handle as unknown as AnyDb); +}); + +// ── Leg 2: real PostgreSQL (§6.8 — binding witness, ci-postgres in CI) ─────── + +const hasPostgres = Boolean(process.env['DATABASE_URL']); + +describe.skipIf(!hasPostgres)('hierarchy audit witnesses — real PostgreSQL', () => { + let handle: ReturnType; + + beforeAll(() => { + handle = createDb(process.env['DATABASE_URL']!); + }); + + afterAll(async () => { + await handle.close(); + }); + + witnessSuite(() => handle as unknown as AnyDb); +}); diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index dc3bf0b2..97f1e649 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -123,18 +123,20 @@ * tracked for M4-1b consideration); the counterfactual — flagging every * bare identifier call — false-positives on essentially all callback * code. Reviews of modules touching db handles carry this residual. - * - The scan perimeter is //src for the three roots; production - * TS outside a src/ directory (e.g. packages/mosaic/framework/**) is not - * scanned (verified free of db/driver/execute references at review time). - * Files excluded from the scan — test files and out-of-src modules — are - * also invisible as import-graph CONDUITS: test files are emitted to - * dist, so a production module could launder a symbol or capability - * through a re-export in one. Importing a test module from production - * code is anomalous and review-visible; the blind spot is accepted as a - * residual, not closed. + * - The scan perimeter is the full / tree for the three roots + * (build output, tool caches, and dot-directories excluded), so + * production TS outside src/ — package configs, e2e helpers, + * packages/mosaic/framework/** — is scanned and conduit-visible + * (widened from src/-only in M4-1b-i; the widened set was measured free + * of every trigger token at the time). Files excluded from the scan — + * test files — are still invisible as import-graph CONDUITS: test files + * are emitted to dist, so a production module could launder a symbol or + * capability through a re-export in one. Importing a test module from + * production code is anomalous and review-visible; that blind spot is + * accepted as a residual, not closed. * * The writer allowlist names hierarchy command/repository modules ONLY. It is - * empty today: the hierarchy command family (M4-1b) has not landed, so no + * empty today: the hierarchy command family (M4-1b-ii) has not landed, so no * production module may write the class tables. The infrastructure register * holds legitimate non-hierarchy raw execution; registered modules are exempt * from prong (iii) only — prongs (i) and (ii) apply to them with no @@ -179,7 +181,8 @@ const CLASS_TABLES = [ /** * Writer allowlist (§6.3b): hierarchy command/repository modules only. - * EMPTY until the hierarchy command family lands (M4-1b). Adding a module + * EMPTY until the hierarchy command family lands (M4-1b-ii; M4-1b-i ships + * only the audit/outbox machinery, which writes no class table). Adding a module * here is a contract-conformance decision reviewed under §5.1 — the module * must be part of the Gateway hierarchy command path, and it must not export * a function that executes caller-supplied SQL. @@ -291,20 +294,23 @@ function isTestPath(rel: string): boolean { ); } +/** Directory names excluded from the walk: build output and tool caches only. */ +const EXCLUDED_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', 'test-results']); + function collectSources(): string[] { const files: string[] = []; for (const root of SCAN_ROOTS) { const rootDir = join(REPO_ROOT, root); if (!existsSync(rootDir)) continue; for (const pkg of readdirSync(rootDir)) { - const srcDir = join(rootDir, pkg, 'src'); - if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) continue; + const pkgDir = join(rootDir, pkg); + if (!statSync(pkgDir).isDirectory()) continue; const walk = (dir: string): void => { for (const entry of readdirSync(dir)) { const full = join(dir, entry); const st = statSync(full); if (st.isDirectory()) { - if (entry === 'node_modules' || entry === 'dist') continue; + if (EXCLUDED_DIRS.has(entry) || entry.startsWith('.')) continue; walk(full); } else if (EXTENSIONS.has(full.slice(full.lastIndexOf('.')))) { const rel = relative(REPO_ROOT, full).split(sep).join('/'); @@ -312,7 +318,7 @@ function collectSources(): string[] { } } }; - walk(srcDir); + walk(pkgDir); } } return files.sort(); diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index d2358583..78bab662 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -4,6 +4,7 @@ */ import { sql } from 'drizzle-orm'; +import type { AnyPgColumn } from 'drizzle-orm/pg-core'; import { pgTable, pgEnum, @@ -1152,3 +1153,111 @@ export const hierarchyGrants = pgTable( index('hierarchy_grants_granted_by_idx').on(t.grantedBy), ], ); + +// ─── Hierarchy audit events + outbox (contract 1 §5.2) ────────────────────── +// NOT part of the record class (the class is exactly the five tables above). +// Append-only semantic audit log for hierarchy mutations, with a dedicated +// transactional outbox — hierarchy events are not workspace-scoped rows and +// do not ride the workspace outbox. Deletion-safe linkage: events reference +// their target by an immutable snapshot (id, slug, parent chain at event +// time), never by a foreign key into the class tables, so append-only events +// survive the deletion of their target. Append-only is enforced at the +// application layer (the hierarchy audit repository exposes no update/delete +// path for events); REQ-AUD-001's INSERT/SELECT-only database role is a +// deployment concern outside this schema. + +export const HIERARCHY_AUDIT_VERBS = [ + 'create', + 'rename', + 'transfer', + 'delete', + 'grant_create', + 'grant_change', + 'grant_revoke', +] as const; + +export const HIERARCHY_AUDIT_TARGET_KINDS = [ + 'company', + 'estate', + 'platform_project', + 'grant', +] as const; + +export const hierarchyAuditEvents = pgTable( + 'hierarchy_audit_events', + { + id: uuid('id').primaryKey().defaultRandom(), + // Global append order; per-target ordering (REQ-AUD-001) is a filter on + // target_id ordered by seq. + seq: bigint('seq', { mode: 'number' }).notNull().generatedAlwaysAsIdentity(), + // No FK: audit events outlive every principal and every target (§5.2). + actorId: text('actor_id').notNull(), + verb: text('verb').notNull(), + targetKind: text('target_kind').notNull(), + targetId: uuid('target_id').notNull(), + // Immutable snapshot at event time. Node events: { id, slug, name, + // parentChain: [{ kind, id, slug }, …] root-first }. Grant events: + // { id, subject: { userId | teamId }, target: { kind, id }, role } + // (subject and role per contract 2 §4.4). + targetSnapshot: jsonb('target_snapshot').notNull(), + // Present exactly on transfers: snapshot of the source/destination + // parent ({ kind, id, slug }), CHECK-enforced below. + transferFrom: jsonb('transfer_from'), + transferTo: jsonb('transfer_to'), + correlationId: text('correlation_id').notNull(), + // Prior event in the causal chain (e.g. cascaded grant_revoke events + // caused by a node delete). Self-FK RESTRICT keeps the chain intact. + causationId: uuid('causation_id').references((): AnyPgColumn => hierarchyAuditEvents.id, { + onDelete: 'restrict', + }), + idempotencyKey: text('idempotency_key').notNull(), + occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('hierarchy_audit_events_idempotency_idx').on(t.idempotencyKey), + uniqueIndex('hierarchy_audit_events_seq_idx').on(t.seq), + index('hierarchy_audit_events_target_seq_idx').on(t.targetId, t.seq), + index('hierarchy_audit_events_correlation_idx').on(t.correlationId), + check( + 'hierarchy_audit_events_verb_check', + sql`verb IN ('create', 'rename', 'transfer', 'delete', 'grant_create', 'grant_change', 'grant_revoke')`, + ), + check( + 'hierarchy_audit_events_target_kind_check', + sql`target_kind IN ('company', 'estate', 'platform_project', 'grant')`, + ), + check( + 'hierarchy_audit_events_transfer_check', + sql`(verb = 'transfer') = (transfer_from IS NOT NULL AND transfer_to IS NOT NULL)`, + ), + ], +); + +export const hierarchyOutboxStatusEnum = pgEnum('hierarchy_outbox_status', [ + 'pending', + 'processing', + 'delivered', +]); + +export const hierarchyOutbox = pgTable( + 'hierarchy_outbox', + { + id: uuid('id').primaryKey().defaultRandom(), + // FK into the append-only events table (not a class table): never + // dangles, so RESTRICT is safe and keeps event/outbox integrity. + eventId: uuid('event_id') + .notNull() + .references(() => hierarchyAuditEvents.id, { onDelete: 'restrict' }), + idempotencyKey: text('idempotency_key').notNull(), + correlationId: text('correlation_id').notNull(), + status: hierarchyOutboxStatusEnum('status').notNull().default('pending'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + deliveredAt: timestamp('delivered_at', { withTimezone: true }), + }, + (t) => [ + uniqueIndex('hierarchy_outbox_event_idx').on(t.eventId), + uniqueIndex('hierarchy_outbox_idempotency_idx').on(t.idempotencyKey), + index('hierarchy_outbox_status_created_idx').on(t.status, t.createdAt), + ], +);