@cosmicdrift/kumiko-bundled-features 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 (48) hide show
  1. package/LICENSE +57 -0
  2. package/package.json +3 -3
  3. package/src/audit/__tests__/audit.integration.ts +2 -2
  4. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +5 -5
  5. package/src/auth-email-password/__tests__/account-lockout.integration.ts +5 -5
  6. package/src/auth-email-password/__tests__/auth-claims.integration.ts +14 -14
  7. package/src/auth-email-password/__tests__/auth.integration.ts +8 -8
  8. package/src/auth-email-password/__tests__/email-verification.integration.ts +5 -5
  9. package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +5 -5
  10. package/src/auth-email-password/__tests__/invite-flow.integration.ts +6 -6
  11. package/src/auth-email-password/__tests__/multi-roles.integration.ts +5 -5
  12. package/src/auth-email-password/__tests__/password-reset.integration.ts +5 -5
  13. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +5 -5
  14. package/src/auth-email-password/__tests__/seed-admin.integration.ts +5 -5
  15. package/src/auth-email-password/__tests__/session-callbacks.integration.ts +5 -5
  16. package/src/auth-email-password/__tests__/signup-flow.integration.ts +6 -6
  17. package/src/cap-counter/__tests__/cap-counter.integration.ts +2 -2
  18. package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +2 -2
  19. package/src/config/__tests__/config.integration.ts +2 -2
  20. package/src/delivery/__tests__/delivery-events.integration.ts +4 -4
  21. package/src/delivery/__tests__/delivery.integration.ts +4 -4
  22. package/src/feature-toggles/__tests__/feature-toggles.integration.ts +5 -5
  23. package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +84 -0
  24. package/src/feature-toggles/handlers/registered.query.ts +7 -2
  25. package/src/file-foundation/__tests__/file-foundation.integration.ts +4 -4
  26. package/src/jobs/__tests__/job-system-user.integration.ts +3 -3
  27. package/src/jobs/__tests__/jobs-events.integration.ts +2 -2
  28. package/src/jobs/__tests__/jobs-feature.integration.ts +3 -3
  29. package/src/legal-pages/__tests__/legal-pages.integration.ts +3 -3
  30. package/src/mail-foundation/__tests__/mail-foundation.integration.ts +4 -4
  31. package/src/secrets/__tests__/rotate.integration.ts +2 -2
  32. package/src/secrets/__tests__/secrets-events.integration.ts +2 -2
  33. package/src/secrets/__tests__/secrets.integration.ts +2 -2
  34. package/src/sessions/__tests__/cleanup.integration.ts +2 -2
  35. package/src/sessions/__tests__/password-auto-revoke.integration.ts +6 -6
  36. package/src/sessions/__tests__/sessions.integration.ts +6 -6
  37. package/src/tenant/__tests__/multi-tenant.integration.ts +4 -4
  38. package/src/tenant/__tests__/seed-testing.integration.ts +4 -4
  39. package/src/tenant/__tests__/tenant.integration.ts +4 -4
  40. package/src/tenant/seeding.ts +12 -1
  41. package/src/text-content/README.md +6 -2
  42. package/src/text-content/__tests__/text-content.integration.ts +2 -2
  43. package/src/tier-engine/__tests__/resolver.integration.ts +183 -0
  44. package/src/tier-engine/__tests__/tier-engine.integration.ts +5 -5
  45. package/src/tier-engine/feature.ts +345 -48
  46. package/src/tier-engine/index.ts +5 -1
  47. package/src/user/__tests__/seed-testing.integration.ts +4 -4
  48. package/src/user/__tests__/user.integration.ts +2 -2
@@ -16,11 +16,11 @@ import {
16
16
  } from "@cosmicdrift/kumiko-framework/engine";
17
17
  import { createEventDispatcher, type EventConsumer } from "@cosmicdrift/kumiko-framework/pipeline";
18
18
  import {
19
- createEntityTable,
20
19
  createTestUser,
21
- pushTables,
22
20
  setupTestStack,
23
21
  type TestStack,
22
+ unsafeCreateEntityTable,
23
+ unsafePushTables,
24
24
  } from "@cosmicdrift/kumiko-framework/stack";
25
25
  import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
26
26
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
@@ -165,12 +165,12 @@ beforeAll(async () => {
165
165
  systemHooks: [],
166
166
  });
