@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
package/LICENSE ADDED
@@ -0,0 +1,57 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: Marc Frost
6
+
7
+ Licensed Work: @cosmicdrift/kumiko-framework
8
+ The Licensed Work is © 2026 Marc Frost.
9
+
10
+ Additional Use Grant:
11
+ You may use the Licensed Work in production for any purpose, including
12
+ commercially, EXCEPT for the Restricted Use.
13
+
14
+ "Restricted Use" is defined as using the Licensed Work to provide a platform
15
+ or service to third parties that allows them to host, deploy, or run their
16
+ own applications built with the Licensed Work. This includes, but is not
17
+ limited to: managed hosting services, software-as-a-service (SaaS) platforms,
18
+ platform-as-a-service (PaaS), developer platforms, or any multi-tenant
19
+ managed offering of the Licensed Work.
20
+
21
+ This restriction does not apply to the Licensor, any entity controlled by,
22
+ controlling, or under common control with the Licensor ("Affiliates"), or
23
+ contractors acting on their behalf. The Licensor remains free to use the
24
+ Licensed Work for any purpose, including for the operation of kumiko.so.
25
+
26
+ Change Date: 2030-05-05
27
+ Change License: Apache License, Version 2.0
28
+
29
+
30
+ Terms
31
+
32
+ The Licensor hereby grants you the right to copy, modify, create derivative works,
33
+ redistribute, and make non-production use of the Licensed Work. The Licensor may
34
+ make an Additional Use Grant, above, permitting limited production use.
35
+
36
+ Effective on the Change Date, or the fourth anniversary of the first publicly
37
+ available distribution of the Licensed Work under this License, whichever comes
38
+ first, this License will convert to the Change License.
39
+
40
+ This Business Source License governs use of the Licensed Work in all cases, except
41
+ as to any use that is explicitly granted in the Additional Use Grant above or
42
+ under the Change License after the Change Date.
43
+
44
+ If your use of the Licensed Work does not comply with the requirements of this
45
+ License, you must cease use of the Licensed Work immediately.
46
+
47
+ All copies of the Licensed Work, and all derivative works thereof, must include
48
+ this License.
49
+
50
+ This License does not grant you any right, title, or interest in any trademark,
51
+ logo, or branding of the Licensor, except as required to comply with this License.
52
+
53
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
54
+ “AS IS” BASIS. LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
55
+ WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
56
+ TITLE, AND NON-INFRINGEMENT. IN NO EVENT WILL LICENSOR BE LIABLE FOR ANY DAMAGES
57
+ ARISING OUT OF OR RELATED TO THIS LICENSE OR THE USE OF THE LICENSED WORK.
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/cosmicdriftgamestudio/kumiko-framework.git",
9
+ "url": "git+https://github.com/CosmicDriftGameStudio/kumiko-framework.git",
10
10
  "directory": "packages/framework"
11
11
  },
12
12
  "bugs": {
13
- "url": "https://github.com/cosmicdriftgamestudio/kumiko-framework/issues"
13
+ "url": "https://github.com/CosmicDriftGameStudio/kumiko-framework/issues"
14
14
  },
15
15
  "homepage": "https://kumiko.so",
16
16
  "keywords": [
@@ -15,7 +15,7 @@ import {
15
15
  defineFeature,
16
16
  type TenantId,
17
17
  } from "../engine";
18
- import { createEntityTable, setupTestStack, type TestStack, TestUsers } from "../stack";
18
+ import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../stack";
19
19
 
20
20
  const TENANT_ID = "00000000-0000-4000-8000-000000000001" as TenantId;
21
21
  const OTHER_TENANT_ID = "00000000-0000-4000-8000-000000000002" as TenantId;
@@ -104,8 +104,8 @@ describe("anonymous access — single-tenant default", () => {
104
104
  features: [shopFeature],
105
105
  anonymousAccess: { defaultTenantId: TENANT_ID },
106
106
  });
107
- await createEntityTable(stack.db, productEntity);
108
- await createEntityTable(stack.db, orderEntity);
107
+ await unsafeCreateEntityTable(stack.db, productEntity);
108
+ await unsafeCreateEntityTable(stack.db, orderEntity);
109
109
  });
