@cosmicdrift/kumiko-framework 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +57 -0
  2. package/package.json +3 -3
  3. package/src/__tests__/anonymous-access.integration.ts +7 -7
  4. package/src/__tests__/error-contract.integration.ts +2 -2
  5. package/src/__tests__/field-access.integration.ts +2 -2
  6. package/src/__tests__/full-stack.integration.ts +2 -2
  7. package/src/__tests__/ownership.integration.ts +2 -2
  8. package/src/__tests__/raw-table.integration.ts +128 -0
  9. package/src/__tests__/reference-data.integration.ts +2 -2
  10. package/src/__tests__/transition-guard.integration.ts +4 -4
  11. package/src/api/__tests__/batch.integration.ts +3 -3
  12. package/src/api/__tests__/dispatcher-live.integration.ts +2 -2
  13. package/src/api/__tests__/nested-write.integration.ts +3 -3
  14. package/src/db/__tests__/drizzle-helpers.integration.ts +2 -2
  15. package/src/db/__tests__/event-store-executor-list.integration.ts +2 -2
  16. package/src/db/__tests__/event-store-executor.integration.ts +9 -3
  17. package/src/db/__tests__/implicit-projection-equivalence.integration.ts +3 -3
  18. package/src/db/__tests__/multi-row-insert.integration.ts +3 -3
  19. package/src/db/__tests__/schema-migration.integration.ts +9 -9
  20. package/src/db/__tests__/tenant-db.integration.ts +4 -4
  21. package/src/db/__tests__/unique-violation-mapping.integration.ts +2 -2
  22. package/src/db/schema-inspection.ts +1 -1
  23. package/src/engine/__tests__/raw-table.test.ts +149 -0
  24. package/src/engine/define-feature.ts +38 -0
  25. package/src/engine/index.ts +6 -0
  26. package/src/engine/registry.ts +46 -0
  27. package/src/engine/tier-resolver-extension.ts +78 -0
  28. package/src/engine/types/feature.ts +55 -0
  29. package/src/engine/types/handlers.ts +13 -5
  30. package/src/engine/types/index.ts +3 -0
  31. package/src/event-store/__tests__/upcaster.integration.ts +11 -5
  32. package/src/event-store/archive.ts +2 -2
  33. package/src/event-store/events-schema.ts +2 -2
  34. package/src/event-store/snapshot.ts +2 -2
  35. package/src/event-store/upcaster-dead-letter.ts +2 -2
  36. package/src/files/__tests__/file-field-column.integration.ts +4 -4
  37. package/src/files/__tests__/file-field-pipeline.integration.ts +2 -2
  38. package/src/files/__tests__/files.integration.ts +8 -8
  39. package/src/observability/__tests__/observability.integration.ts +2 -2
  40. package/src/pipeline/__tests__/archive-stream.integration.ts +2 -2
  41. package/src/pipeline/__tests__/cascade-handler.integration.ts +9 -9
  42. package/src/pipeline/__tests__/causation-chain.integration.ts +2 -2
  43. package/src/pipeline/__tests__/ctx-bridge.integration.ts +3 -3
  44. package/src/pipeline/__tests__/dispatcher.test.ts +73 -0
  45. package/src/pipeline/__tests__/domain-events-projections.integration.ts +2 -2
  46. package/src/pipeline/__tests__/event-dedup.integration.ts +2 -2
  47. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +2 -2
  48. package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +3 -3
  49. package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +2 -2
  50. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +2 -2
  51. package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +2 -2
  52. package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +4 -4
  53. package/src/pipeline/__tests__/event-dispatcher.integration.ts +4 -4
  54. package/src/pipeline/__tests__/event-retention.integration.ts +2 -2
  55. package/src/pipeline/__tests__/fetch-for-writing.integration.ts +2 -2
  56. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +100 -0
  57. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +2 -2
  58. package/src/pipeline/__tests__/msp-error-mode.integration.ts +2 -2
  59. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +2 -2
  60. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  61. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +3 -3
  62. package/src/pipeline/__tests__/perf-rebuild.integration.ts +2 -2
  63. package/src/pipeline/__tests__/projection-rebuild.integration.ts +9 -3
  64. package/src/pipeline/__tests__/query-projection.integration.ts +2 -2
  65. package/src/pipeline/dispatcher.ts +35 -15
  66. package/src/pipeline/event-consumer-state.ts +2 -2
  67. package/src/pipeline/event-dispatcher.ts +10 -1
  68. package/src/pipeline/lifecycle-pipeline.ts +22 -4
  69. package/src/pipeline/projection-state.ts +3 -3
  70. package/src/stack/index.ts +4 -3
  71. package/src/stack/push-entity-projection-tables.ts +51 -0
  72. package/src/stack/table-helpers.ts +20 -13
  73. package/src/stack/test-stack.ts +14 -5
  74. package/src/testing/__tests__/ensure-entity-table.integration.ts +16 -11
@@ -7,49 +7,56 @@ import { buildDrizzleTable, toTableName } from "../db/table-builder";
7
7
  import type { TestStack } from "./test-stack";
8
8
 