167
167
 
168
- await pushTables(stack.db, { globalFeatureStateTable });
168
+ await unsafePushTables(stack.db, { globalFeatureStateTable });
169
169
  // widgetTrackerTable is auto-pushed by setupTestStack because it's the
170
170
  // projection-table of a registered r.multiStreamProjection — manually
171
171
  // pushing again would re-run the CREATE TABLE and fail duplicate.
172
- await createEntityTable(stack.db, widgetEntity);
173
- await createEntityTable(stack.db, widgetAuditEntity, "widget-audit");
172
+ await unsafeCreateEntityTable(stack.db, widgetEntity);
173
+ await unsafeCreateEntityTable(stack.db, widgetAuditEntity, "widget-audit");
174
174
 
175
175
  runtime = new GlobalFeatureToggleRuntime(stack.db, stack.registry);
176
176
  await runtime.initialize();
@@ -0,0 +1,84 @@
1
+ // Sprint 8a recipe-test pin: registered.query nutzt SYSTEM_TENANT_ID
2
+ // statt event.user.tenantId. Operator-tooling muss PLATTFORM-truth
3
+ // sehen, nicht den eigenen tier-cut. Convention ist in DispatcherOptions.
4
+ // effectiveFeatures dokumentiert; dieser test pinst sie damit ein
5
+ // future-refactor (z.B. mechanisches sed oder copy-paste) sie nicht
6
+ // silent zurückdreht zu event.user.tenantId.
7
+ //
8
+ // Pure unit-test ist nicht möglich weil registered.query einen DB-select
9
+ // auf globalFeatureStateTable macht BEVOR der effectiveFeatures-call
10
+ // läuft. Wir mocken den ctx.db.select-pfad damit der handler komplett
11
+ // durchläuft. Die Convention-Pin ist die einzige Aussage des tests —
12
+ // echtes integration-Verhalten deckt feature-toggles.integration.ts ab.
13
+
14
+ import {
15
+ createEntity,
16
+ createRegistry,
17
+ createTextField,
18
+ defineFeature,
19
+ SYSTEM_TENANT_ID,
20
+ type TenantId,
21
+ } from "@cosmicdrift/kumiko-framework/engine";
22
+ import { createDispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
23
+ import { createTestUser } from "@cosmicdrift/kumiko-framework/stack";
24
+ import { describe, expect, test } from "vitest";
25
+ import { createFeatureTogglesFeature } from "../feature";
26
+ import type { GlobalFeatureToggleRuntime } from "../toggle-runtime";
27
+
28
+ describe("Sprint 8a: registered.query SYSTEM_TENANT_ID convention", () => {
29
+ test("ruft effectiveFeatures mit SYSTEM_TENANT_ID, nicht mit caller-tenantId", async () => {
30
+ const observed: string[] = [];
31
+
32
+ const dummy = defineFeature("dummy", (r) => {
33
+ r.entity("widget", createEntity({ table: "Widgets", fields: { name: createTextField() } }));
34
+ });
35
+
36
+ const runtime: GlobalFeatureToggleRuntime | null = null;
37
+ const featureToggles = createFeatureTogglesFeature({
38
+ getRuntime: () => {
39
+ if (!runtime) throw new Error("runtime not initialized");
40
+ return runtime;
41
+ },
42
+ });
43
+
44
+ const registry = createRegistry([dummy, featureToggles]);
45
+
46
+ // Mock ctx.db.select-chain damit der handler durch den DB-Pfad
47
+ // kommt. Wir liefern leere overrides (.from() returnt []), das
48
+ // genügt — registered.query iteriert dann über registry.features
49
+ // und ruft ctx.effectiveFeatures, was unser observable ist.
50
+ const mockDb = {
51
+ select: () => ({ from: async () => [] as unknown[] }),
52
+ } as unknown as Parameters<typeof createDispatcher>[1]["db"];
53
+
54
+ const callerTenant = "00000000-0000-4000-8000-0000000000c1" as TenantId;
55
+
56
+ const dispatcher = createDispatcher(
57
+ registry,
58
+ { db: mockDb },
59
+ {
60
+ effectiveFeatures: (tenantId) => {
61
+ observed.push(tenantId);
62
+ return new Set(["dummy", "feature-toggles"]);
63
+ },
64
+ },
65
+ );
66
+
67
+ const admin = createTestUser({
68
+ id: "admin-1",
69
+ tenantId: callerTenant,
70
+ roles: ["SystemAdmin"],
71
+ });
72
+
73
+ await dispatcher.query("feature-toggles:query:registered", {}, admin);
74
+
75
+ // Pin: registered.query call führt zu MINDESTENS zwei effectiveFeatures-
76
+ // calls:
77
+ // 1. dispatcher's checkFeatureEnabled (mit user.tenantId = callerTenant)
78
+ // 2. registered.query handler-body (mit SYSTEM_TENANT_ID)
79
+ // Wenn ein future-refactor SYSTEM_TENANT_ID zu event.user.tenantId zurück-
80
+ // dreht, fehlt der zweite call und dieser test fail't.
81
+ expect(observed).toContain(callerTenant);
82
+ expect(observed).toContain(SYSTEM_TENANT_ID);
83
+ });
84
+ });
@@ -1,4 +1,4 @@
1
- import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { z } from "zod";
3
3
  import { globalFeatureStateTable } from "../global-feature-state-table";
4
4
 
@@ -24,7 +24,12 @@ export const registeredQuery = defineQueryHandler({
24
24
  .from(globalFeatureStateTable)) as OverrideRow[];
25
25
  const overrides = new Map(overrideRows.map((r) => [r.featureName, r.enabled]));
26
26
 
27
- const effective = ctx.effectiveFeatures?.();
27
+ // SystemAdmin operator-tooling: das listing soll die PLATTFORM-truth
28
+ // zeigen (alle features im Registry), nicht den eigenen tier-cut.
29
+ // Sprint-8a per-tenant signature → wir rufen mit SYSTEM_TENANT_ID,
30
+ // App-resolver returnt union-of-all-tier-features. Sentinel-Convention
31
+ // dokumentiert in DispatcherOptions.effectiveFeatures.
32
+ const effective = ctx.effectiveFeatures?.(SYSTEM_TENANT_ID);
28
33
 
29
34
  const items = [];
30
35
  for (const feature of ctx.registry.features.values()) {
@@ -8,12 +8,12 @@ import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework
8
8
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
9
9
  import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
10
10
  import {
11
- createEntityTable,
12
11
  createTestUser,
13
- pushTables,
14
12
  setupTestStack,
15
13
  type TestStack,
16
14
  testTenantId,
15
+ unsafeCreateEntityTable,
16
+ unsafePushTables,
17
17
  } from "@cosmicdrift/kumiko-framework/stack";
18
18
  import {
19
19
  createMutableMasterKeyProvider,
@@ -102,8 +102,8 @@ beforeAll(async () => {
102
102
  });
103
103
  db = stack.db;
104
104
 
105
- await createEntityTable(db, tenantEntity);
106
- await pushTables(db, { configValuesTable, tenant_secrets: tenantSecretsTable });
105
+ await unsafeCreateEntityTable(db, tenantEntity);
106
+ await unsafePushTables(db, { configValuesTable, tenant_secrets: tenantSecretsTable });
107
107
  await createEventsTable(db);
108
108
  });
109
109
 
@@ -20,10 +20,10 @@ import { createJobRunner, type JobRunner } from "@cosmicdrift/kumiko-framework/j
20
20
  import {
21
21
  createTestDb,
22
22
  createTestRedis,
23
- pushTables,
24
23
  type TestDb,
25
24
  type TestRedis,
26
25
  TestUsers,
26
+ unsafePushTables,
27
27
  } from "@cosmicdrift/kumiko-framework/stack";
28
28
  import { bridgeStub, sleep } from "@cosmicdrift/kumiko-framework/testing";
29
29
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
@@ -99,11 +99,11 @@ beforeAll(async () => {
99
99
  testRedis = await createTestRedis();
100
100
  db = testDb.db;
101
101
 
102
- await pushTables(db, { configValuesTable });
102
+ await unsafePushTables(db, { configValuesTable });
103
103
  // Post-ES config writes go through the event-store executor, which needs
104
104
  // the framework events + archived-streams tables to exist before the
105
105
  // first append. setupTestStack provisions them automatically; this test
106
- // builds its DB manually (createTestDb + pushTables), so we do it here.
106
+ // builds its DB manually (createTestDb + unsafePushTables), so we do it here.
107
107
  await createEventsTable(db);
108
108
  await createArchivedStreamsTable(db);
109
109
 
@@ -13,9 +13,9 @@ import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/ev
13
13
  import {
14
14
  createTestDb,
15
15
  createTestRedis,
16
- pushTables,
17
16
  type TestDb,
18
17
  type TestRedis,
18
+ unsafePushTables,
19
19
  } from "@cosmicdrift/kumiko-framework/stack";
20
20
  import { eq } from "drizzle-orm";
21
21
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
@@ -37,7 +37,7 @@ beforeAll(async () => {
37
37
  testDb = await createTestDb();
38
38
  testRedis = await createTestRedis();
39
39
  const registry = createRegistry([createJobsFeature()]);
40
- await pushTables(testDb.db, { jobRunsTable, jobRunLogsTable });
40
+ await unsafePushTables(testDb.db, { jobRunsTable, jobRunLogsTable });
41
41
  await createEventsTable(testDb.db);
42
42
  logger = createJobRunLogger({ db: testDb.db, registry });
43
43
  });
@@ -11,10 +11,10 @@ import {
11
11
  createTestDb,
12
12
  createTestRedis,
13
13
  createTestUser,
14
- pushTables,
15
14
  type TestDb,
16
15
  type TestRedis,
17
16
  TestUsers,
17
+ unsafePushTables,
18
18
  } from "@cosmicdrift/kumiko-framework/stack";
19
19
  import { sleep } from "@cosmicdrift/kumiko-framework/testing";
20
20
  import type { Hono } from "hono";
@@ -72,10 +72,10 @@ beforeAll(async () => {
72
72
  const registry = createRegistry([appFeature, jobsFeature]);
73
73
 
74
74
  // jobRuns + jobRunLogs are projection tables (auto-pushed by
75
- // pushTables via the registry-declared inline projections in jobs-feature).
75
+ // unsafePushTables via the registry-declared inline projections in jobs-feature).
76
76
  // We need events + archived_streams for the ES writes the job-runner's
77
77
  // logger does.
78
- await pushTables(db, { jobRunsTable, jobRunLogsTable });
78
+ await unsafePushTables(db, { jobRunsTable, jobRunLogsTable });
79
79
  await createEventsTable(db);
80
80
 
81
81
  const redisUrl = `redis://${testRedis.redis.options.host}:${testRedis.redis.options.port}/${testRedis.redis.options.db}`;
@@ -9,9 +9,9 @@ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
9
9
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
10
10
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
11
11
  import {
12
- createEntityTable,
13
12
  setupTestStack,
14
13
  type TestStack,
14
+ unsafeCreateEntityTable,
15
15
  } from "@cosmicdrift/kumiko-framework/stack";
16
16
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
17
17
  import { createLegalPagesFeature, runLegalPagesBootCheck } from "../feature";
@@ -36,7 +36,7 @@ beforeAll(async () => {
36
36
  }),
37
37
  });
38
38
  db = stack.db;
39
- await createEntityTable(db, textBlockEntity);
39
+ await unsafeCreateEntityTable(db, textBlockEntity);
40
40
  await createEventsTable(db);
41
41
 
42
42
  // Seed legal blocks für SYSTEM_TENANT in DE
@@ -219,7 +219,7 @@ describe("legal-pages :: SYSTEM_TENANT-routing (production-bug-regression)", ()
219
219
  }),
220
220
  });
221
221
  try {
222
- await createEntityTable(hostScopedStack.db, textBlockEntity);
222
+ await unsafeCreateEntityTable(hostScopedStack.db, textBlockEntity);
223
223
  await createEventsTable(hostScopedStack.db);
224
224
 
225
225
  // Block NUR im SYSTEM_TENANT seeden — NICHT im otherTenantId
@@ -14,12 +14,12 @@ import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework
14
14
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
15
15
  import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
16
16
  import {
17
- createEntityTable,
18
17
  createTestUser,
19
- pushTables,
20
18
  setupTestStack,
21
19
  type TestStack,
22
20
  testTenantId,
21
+ unsafeCreateEntityTable,
22
+ unsafePushTables,
23
23
  } from "@cosmicdrift/kumiko-framework/stack";
24
24
  import {
25
25
  createMutableMasterKeyProvider,
@@ -106,8 +106,8 @@ beforeAll(async () => {
106
106
  });
107
107
  db = stack.db;
108
108
 
109
- await createEntityTable(db, tenantEntity);
110
- await pushTables(db, { configValuesTable, tenant_secrets: tenantSecretsTable });
109
+ await unsafeCreateEntityTable(db, tenantEntity);
110
+ await unsafePushTables(db, { configValuesTable, tenant_secrets: tenantSecretsTable });
111
111
  await createEventsTable(db);
112
112
  });
113
113
 
@@ -12,9 +12,9 @@ import {
12
12
  } from "@cosmicdrift/kumiko-framework/secrets";
13
13
  import {
14
14
  createTestUser,
15
- pushTables,
16
15
  setupTestStack,
17
16
  type TestStack,
17
+ unsafePushTables,
18
18
  } from "@cosmicdrift/kumiko-framework/stack";
19
19
  import { eq, sql } from "drizzle-orm";
20
20
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
@@ -76,7 +76,7 @@ beforeAll(async () => {
76
76
  secrets: createSecretsContext({ db, masterKeyProvider: seedProvider }),
77
77
  }),
78
78
  });