110
110
 
111
111
  afterAll(() => stack.cleanup());
@@ -216,8 +216,8 @@ describe("anonymous access — header-supplied tenant", () => {
216
216
  tenantExists: async (id: TenantId) => id === TENANT_ID || id === OTHER_TENANT_ID,
217
217
  },
218
218
  });
219
- await createEntityTable(stack.db, productEntity);
220
- await createEntityTable(stack.db, orderEntity);
219
+ await unsafeCreateEntityTable(stack.db, productEntity);
220
+ await unsafeCreateEntityTable(stack.db, orderEntity);
221
221
  });
222
222
 
223
223
  afterAll(() => stack.cleanup());
@@ -309,8 +309,8 @@ describe("anonymous access — disabled by default", () => {
309
309
 
310
310
  beforeAll(async () => {
311
311
  stack = await setupTestStack({ features: [shopFeature] });
312
- await createEntityTable(stack.db, productEntity);
313
- await createEntityTable(stack.db, orderEntity);
312
+ await unsafeCreateEntityTable(stack.db, productEntity);
313
+ await unsafeCreateEntityTable(stack.db, orderEntity);
314
314
  });
315
315
 
316
316
  afterAll(() => stack.cleanup());
@@ -22,11 +22,11 @@ import {
22
22
  writeFailure,
23
23
  } from "../errors";
24
24
  import {
25
- createEntityTable,
26
25
  createTestUser,
27
26
  setupTestStack,
28
27
  type TestStack,
29
28
  TestUsers,
29
+ unsafeCreateEntityTable,
30
30
  } from "../stack";
31
31
 
32
32
  // --- Entity + handlers that deliberately raise each Kumiko error class ---
@@ -187,7 +187,7 @@ const guest = createTestUser({ id: 2, roles: ["Guest"] });
187
187
 
188
188
  beforeAll(async () => {
189
189
  stack = await setupTestStack({ features: [errorFeature] });
190
- await createEntityTable(stack.db, itemEntity);
190
+ await unsafeCreateEntityTable(stack.db, itemEntity);
191
191
  });
192
192
  afterAll(async () => stack.cleanup());
193
193
  beforeEach(async () => {
@@ -12,13 +12,13 @@ import {
12
12
  type SessionUser,
13
13
  } from "../engine";
14
14
  import {
15
- createEntityTable,
16
15
  createTestDb,
17
16
  createTestRedis,
18
17
  createTestUser,
19
18
  type TestDb,
20
19
  type TestRedis,
21
20
  TestUsers,
21
+ unsafeCreateEntityTable,
22
22
  } from "../stack";
23
23
 
24
24
  // --- Entity with field-level access ---
@@ -52,7 +52,7 @@ beforeAll(async () => {
52
52
  testDb = await createTestDb();
53
53
  testRedis = await createTestRedis();
54
54
 
55
- await createEntityTable(testDb.db, employeeEntity);
55
+ await unsafeCreateEntityTable(testDb.db, employeeEntity);
56
56
 
57
57
  const feature = defineFeature("employees", (r) => {
58
58
  r.entity("employee", employeeEntity);
@@ -5,11 +5,11 @@ import { defineFeature, type EntityId, type HandlerContext, type SaveContext } f
5
5
  import { UnprocessableError, writeFailure } from "../errors";
6
6
  import { eventsTable } from "../event-store";
7
7
  import {
8
- createEntityTable,
9
8
  createTestUser,
10
9
  setupTestStack,
11
10
  type TestStack,
12
11
  TestUsers,
12
+ unsafeCreateEntityTable,
13
13
  } from "../stack";
14
14
  import { expectErrorIncludes, sharedUserEntity, sharedUserTable } from "../testing";
15
15
 
@@ -203,7 +203,7 @@ const otherTenantAdmin = createTestUser({
203
203
 
204
204
  beforeAll(async () => {
205
205
  stack = await setupTestStack({ features: [userFeature] });
206
- await createEntityTable(stack.db, userEntity, "user");
206
+ await unsafeCreateEntityTable(stack.db, userEntity, "user");
207
207
  });
208
208
 
209
209
  afterAll(async () => {
@@ -15,12 +15,12 @@ import {
15
15
  } from "../engine";
16
16
  import type { SessionUser, TenantId } from "../engine/types";
17
17
  import {
18
- createEntityTable,
19
18
  createTestUser,
20
19
  setupTestStack,
21
20
  type TestStack,
22
21
  TestUsers,
23
22
  testTenantId,
23
+ unsafeCreateEntityTable,
24
24
  } from "../stack";
25
25
  import { expectErrorIncludes } from "../testing";
26
26
 
@@ -123,7 +123,7 @@ let stack: TestStack;
123
123
 
124
124
  beforeAll(async () => {
125
125
  stack = await setupTestStack({ features: [teamsFeature, contractsFeature] });
126
- await createEntityTable(stack.db, contractEntity, "contract");
126
+ await unsafeCreateEntityTable(stack.db, contractEntity, "contract");
127
127
  });
128
128
 
129
129
  afterAll(async () => {
@@ -0,0 +1,128 @@
1
+ // Integration test for r.rawTable() — proves the full boot path:
2
+ // defineFeature declares a raw table → setupTestStack auto-pushes it →
3
+ // INSERT/SELECT against the real DB roundtrip. Plan reference:
4
+ // kumiko-platform/docs/plans/architecture/table-ddl-guard.md (Stufe 3).
5
+
6
+ import { eq, sql } from "drizzle-orm";
7
+ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
8
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
9
+ import { defineFeature } from "../engine";
10
+ import { setupTestStack, type TestStack, unsafePushTables } from "../stack";
11
+
12
+ // External-system payload cache — the textbook r.rawTable() use case:
13
+ // write-only by an integration handler, read-only by a query, never
14
+ // event-sourced (the data isn't a domain fact, it's a side-effect
15
+ // snapshot).
16
+ const stripeWebhookCache = pgTable("rt_int_stripe_webhook_cache", {
17
+ eventId: text("event_id").primaryKey(),
18
+ payload: text("payload").notNull(),
19
+ receivedAt: timestamp("received_at", { withTimezone: true }).notNull().defaultNow(),
20
+ });
21
+
22
+ // A second physical table reachable through two distinct r.rawTable
23
+ // registrations — pins the seenTables-by-reference dedupe at push time.
24
+ // Two logical names, one CREATE.
25
+ const sharedSyncCache = pgTable("rt_int_shared_sync_cache", {
26
+ syncId: text("sync_id").primaryKey(),
27
+ payload: text("payload").notNull(),
28
+ });
29
+
30
+ const webhookCacheFeature = defineFeature("webhook-cache", (r) => {
31
+ r.rawTable("stripe", stripeWebhookCache, {
32
+ reason: "external Stripe webhook payload cache — write-only by webhook handler",
33
+ });
34
+ r.rawTable("primary-sync", sharedSyncCache, {
35
+ reason: "shared sync-state cache, primary writer",
36
+ });
37
+ r.rawTable("secondary-sync", sharedSyncCache, {
38
+ reason: "same physical table, different logical role for read consumers",
39
+ });
40
+ });
41
+
42
+ let stack: TestStack;
43
+
44
+ beforeAll(async () => {
45
+ stack = await setupTestStack({ features: [webhookCacheFeature] });
46
+ });
47
+
48
+ afterAll(async () => {
49
+ await stack.cleanup();
50
+ });
51
+
52
+ describe("r.rawTable — DB roundtrip via setupTestStack", () => {
53
+ test("table is auto-pushed and accepts INSERT + SELECT", async () => {
54
+ const eventId = "evt_test_123";
55
+ const payload = JSON.stringify({ type: "invoice.paid", amount: 4200 });
56
+
57
+ await stack.db.insert(stripeWebhookCache).values({ eventId, payload });
58
+
59
+ const rows = await stack.db
60
+ .select()
61
+ .from(stripeWebhookCache)
62
+ .where(eq(stripeWebhookCache.eventId, eventId));
63
+
64
+ expect(rows).toHaveLength(1);
65
+ expect(rows[0]?.payload).toBe(payload);
66
+ expect(rows[0]?.receivedAt).toBeInstanceOf(Date);
67
+ });
68
+
69
+ test("registry exposes the raw table with its reason and featureName", () => {
70
+ const all = stack.registry.getAllRawTables();
71
+ const entry = all.get("stripe");
72
+ expect(entry).toBeDefined();
73
+ expect(entry?.featureName).toBe("webhook-cache");
74
+ expect(entry?.reason).toContain("Stripe webhook payload cache");
75
+ expect(entry?.table).toBe(stripeWebhookCache);
76
+ });
77
+
78
+ test("INSERT bypasses the event store — no kumiko_events row produced", async () => {
79
+ // Proves that the rawTable lives outside the event-sourcing graph:
80
+ // a write to it shouldn't append anything to kumiko_events. If a
81
+ // future regression accidentally routed raw-table writes through
82
+ // the executor, a row would show up here.
83
+ const before = await stack.db.execute<{ count: string }>(
84
+ sql`SELECT COUNT(*)::text AS count FROM kumiko_events`,
85
+ );
86
+ const beforeCount = Number(before[0]?.count ?? 0);
87
+
88
+ await stack.db.insert(stripeWebhookCache).values({
89
+ eventId: "evt_no_event_emitted",
90
+ payload: "{}",
91
+ });
92
+
93
+ const after = await stack.db.execute<{ count: string }>(
94
+ sql`SELECT COUNT(*)::text AS count FROM kumiko_events`,
95
+ );
96
+ const afterCount = Number(after[0]?.count ?? 0);
97
+
98
+ expect(afterCount).toBe(beforeCount);
99
+ });
100
+
101
+ test("two registrations sharing one PgTable result in one CREATE (dedupe by reference)", async () => {
102
+ // primary-sync + secondary-sync both target sharedSyncCache. If the
103
+ // setupTestStack dedupe (seenTables-by-table-reference) had silently
104
+ // broken, beforeAll's setupTestStack would have raised a 42P07 on
105
+ // the second push and never reached this test.
106
+ await stack.db.insert(sharedSyncCache).values({ syncId: "sync_1", payload: "{}" });
107
+ const rows = await stack.db
108
+ .select()
109
+ .from(sharedSyncCache)
110
+ .where(eq(sharedSyncCache.syncId, "sync_1"));
111
+ expect(rows).toHaveLength(1);
112
+ // Both registrations are visible in the registry — same physical
113
+ // target, different logical handles.
114
+ expect(stack.registry.getAllRawTables().get("primary-sync")?.table).toBe(sharedSyncCache);
115
+ expect(stack.registry.getAllRawTables().get("secondary-sync")?.table).toBe(sharedSyncCache);
116
+ });
117
+
118
+ test("a second push on the same rawTable would crash — proves tableExists-filter is load-bearing", async () => {
119
+ // The setupTestStack push is idempotent because it filters by
120
+ // tableExists before calling unsafePushTables (which is itself
121
+ // strict — that's the contract of the unsafe-prefix). Pin the
122
+ // failure mode of the unfiltered call: a direct second push on
123
+ // the same Drizzle schema raises the underlying PG error. This
124
+ // is the negative form of the idempotency contract — without the
125
+ // filter, a re-boot against a persistent dev DB would crash here.
126
+ await expect(unsafePushTables(stack.db, { idem: stripeWebhookCache })).rejects.toThrow();
127
+ });
128
+ });
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest";
2
2
  import { integer, table as pgTable, serial, text } from "../db/dialect";
3
3
  import { seedReferenceData } from "../db/reference-data";
4
4
  import type { ReferenceDataDef } from "../engine/types";
5
- import { createTestDb, pushTables, type TestDb } from "../stack";
5
+ import { createTestDb, type TestDb, unsafePushTables } from "../stack";
6
6
 
7
7
  // --- Tables ---
8
8
 
@@ -25,7 +25,7 @@ let testDb: TestDb;
25
25
 
26
26
  beforeAll(async () => {
27
27
  testDb = await createTestDb();
28
- await pushTables(testDb.db, { countryTable, statusTable });
28
+ await unsafePushTables(testDb.db, { countryTable, statusTable });
29
29
  });
30
30
 
31
31
  afterAll(async () => {
@@ -9,7 +9,7 @@ import {
9
9
  createTextField,
10
10
  defineFeature,
11
11
  } from "../engine";
12
- import { createEntityTable, setupTestStack, type TestStack, TestUsers } from "../stack";
12
+ import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../stack";
13
13
  import { expectErrorIncludes } from "../testing";
14
14
 
15
15
  // Two entities, both with a field named `status`, but different transitions.
@@ -178,9 +178,9 @@ const admin = TestUsers.admin;
178
178
 
179
179
  beforeAll(async () => {
180
180
  stack = await setupTestStack({ features: [feature] });
181
- await createEntityTable(stack.db, invoiceEntity);
182
- await createEntityTable(stack.db, orderEntity);
183
- await createEntityTable(stack.db, ticketEntity);
181
+ await unsafeCreateEntityTable(stack.db, invoiceEntity);
182
+ await unsafeCreateEntityTable(stack.db, orderEntity);
183
+ await unsafeCreateEntityTable(stack.db, ticketEntity);
184
184
  });
185
185
 
186
186
  afterAll(async () => {
@@ -12,7 +12,7 @@ import {
12
12
  type SaveContext,
13
13
  } from "../../engine";
14
14
  import { UnprocessableError, writeFailure } from "../../errors";
15
- import { createEntityTable, setupTestStack, type TestStack, TestUsers } from "../../stack";
15
+ import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../../stack";
16
16
 
17
17
  // Entity: a simple "item" with name + counter
18
18
  const itemEntity = createEntity({
@@ -151,8 +151,8 @@ const admin = TestUsers.admin;
151
151
 
152
152
  beforeAll(async () => {
153
153
  stack = await setupTestStack({ features: [itemFeature] });
154
- await createEntityTable(stack.db, itemEntity);
155
- await createEntityTable(stack.db, auditEntity);
154
+ await unsafeCreateEntityTable(stack.db, itemEntity);
155
+ await unsafeCreateEntityTable(stack.db, auditEntity);
156
156
  });
157
157
 
158
158
  afterAll(async () => {
@@ -5,7 +5,7 @@ import { generateToken } from "../../api/tokens";
5
5
  import { createEventStoreExecutor } from "../../db/event-store-executor";
6
6
  import { buildDrizzleTable } from "../../db/table-builder";
7
7
  import { createEntity, createTextField, defineFeature } from "../../engine";
8
- import { createEntityTable, setupTestStack, type TestStack, TestUsers } from "../../stack";
8
+ import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../../stack";
9
9
  import { generateId } from "../../utils";
10
10
 
11
11
  // End-to-end: UI code would call `dispatcher.write("feat:write:item:create", ...)`.
@@ -97,7 +97,7 @@ async function buildFetch(): Promise<{
97
97
 
98
98
  beforeAll(async () => {
99
99
  stack = await setupTestStack({ features: [itemFeature] });
100
- await createEntityTable(stack.db, itemEntity);
100
+ await unsafeCreateEntityTable(stack.db, itemEntity);
101
101
  });
102
102
 
103
103
  afterAll(async () => {
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  import { createEventStoreExecutor } from "../../db/event-store-executor";
5
5
  import { buildDrizzleTable } from "../../db/table-builder";
6
6
  import { createEntity, createTextField, defineFeature } from "../../engine";
7
- import { createEntityTable, setupTestStack, type TestStack, TestUsers } from "../../stack";
7
+ import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../../stack";
8
8
 
9
9
  // Two entities in a 1:N relation. The relation is declared with
10
10
  // `nestedWrite: true`, which opts the framework into expanding
@@ -69,8 +69,8 @@ const admin = TestUsers.admin;
69
69
 
70
70
  beforeAll(async () => {
71
71
  stack = await setupTestStack({ features: [nestedFeature] });
72
- await createEntityTable(stack.db, projectEntity);
73
- await createEntityTable(stack.db, taskEntity);
72
+ await unsafeCreateEntityTable(stack.db, projectEntity);
73
+ await unsafeCreateEntityTable(stack.db, taskEntity);
74
74
  });
75
75
 
76
76
  afterAll(async () => {
@@ -2,7 +2,7 @@ import { drizzle } from "drizzle-orm/postgres-js";
2
2
  import postgres from "postgres";
3
3
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
4
4
  import { createBooleanField, createEntity, createTextField } from "../../engine";
5
- import { createEntityTable, testTenantId } from "../../stack";
5
+ import { testTenantId, unsafeCreateEntityTable } from "../../stack";
6
6
  import { applyCursorQuery, encodeCursor } from "../cursor";
7
7
  import { buildDrizzleTable } from "../table-builder";
8
8
 
@@ -49,7 +49,7 @@ beforeAll(async () => {
49
49
  client = postgres(testUrl);
50
50
  db = drizzle(client);
51
51
 
52
- await createEntityTable(db, entity);
52
+ await unsafeCreateEntityTable(db, entity);
53
53
 
54
54
  const rows = [
55
55
  {
@@ -8,7 +8,7 @@ import { sql } from "drizzle-orm";
8
8
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
9
9
  import { createEntity, createNumberField, createTextField } from "../../engine";
10
10
  import { createEventsTable } from "../../event-store";
11
- import { createEntityTable, createTestDb, type TestDb, TestUsers } from "../../stack";
11
+ import { createTestDb, type TestDb, TestUsers, unsafeCreateEntityTable } from "../../stack";
12
12
  import { createEventStoreExecutor } from "../event-store-executor";
13
13
  import { buildDrizzleTable } from "../table-builder";
14
14
  import { createTenantDb, type TenantDb } from "../tenant-db";
@@ -28,7 +28,7 @@ const admin = TestUsers.admin;
28
28
 
29
29
  beforeAll(async () => {
30
30
  testDb = await createTestDb();
31
- await createEntityTable(testDb.db, entity, "pagerItem");
31
+ await unsafeCreateEntityTable(testDb.db, entity, "pagerItem");
32
32
  await createEventsTable(testDb.db);
33
33
  tdb = createTenantDb(testDb.db, admin.tenantId);
34
34
  });
@@ -2,7 +2,13 @@ import { sql } from "drizzle-orm";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
3
3
  import { createBooleanField, createEntity, createTextField } from "../../engine";
4
4
  import { createEventsTable } from "../../event-store";
5
- import { createEntityTable, createTestDb, type TestDb, TestUsers, testTenantId } from "../../stack";
5
+ import {
6
+ createTestDb,
7
+ type TestDb,
8
+ TestUsers,
9
+ testTenantId,
10
+ unsafeCreateEntityTable,
11
+ } from "../../stack";
6
12
  import { createEventStoreExecutor } from "../event-store-executor";
7
13
  import { buildDrizzleTable } from "../table-builder";
8
14
  import { createTenantDb, type TenantDb } from "../tenant-db";
@@ -24,7 +30,7 @@ const adminUser = TestUsers.admin;
24
30
 
25
31
  beforeAll(async () => {
26
32
  testDb = await createTestDb();
27
- await createEntityTable(testDb.db, entity, "esExecUser");
33
+ await unsafeCreateEntityTable(testDb.db, entity, "esExecUser");
28
34
  await createEventsTable(testDb.db);
29
35
  tdb = createTenantDb(testDb.db, adminUser.tenantId);
30
36
  });
@@ -119,7 +125,7 @@ describe("event-store-executor — sensitive fields", () => {
119
125
  });
120
126
 
121
127
  beforeAll(async () => {
122
- await createEntityTable(testDb.db, sensitiveEntity, "esExecSensitive");
128
+ await unsafeCreateEntityTable(testDb.db, sensitiveEntity, "esExecSensitive");
123
129
  });
124
130
 
125
131
  beforeEach(async () => {
@@ -22,7 +22,7 @@ import { createRegistry } from "../../engine/registry";
22
22
  import { createEventsTable } from "../../event-store";
23
23
  import { rebuildProjection } from "../../pipeline";
24
24
  import { createProjectionStateTable } from "../../pipeline/projection-state";
25
- import { createEntityTable, createTestDb, type TestDb, TestUsers } from "../../stack";
25
+ import { createTestDb, type TestDb, TestUsers, unsafeCreateEntityTable } from "../../stack";
26
26
  import { createEventStoreExecutor } from "../event-store-executor";
27
27
  import { buildDrizzleTable } from "../table-builder";
28
28
  import { createTenantDb, type TenantDb } from "../tenant-db";
@@ -49,7 +49,7 @@ const adminUser = TestUsers.admin;
49
49
 
50
50
  beforeAll(async () => {
51
51
  testDb = await createTestDb();
52
- await createEntityTable(testDb.db, userEntity, "user");
52
+ await unsafeCreateEntityTable(testDb.db, userEntity, "user");
53
53
  await createEventsTable(testDb.db);
54
54
  await createProjectionStateTable(testDb.db);
55
55
  tdb = createTenantDb(testDb.db, adminUser.tenantId);
@@ -244,7 +244,7 @@ const sensitiveDrizzleTable = buildDrizzleTable("sensitive-user", sensitiveEntit
244
244
 
245
245
  describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
246
246
  beforeAll(async () => {
247
- await createEntityTable(testDb.db, sensitiveEntity, "sensitive-user");
247
+ await unsafeCreateEntityTable(testDb.db, sensitiveEntity, "sensitive-user");
248
248
  });
249
249
 
250
250
  beforeEach(async () => {
@@ -10,7 +10,7 @@
10
10
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
11
11
  import { buildDrizzleTable } from "../../db/table-builder";
12
12
  import { createEntity, createTextField } from "../../engine";
13
- import { createEntityTable, setupTestStack, type TestStack } from "../../stack";
13
+ import { setupTestStack, type TestStack, unsafeCreateEntityTable } from "../../stack";
14
14
 
15
15
  const linkEntity = createEntity({
16
16
  table: "mri_links",
@@ -25,7 +25,7 @@ let stack: TestStack;
25
25
 
26
26
  beforeAll(async () => {
27
27
  stack = await setupTestStack({ features: [] });
28
- await createEntityTable(stack.db, linkEntity);
28
+ await unsafeCreateEntityTable(stack.db, linkEntity);
29
29
  });
30
30
 
31
31
  afterAll(async () => stack?.cleanup());
@@ -44,7 +44,7 @@ describe("instant() customType is forgiving with ISO strings", () => {
44
44
  const tsTable = buildDrizzleTable("ts-row", tsEntity);
45
45
 
46
46
  test("INSERT accepts an ISO string for an instant column (forgiving path)", async () => {
47
- await createEntityTable(stack.db, tsEntity, "ts-row");
47
+ await unsafeCreateEntityTable(stack.db, tsEntity, "ts-row");
48
48
  // insertedAt is base-column, type instant. Pass an ISO string —
49
49
  // coercion in toDriver handles it. Without the fix, Drizzle-driver
50
50
  // would call .toString() on a string and produce a malformed driver
@@ -9,7 +9,7 @@ import {
9
9
  defineFeature,
10
10
  } from "../../engine";
11
11
  import type { FeatureDefinition } from "../../engine/types";
12
- import { createTestDb, pushTables, type TestDb } from "../../stack";
12
+ import { createTestDb, type TestDb, unsafePushTables } from "../../stack";
13
13
  import { buildDrizzleTable } from "../table-builder";
14
14
 
15
15
  /**
@@ -19,7 +19,7 @@ import { buildDrizzleTable } from "../table-builder";
19
19
  * Each test simulates:
20
20
  * 1. Developer defines/changes entities
21
21
  * 2. buildDrizzleTable creates Drizzle table objects
22
- * 3. Schema is applied to a real database via pushTables (drizzle-kit push)
22
+ * 3. Schema is applied to a real database via unsafePushTables (drizzle-kit push)
23
23
  * 4. We verify the DB state matches expectations
24
24
  */
25
25
 
@@ -41,7 +41,7 @@ async function applySchema(features: readonly FeatureDefinition[]): Promise<void
41
41
  tables[entityName] = buildDrizzleTable(entityName, entity);
42
42
  }
43
43
  }
44
- await pushTables(testDb.db, tables);
44
+ await unsafePushTables(testDb.db, tables);
45
45
  }
46
46
 
47
47
  // Helper: read column info from information_schema
@@ -136,7 +136,7 @@ describe("schema migration workflows", () => {
136
136
  table: "wf2_users",
137
137
  fields: { email: createTextField() },
138
138
  });
139
- await pushTables(testDb.db, { user: buildDrizzleTable("user", initialEntity) });
139
+ await unsafePushTables(testDb.db, { user: buildDrizzleTable("user", initialEntity) });
140
140
 
141
141
  // Developer adds a new field
142
142
  const updatedEntity = createEntity({
@@ -148,7 +148,7 @@ describe("schema migration workflows", () => {
148
148
  });
149
149
 
150
150
  // Push updated schema — drizzle-kit generates ALTER TABLE ADD COLUMN
151
- await pushTables(
151
+ await unsafePushTables(
152
152
  testDb.db,
153
153
  { user: buildDrizzleTable("user", updatedEntity) },
154
154
  { user: buildDrizzleTable("user", initialEntity) },
@@ -167,7 +167,7 @@ describe("schema migration workflows", () => {
167
167
  fields: { name: createTextField() },
168
168
  });
169
169
  const initialTable = buildDrizzleTable("project", initialEntity);
170
- await pushTables(testDb.db, { project: initialTable });
170
+ await unsafePushTables(testDb.db, { project: initialTable });
171
171
 
172
172
  // Insert a row first (to prove ADD COLUMN with default doesn't break existing rows)
173
173
  await testDb.db
@@ -180,7 +180,7 @@ describe("schema migration workflows", () => {
180
180
  fields: { name: createTextField(), isArchived: createBooleanField({ default: false }) },
181
181
  });
182
182
  const updatedTable = buildDrizzleTable("project", updatedEntity);
183
- await pushTables(testDb.db, { project: updatedTable }, { project: initialTable });
183
+ await unsafePushTables(testDb.db, { project: updatedTable }, { project: initialTable });
184
184
 
185
185
  // Existing row should have the default value
186
186
  const rows = await testDb.db.select().from(updatedTable);
@@ -200,7 +200,7 @@ describe("schema migration workflows", () => {
200
200
  fields: { email: createTextField({ required: true }) },
201
201
  });
202
202
  const initialTable = buildDrizzleTable("user", initialEntity);
203
- await pushTables(testDb.db, { user: initialTable });
203
+ await unsafePushTables(testDb.db, { user: initialTable });
204
204
 
205
205
  await testDb.db
206
206
  .insert(initialTable)
@@ -214,7 +214,7 @@ describe("schema migration workflows", () => {
214
214
  },
215
215
  });
216
216
  const updatedTable = buildDrizzleTable("user", updatedEntity);
217
- await pushTables(testDb.db, { user: updatedTable }, { user: initialTable });
217
+ await unsafePushTables(testDb.db, { user: updatedTable }, { user: initialTable });
218
218
 
219
219
  const rows = await testDb.db.select().from(updatedTable);
220
220
  expect(rows[0]).toMatchObject({ email: "x@y.z", roles: "[]" });