9
9
  /**
10
- * Syncs a Drizzle table to the database via drizzle-kit migration.
11
- * No manual SQL Drizzle generates CREATE/ALTER TABLE statements.
10
+ * Bypass: creates a Drizzle table directly, without registering it as
11
+ * a projection of the event-sourcing engine. Apps should declare data
12
+ * via `r.entity(...)` and get tables, migrations, snapshots and audit
13
+ * for free — this helper is reserved for framework-internal meta-tables
14
+ * (event-store, snapshots, projection-state) and test setup.
15
+ *
12
16
  * Strict: raises a postgres `relation already exists` (42P07) error if
13
- * the table is already there. Use `ensureEntityTable` for idempotent
14
- * boot paths.
17
+ * the table is already there. Use `unsafeEnsureEntityTable` for the
18
+ * idempotent boot-path variant.
15
19
  */
16
- export async function createEntityTable(
20
+ export async function unsafeCreateEntityTable(
17
21
  db: ReturnType<typeof drizzle>,
18
22
  entity: import("../engine/types").EntityDefinition,
19
23
  entityName?: string,
20
24
  ): Promise<void> {
21
25
  const table = buildDrizzleTable(entityName ?? "entity", entity);
22
- await pushTables(db, { [entityName ?? "entity"]: table });
26
+ await unsafePushTables(db, { [entityName ?? "entity"]: table });
23
27
  }
24
28
 
25
29
  /**
26
- * Idempotent variant of `createEntityTable`: checks whether the entity's
30
+ * Bypass (idempotent): same caveat as `unsafeCreateEntityTable`
31
+ * apps declare data via `r.entity(...)`. Checks whether the entity's
27
32
  * table already exists and skips creation if so. Schema-drift is *not*
28
- * detected if the table is there but has the wrong columns, that's
33
+ * detected: if the table is there but has the wrong columns, that's
29
34
  * the caller's problem (the dev-server contract is "drop the DB by
30
35
  * hand when you change the schema"). Tests should use
31
- * `createEntityTable` instead, since they rely on fresh DBs.
36
+ * `unsafeCreateEntityTable` instead, since they rely on fresh DBs.
32
37
  */
33
- export async function ensureEntityTable(
38
+ export async function unsafeEnsureEntityTable(
34
39
  db: ReturnType<typeof drizzle>,
35
40
  entity: import("../engine/types").EntityDefinition,
36
41
  entityName?: string,
37
42
  ): Promise<boolean> {
38
43
  const resolvedName = entity.table ?? toTableName(entityName ?? "entity");
39
44
  if (await tableExists(db, `public.${resolvedName}`)) return false;
40
- await createEntityTable(db, entity, entityName);
45
+ await unsafeCreateEntityTable(db, entity, entityName);
41
46
  return true;
42
47
  }
43
48
 
44
49
  /**
45
- * Pushes Drizzle table definitions to the database.
50
+ * Bypass: pushes Drizzle table definitions to the database directly.
46
51
  * Uses drizzle-kit's generateDrizzleJson + generateMigration to produce SQL,
47
52
  * then executes it. Same SQL that `drizzle-kit push` would generate.
53
+ * Reserved for framework-internal meta-tables (event-store, projections,
54
+ * consumer-state) and test setup — apps declare data via `r.entity(...)`.
48
55
  *
49
56
  * @param prevTables - Previous table definitions (for ALTER TABLE scenarios).
50
57
  * If omitted, assumes empty DB (CREATE TABLE).
51
58
  */
52
- export async function pushTables(
59
+ export async function unsafePushTables(
53
60
  db: ReturnType<typeof drizzle>,
54
61
  tables: Record<string, unknown>,
55
62
  prevTables?: Record<string, unknown>,
@@ -17,7 +17,7 @@ import { createTestDb } from "./db";
17
17
  import { createEventCollector, type EventCollector } from "./event-collector";
18
18
  import { createTestRedis, type TestRedis } from "./redis";
19
19
  import { createRequestHelper, type RequestHelper } from "./request-helper";
20
- import { pushTables } from "./table-helpers";
20
+ import { unsafePushTables } from "./table-helpers";
21
21
 
22
22
  export type TestStack = {
23
23
  app: Hono;
@@ -89,7 +89,7 @@ export type TestStackOptions = {
89
89
  * GlobalFeatureToggleRuntime.effectiveFeatures for real DB-backed
90
90
  * toggles, or a plain `() => new Set<string>(registry.features.keys())`
91
91
  * to force a specific snapshot in a unit-style setup. */
92
- effectiveFeatures?: () => ReadonlySet<string>;
92
+ effectiveFeatures?: (tenantId: TenantId) => ReadonlySet<string>;
93
93
  /** Pin the underlying Postgres DB name instead of the default
94
94
  * `kumiko_test_<8chars>`. Forwarded to createTestDb. Primary use
95
95
  * case: dev servers that want persistent storage across restarts —
@@ -161,7 +161,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
161
161
  // stays off tenant test DBs that never touch files.
162
162
  if (options.files) {
163
163
  const { fileRefsTable } = await import("../files");
164
- await pushTables(testDb.db, { fileRefsTable });
164
+ await unsafePushTables(testDb.db, { fileRefsTable });
165
165
  }
166
166
 
167
167
  // Projection tables: the executor writes into them in the same TX as the
@@ -189,9 +189,18 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
189
189
  seenTables.add(msp.table);
190
190
  projectionTables[`msp_${mspName}`] = msp.table;
191
191
  }
192
+ // Raw tables declared via r.rawTable(). Same auto-push rule — the
193
+ // table needs to exist before the first reader query runs. The
194
+ // bypass is in the registration site (r.rawTable's `unsafe` cousins
195
+ // would target the same DDL), not in setting up the test DB.
196
+ for (const [rawName, raw] of Object.entries(feature.rawTables)) {
197
+ if (seenTables.has(raw.table)) continue;
198
+ seenTables.add(raw.table);
199
+ projectionTables[`raw_${rawName}`] = raw.table;
200
+ }
192
201
  }
193
202
  if (Object.keys(projectionTables).length > 0) {
194
- // pushTables emits raw CREATE TABLE — fine for ephemeral test DBs but
203
+ // unsafePushTables emits raw CREATE TABLE — fine for ephemeral test DBs but
195
204
  // collides on re-boot against a persistent DB whose projection tables
196
205
  // were created during a previous run. Filter out the ones that already
197
206
  // exist; drizzle-kit's diff machinery would otherwise emit CREATE for
@@ -205,7 +214,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
205
214
  missing[key] = tbl;
206
215
  }
207
216
  if (Object.keys(missing).length > 0) {
208
- await pushTables(testDb.db, missing);
217
+ await unsafePushTables(testDb.db, missing);
209
218
  }
210
219
  }
211
220
 
@@ -1,11 +1,16 @@
1
1
  import { sql } from "drizzle-orm";
2
2
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
3
3
  import type { EntityDefinition } from "../../engine/types";
4
- import { createEntityTable, createTestDb, ensureEntityTable, type TestDb } from "../../stack";
5
-
6
- // ensureEntityTable ist die idempotente Variante von createEntityTable —
4
+ import {
5
+ createTestDb,
6
+ type TestDb,
7
+ unsafeCreateEntityTable,
8
+ unsafeEnsureEntityTable,
9
+ } from "../../stack";
10
+
11
+ // unsafeEnsureEntityTable ist die idempotente Variante von unsafeCreateEntityTable —
7
12
  // existiert wegen des dev-server-Boot-Pfads (persistente DB, Table von
8
- // letztem Run). createEntityTable bleibt strict, damit Tests ein
13
+ // letztem Run). unsafeCreateEntityTable bleibt strict, damit Tests ein
9
14
  // falsches Schema nicht stillschweigend akzeptieren.
10
15
 
11
16
  const tenantEntity: EntityDefinition = {
@@ -25,9 +30,9 @@ afterAll(async () => {
25
30
  await db.cleanup();
26
31
  });
27
32
 
28
- describe("ensureEntityTable", () => {
33
+ describe("unsafeEnsureEntityTable", () => {
29
34
  test("legt die Tabelle beim ersten Aufruf an (returnt true)", async () => {
30
- const created = await ensureEntityTable(db.db, tenantEntity, "probe");
35
+ const created = await unsafeEnsureEntityTable(db.db, tenantEntity, "probe");
31
36
  expect(created).toBe(true);
32
37
  const rows = await db.db.execute<{ exists: boolean }>(
33
38
  sql`SELECT to_regclass('public.ensure_entity_table_probe') IS NOT NULL AS exists`,
@@ -36,17 +41,17 @@ describe("ensureEntityTable", () => {
36
41
  });
37
42
 
38
43
  test("ist beim zweiten Aufruf ein No-Op (returnt false, kein Fehler)", async () => {
39
- const created = await ensureEntityTable(db.db, tenantEntity, "probe");
44
+ const created = await unsafeEnsureEntityTable(db.db, tenantEntity, "probe");
40
45
  expect(created).toBe(false);
41
46
  });
42
47
 
43
- test("createEntityTable bleibt strict — wirft bei existierender Tabelle", async () => {
44
- // Gleiche Entity zweimal via createEntityTable → postgres 42P07
48
+ test("unsafeCreateEntityTable bleibt strict — wirft bei existierender Tabelle", async () => {
49
+ // Gleiche Entity zweimal via unsafeCreateEntityTable → postgres 42P07
45
50
  // (relation already exists). Drizzle wrappt den PG-Error in
46
51
  // DrizzleQueryError; der echte Code steckt in .cause. Sicherstellt,
47
- // dass ensureEntityTable nicht versehentlich das strict-Verhalten
52
+ // dass unsafeEnsureEntityTable nicht versehentlich das strict-Verhalten
48
53
  // verändert.
49
- await expect(createEntityTable(db.db, tenantEntity, "probe")).rejects.toSatisfy((err) => {
54
+ await expect(unsafeCreateEntityTable(db.db, tenantEntity, "probe")).rejects.toSatisfy((err) => {
50
55
  const cause = (err as { cause?: { code?: string } }).cause;
51
56
  return cause?.code === "42P07";
52
57
  });