79
- await pushTables(stack.db, {
79
+ await unsafePushTables(stack.db, {
80
80
  tenant_secrets: tenantSecretsTable,
81
81
  });
82
82
 
@@ -13,9 +13,9 @@ import {
13
13
  } from "@cosmicdrift/kumiko-framework/secrets";
14
14
  import {
15
15
  createTestUser,
16
- pushTables,
17
16
  setupTestStack,
18
17
  type TestStack,
18
+ unsafePushTables,
19
19
  } from "@cosmicdrift/kumiko-framework/stack";
20
20
  import { eq } from "drizzle-orm";
21
21
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
@@ -50,7 +50,7 @@ beforeAll(async () => {
50
50
  secrets: createSecretsContext({ db, masterKeyProvider: provider }),
51
51
  }),
52
52
  });
53
- await pushTables(stack.db, { tenantSecretsTable });
53
+ await unsafePushTables(stack.db, { tenantSecretsTable });
54
54
  await createEventsTable(stack.db);
55
55
  });
56
56
 
@@ -12,9 +12,9 @@ import {
12
12
  } from "@cosmicdrift/kumiko-framework/secrets";
13
13
  import {
14
14
  createTestUser,
15
- pushTables,
16
15
  setupTestStack,
17
16
  type TestStack,
17
+ unsafePushTables,
18
18
  } from "@cosmicdrift/kumiko-framework/stack";
