/** * BI-P002 (domain model) + BI-P003 (data tiers), the parts provable without * infrastructure. The Postgres half — real tables, real immutability * triggers, real tenant scoping — is * tests/analytics/analytics-domain-postgres.realinfra.test.ts. * * The registry-vs-migration test below is the one that earns its place: it * reads sql/008_analytics_domain_model.sql from disk and asserts a CREATE * TABLE exists for every one of the nineteen canonical entities. A future * rename, or an entity quietly dropped from the migration, fails HERE rather * than at 3am against a database that turns out to be missing a table. */ import { describe, it, expect } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; import { ANALYTICS_DOMAIN_ENTITIES, ANALYTICS_ENTITY_TABLES, IMMUTABLE_VERSION_ENTITIES, } from '@/src/analytics/domain/entities'; import { ANALYTICS_TIERS, assertReadableByBiSurface, assertTierLineageSane, BI_READABLE_TIERS, isAnalyticsTier, TIER_RANK, } from '@/src/analytics/domain/tiers'; const MIGRATION = fs.readFileSync(path.join(process.cwd(), 'sql', '008_analytics_domain_model.sql'), 'utf8'); describe('BI-P002 canonical entity set', () => { it('lists exactly the nineteen entities the program names, verbatim', () => { expect(ANALYTICS_DOMAIN_ENTITIES).toEqual([ 'AnalyticsDataset', 'DatasetVersion', 'DatasetField', 'DatasetGrant', 'MetricDefinition', 'MetricVersion', 'DimensionDefinition', 'SemanticModel', 'AnalyticsQuery', 'QueryExecution', 'QueryResult', 'DashboardDefinition', 'DashboardVersion', 'ReportDefinition', 'ReportRun', 'DataQualityRule', 'DataQualityRun', 'LineageReference', 'BIApplicationBinding', ]); expect(ANALYTICS_DOMAIN_ENTITIES).toHaveLength(19); }); it('sql/008 defines a table for every entity (no entity is a type without storage)', () => { const missing: string[] = []; for (const entity of ANALYTICS_DOMAIN_ENTITIES) { const table = ANALYTICS_ENTITY_TABLES[entity]; if (!new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`).test(MIGRATION)) { missing.push(`${entity} -> ${table}`); } } expect(missing).toEqual([]); }); it('every tenant-owned table carries tenant_id AND project_id', () => { // Split on the table definitions and check each block. const blocks = MIGRATION.split(/CREATE TABLE IF NOT EXISTS /).slice(1); const offenders: string[] = []; for (const block of blocks) { const name = block.split(/[\s(]/)[0]; if (!Object.values(ANALYTICS_ENTITY_TABLES).includes(name)) continue; const body = block.slice(0, block.indexOf(');')); if (!/\btenant_id TEXT NOT NULL\b/.test(body) || !/\bproject_id TEXT NOT NULL\b/.test(body)) { offenders.push(name); } } expect(offenders).toEqual([]); }); it('the four immutable version entities have a BEFORE UPDATE OR DELETE trigger in sql/008', () => { expect(IMMUTABLE_VERSION_ENTITIES).toEqual([ 'DatasetVersion', 'MetricVersion', 'SemanticModel', 'DashboardVersion', ]); for (const entity of IMMUTABLE_VERSION_ENTITIES) { const table = ANALYTICS_ENTITY_TABLES[entity]; expect( new RegExp(`BEFORE UPDATE OR DELETE ON ${table}\\b`).test(MIGRATION), `${entity} (${table}) has no immutability trigger`, ).toBe(true); } }); it('BIApplicationBinding storage carries NO credential column — the ownership boundary is structural', () => { const block = MIGRATION.split('CREATE TABLE IF NOT EXISTS opt_analytics_bi_application_binding')[1] ?? ''; const body = block.slice(0, block.indexOf(');')); for (const forbidden of ['password', 'dsn', 'secret', 'connection_string', 'clickhouse_url', 'token']) { expect(body.toLowerCase()).not.toContain(forbidden); } }); it('sql/008 is idempotent-shaped: every CREATE is IF NOT EXISTS / OR REPLACE', () => { const creates = MIGRATION.match(/^CREATE (TABLE|INDEX|TRIGGER|FUNCTION|OR REPLACE FUNCTION)[^\n]*/gm) ?? []; const nonIdempotent = creates.filter( (line) => !/IF NOT EXISTS/.test(line) && !/OR REPLACE/.test(line) && !/^CREATE TRIGGER/.test(line), ); expect(nonIdempotent).toEqual([]); // Triggers cannot take IF NOT EXISTS in Postgres, so each one must be // preceded by a DROP TRIGGER IF EXISTS to stay replayable. const triggers = MIGRATION.match(/^CREATE TRIGGER (\w+)/gm) ?? []; for (const t of triggers) { const name = t.replace('CREATE TRIGGER ', ''); expect(MIGRATION).toContain(`DROP TRIGGER IF EXISTS ${name}`); } }); }); describe('BI-P003 analytics data tiers', () => { it('canonicalizes exactly RAW / NORMALIZED / CURATED / SERVING', () => { expect(ANALYTICS_TIERS).toEqual(['RAW', 'NORMALIZED', 'CURATED', 'SERVING']); expect(isAnalyticsTier('CURATED')).toBe(true); expect(isAnalyticsTier('raw')).toBe(false); expect(isAnalyticsTier('BRONZE')).toBe(false); }); it('no business dashboard reads a RAW/NORMALIZED dataset by default', () => { expect(BI_READABLE_TIERS).toEqual(['SERVING', 'CURATED']); for (const tier of ['RAW', 'NORMALIZED'] as const) { expect(() => assertReadableByBiSurface({ dataset_id: 'ds_1', tier }, 'dashboard:exec-overview'), ).toThrowError(/may not read/); } for (const tier of ['SERVING', 'CURATED'] as const) { expect(() => assertReadableByBiSurface({ dataset_id: 'ds_1', tier }, 'dashboard:x')).not.toThrow(); } }); it('a recorded exception is the only way a RAW dataset reaches a BI surface', () => { expect(() => assertReadableByBiSurface( { dataset_id: 'ds_1', tier: 'RAW', raw_access_exception: 'INC-2026-09-06 forensic review, expires 2026-10-01', }, 'dashboard:incident', ), ).not.toThrow(); // An empty/whitespace "exception" is not an exception. expect(() => assertReadableByBiSurface( { dataset_id: 'ds_1', tier: 'RAW', raw_access_exception: ' ' }, 'dashboard:x', ), ).toThrowError(/may not read/); }); it('tier order is enforced: a CURATED dataset cannot be built from SERVING', () => { expect(TIER_RANK.RAW).toBeLessThan(TIER_RANK.NORMALIZED); expect(TIER_RANK.NORMALIZED).toBeLessThan(TIER_RANK.CURATED); expect(TIER_RANK.CURATED).toBeLessThan(TIER_RANK.SERVING); expect(() => assertTierLineageSane('CURATED', 'SERVING')).toThrowError(/tier order/); expect(() => assertTierLineageSane('SERVING', 'CURATED')).not.toThrow(); expect(() => assertTierLineageSane('CURATED', 'CURATED')).not.toThrow(); }); it('sql/008 constrains tier at the DATABASE level, not only in TypeScript', () => { expect(MIGRATION).toContain("CHECK (tier IN ('RAW','NORMALIZED','CURATED','SERVING'))"); }); it("BI-P003's eight required declarations are all NOT NULL in sql/008", () => { const block = MIGRATION.split('CREATE TABLE IF NOT EXISTS opt_analytics_dataset ')[1] ?? ''; const body = block.slice(0, block.indexOf(');')); for (const col of [ 'tier', 'schema_contract_id', 'owner_id', 'source_ref', 'freshness_slo_seconds', 'retention_days', 'classification', 'quality_rule_ids', ]) { expect(new RegExp(`\\b${col}\\b[^,]*NOT NULL`).test(body), `${col} is nullable`).toBe(true); } }); });