19
19
  import { and, eq } from "drizzle-orm";
20
20
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
@@ -49,7 +49,7 @@ beforeAll(async () => {
49
49
  // table (tenant_secrets) still needs an explicit push here, since it
50
50
  // belongs to an ES entity (and entity-tables aren't auto-pushed by
51
51
  // setupTestStack).
52
- await pushTables(stack.db, { tenant_secrets: tenantSecretsTable });
52
+ await unsafePushTables(stack.db, { tenant_secrets: tenantSecretsTable });
53
53
  await createEventsTable(stack.db);
54
54
  });
55
55
 
@@ -6,10 +6,10 @@
6
6
 
7
7
  import type { AppContext } from "@cosmicdrift/kumiko-framework/engine";
8
8
  import {
9
- createEntityTable,
10
9
  setupTestStack,
11
10
  type TestStack,
12
11
  testTenantId,
12
+ unsafeCreateEntityTable,
13
13
  } from "@cosmicdrift/kumiko-framework/stack";
14
14
  import { sql } from "drizzle-orm";
15
15
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
@@ -38,7 +38,7 @@ beforeAll(async () => {
38
38
  stack = await setupTestStack({
39
39
  features: [createSessionsFeature()],
40
40
  });
41
- await createEntityTable(stack.db, userSessionEntity);
41
+ await unsafeCreateEntityTable(stack.db, userSessionEntity);
42
42
  });
43
43
 
44
44
  afterAll(async () => {
@@ -2,11 +2,11 @@ import { randomBytes } from "node:crypto";
2
2
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import {
5
- createEntityTable,
6
- pushTables,
7
5
  setupTestStack,
8
6
  type TestStack,
9
7
  testTenantId,
8
+ unsafeCreateEntityTable,
9
+ unsafePushTables,
10
10
  } from "@cosmicdrift/kumiko-framework/stack";
11
11
  import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
12
12
  import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
@@ -77,10 +77,10 @@ beforeAll(async () => {
77
77
  callbacks.set(createSessionCallbacks({ db: stack.db }));
78
78
  h = makeSessionHelpers(stack, TENANT);
79
79
 
80
- await createEntityTable(stack.db, userEntity);
81
- await createEntityTable(stack.db, tenantEntity);
82
- await createEntityTable(stack.db, userSessionEntity);
83
- await pushTables(stack.db, { configValuesTable, tenantMembershipsTable });
80
+ await unsafeCreateEntityTable(stack.db, userEntity);
81
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
82
+ await unsafeCreateEntityTable(stack.db, userSessionEntity);
83
+ await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
84
84
  });
85
85
 
86
86
  afterAll(async () => {
@@ -2,11 +2,11 @@ import { randomBytes } from "node:crypto";
2
2
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import {
5
- createEntityTable,
6
- pushTables,
7
5
  setupTestStack,
8
6
  type TestStack,
9
7
  testTenantId,
8
+ unsafeCreateEntityTable,
9
+ unsafePushTables,
10
10
  } from "@cosmicdrift/kumiko-framework/stack";
11
11
  import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
12
12
  import { and, eq } from "drizzle-orm";
@@ -64,10 +64,10 @@ beforeAll(async () => {
64
64
  callbacks.set(createSessionCallbacks({ db: stack.db }));
65
65
  h = makeSessionHelpers(stack, TENANT);
66
66
 
67
- await createEntityTable(stack.db, userEntity);
68
- await createEntityTable(stack.db, tenantEntity);
69
- await createEntityTable(stack.db, userSessionEntity);
70
- await pushTables(stack.db, { configValuesTable, tenantMembershipsTable });
67
+ await unsafeCreateEntityTable(stack.db, userEntity);
68
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
69
+ await unsafeCreateEntityTable(stack.db, userSessionEntity);
70
+ await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
71
71
  });
72
72
 
73
73
  afterAll(async () => {
@@ -9,15 +9,15 @@ import {
9
9
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
10
10
  import { createJobRunner, type JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
11
11
  import {
12
- createEntityTable,
13
12
  createTestDb,
14
13
  createTestRedis,
15
14
  createTestUser,
16
- pushTables,
17
15
  type TestDb,
18
16
  type TestRedis,
19
17
  TestUsers,
20
18
  testTenantId,
19
+ unsafeCreateEntityTable,
20
+ unsafePushTables,
21
21
  } from "@cosmicdrift/kumiko-framework/stack";
22
22
  import { bridgeStub, sleep } from "@cosmicdrift/kumiko-framework/testing";
23
23
  import type { Hono } from "hono";
@@ -60,8 +60,8 @@ beforeAll(async () => {
60
60
  testRedis = await createTestRedis();
61
61
  db = testDb.db;
62
62
 
63
- await createEntityTable(db, tenantEntity);
64
- await pushTables(db, { tenantMembershipsTable, configValuesTable });
63
+ await unsafeCreateEntityTable(db, tenantEntity);
64
+ await unsafePushTables(db, { tenantMembershipsTable, configValuesTable });
65
65
  await createEventsTable(db);
66
66
 
67
67
  const configFeature = createConfigFeature();
@@ -14,12 +14,12 @@
14
14
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
15
15
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
16
16
  import {
17
- createEntityTable,
18
17
  createTestUser,
19
- pushTables,
20
18
  setupTestStack,
21
19
  type TestStack,
22
20
  TestUsers,
21
+ unsafeCreateEntityTable,
22
+ unsafePushTables,
23
23
  } from "@cosmicdrift/kumiko-framework/stack";
24
24
  import { and, eq } from "drizzle-orm";
25
25
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
@@ -43,8 +43,8 @@ beforeAll(async () => {
43
43
  features: [createConfigFeature(), createTenantFeature()],
44
44
  extraContext: { configResolver: resolver },
45
45
  });
46
- await createEntityTable(stack.db, tenantEntity);
47
- await pushTables(stack.db, { configValuesTable, tenantMembershipsTable });
46
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
47
+ await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
48
48
  await createEventsTable(stack.db);
49
49
  });
50
50
 
@@ -2,12 +2,12 @@ import { randomBytes } from "node:crypto";
2
2
  import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
4
4
  import {
5
- createEntityTable,
6
5
  createTestUser,
7
- pushTables,
8
6
  setupTestStack,
9
7
  type TestStack,
10
8
  TestUsers,
9
+ unsafeCreateEntityTable,
10
+ unsafePushTables,
11
11
  } from "@cosmicdrift/kumiko-framework/stack";
12
12
  import { expectErrorIncludes, rolesOf } from "@cosmicdrift/kumiko-framework/testing";
13
13
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
@@ -42,8 +42,8 @@ beforeAll(async () => {
42
42
  });
43
43
  db = stack.db;
44
44
 
45
- await createEntityTable(db, tenantEntity);
46
- await pushTables(db, { configValuesTable });
45
+ await unsafeCreateEntityTable(db, tenantEntity);
46
+ await unsafePushTables(db, { configValuesTable });
47
47
  await createEventsTable(db);
48
48
  });
49
49
 
@@ -38,8 +38,9 @@ import {
38
38
  fetchOne,
39
39
  } from "@cosmicdrift/kumiko-framework/db";
40
40
  import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
41
+ import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
41
42
  import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
42
- import { eq } from "drizzle-orm";
43
+ import { eq, max as maxFn } from "drizzle-orm";
43
44
  import { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table";
44
45
  import { tenantEntity, tenantTable } from "./schema/tenant";
45
46
 
@@ -94,6 +95,16 @@ export async function seedTenant(db: DbConnection, options: SeedTenantOptions):
94
95
  const existing = await fetchOne(db, tenantTable, eq(tenantTable["id"], options.id));
95
96
  if (existing) return options.id;
96
97
 
98
+ // Idempotenz: Aggregate kann im Event-Store existieren ohne Projection-Row
99
+ // (Projection-Drift nach rebuild, manuellem DELETE, oder async-lag). Wenn
100
+ // Stream-Version > 0 → kein create() — wäre version_conflict. Caller
101
+ // bekommt die ID, Projection wird beim nächsten Dispatcher-Cycle aufgebaut.
102
+ const [streamRow] = await db
103
+ .select({ v: maxFn(eventsTable.version) })
104
+ .from(eventsTable)
105
+ .where(eq(eventsTable.aggregateId, options.id));
106
+ if ((streamRow?.v ?? 0) > 0) return options.id;
107
+
97
108
  const result = await tenantExecutor.create(
98
109
  { id: options.id, key: options.key, name: options.name },
99
110
  by,
@@ -38,12 +38,16 @@ container exits. No auto-heal in production. See
38
38
  In integration tests (vitest) it's enough to do:
39
39
 
40
40
  ```typescript
41
- import { createEntityTable } from "@cosmicdrift/kumiko-framework/stack";
41
+ import { unsafeCreateEntityTable } from "@cosmicdrift/kumiko-framework/stack";
42
42
  import { textBlockEntity } from "@cosmicdrift/kumiko-bundled-features/text-content";
43
43
 
44
- await createEntityTable(stack.db, textBlockEntity);
44
+ await unsafeCreateEntityTable(stack.db, textBlockEntity);
45
45
  ```
46
46
 
47
+ The `unsafe` prefix is intentional — it bypasses the projection
48
+ registry and is reserved for test setup and framework-internals. Apps
49
+ declare data via `r.entity(...)` everywhere else.
50
+
47
51
  ## Use cases
48
52
 
49
53
  text-content is generic — anything that's static Markdown text per
@@ -1,11 +1,11 @@
1
1
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
3
3
  import {
4
- createEntityTable,
5
4
  createTestUser,
6
5
  setupTestStack,
7
6
  type TestStack,
8
7
  TestUsers,
8
+ unsafeCreateEntityTable,
9
9
  } from "@cosmicdrift/kumiko-framework/stack";
10
10
  import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
11
11
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
@@ -26,7 +26,7 @@ const feature = createTextContentFeature();
26
26
  beforeAll(async () => {
27
27
  stack = await setupTestStack({ features: [feature] });
28
28
  db = stack.db;
29
- await createEntityTable(db, textBlockEntity);
29
+ await unsafeCreateEntityTable(db, textBlockEntity);
30
30
  await createEventsTable(db);
31
31
  });
32
32