@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.
- package/LICENSE +57 -0
- package/package.json +3 -3
- package/src/__tests__/anonymous-access.integration.ts +7 -7
- package/src/__tests__/error-contract.integration.ts +2 -2
- package/src/__tests__/field-access.integration.ts +2 -2
- package/src/__tests__/full-stack.integration.ts +2 -2
- package/src/__tests__/ownership.integration.ts +2 -2
- package/src/__tests__/raw-table.integration.ts +128 -0
- package/src/__tests__/reference-data.integration.ts +2 -2
- package/src/__tests__/transition-guard.integration.ts +4 -4
- package/src/api/__tests__/batch.integration.ts +3 -3
- package/src/api/__tests__/dispatcher-live.integration.ts +2 -2
- package/src/api/__tests__/nested-write.integration.ts +3 -3
- package/src/db/__tests__/drizzle-helpers.integration.ts +2 -2
- package/src/db/__tests__/event-store-executor-list.integration.ts +2 -2
- package/src/db/__tests__/event-store-executor.integration.ts +9 -3
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +3 -3
- package/src/db/__tests__/multi-row-insert.integration.ts +3 -3
- package/src/db/__tests__/schema-migration.integration.ts +9 -9
- package/src/db/__tests__/tenant-db.integration.ts +4 -4
- package/src/db/__tests__/unique-violation-mapping.integration.ts +2 -2
- package/src/db/schema-inspection.ts +1 -1
- package/src/engine/__tests__/raw-table.test.ts +149 -0
- package/src/engine/define-feature.ts +38 -0
- package/src/engine/index.ts +6 -0
- package/src/engine/registry.ts +46 -0
- package/src/engine/tier-resolver-extension.ts +78 -0
- package/src/engine/types/feature.ts +55 -0
- package/src/engine/types/handlers.ts +13 -5
- package/src/engine/types/index.ts +3 -0
- package/src/event-store/__tests__/upcaster.integration.ts +11 -5
- package/src/event-store/archive.ts +2 -2
- package/src/event-store/events-schema.ts +2 -2
- package/src/event-store/snapshot.ts +2 -2
- package/src/event-store/upcaster-dead-letter.ts +2 -2
- package/src/files/__tests__/file-field-column.integration.ts +4 -4
- package/src/files/__tests__/file-field-pipeline.integration.ts +2 -2
- package/src/files/__tests__/files.integration.ts +8 -8
- package/src/observability/__tests__/observability.integration.ts +2 -2
- package/src/pipeline/__tests__/archive-stream.integration.ts +2 -2
- package/src/pipeline/__tests__/cascade-handler.integration.ts +9 -9
- package/src/pipeline/__tests__/causation-chain.integration.ts +2 -2
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +3 -3
- package/src/pipeline/__tests__/dispatcher.test.ts +73 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +2 -2
- package/src/pipeline/__tests__/event-dedup.integration.ts +2 -2
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +2 -2
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +3 -3
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +2 -2
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +2 -2
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +2 -2
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +4 -4
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +4 -4
- package/src/pipeline/__tests__/event-retention.integration.ts +2 -2
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +2 -2
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +100 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +2 -2
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +2 -2
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +2 -2
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +3 -3
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +2 -2
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +9 -3
- package/src/pipeline/__tests__/query-projection.integration.ts +2 -2
- package/src/pipeline/dispatcher.ts +35 -15
- package/src/pipeline/event-consumer-state.ts +2 -2
- package/src/pipeline/event-dispatcher.ts +10 -1
- package/src/pipeline/lifecycle-pipeline.ts +22 -4
- package/src/pipeline/projection-state.ts +3 -3
- package/src/stack/index.ts +4 -3
- package/src/stack/push-entity-projection-tables.ts +51 -0
- package/src/stack/table-helpers.ts +20 -13
- package/src/stack/test-stack.ts +14 -5
- package/src/testing/__tests__/ensure-entity-table.integration.ts +16 -11
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../db/dialect";
|
|
13
13
|
import { tableExists } from "../db/schema-inspection";
|
|
14
14
|
import type { TenantId } from "../engine/types";
|
|
15
|
-
import {
|
|
15
|
+
import { unsafePushTables } from "../stack";
|
|
16
16
|
import { isStreamArchived } from "./archive";
|
|
17
17
|
import { loadEventsAfterVersion, type StoredEvent } from "./event-store";
|
|
18
18
|
|
|
@@ -73,7 +73,7 @@ export const snapshotsTable = pgTable(
|
|
|
73
73
|
export async function createSnapshotsTable(db: DbConnection): Promise<void> {
|
|
74
74
|
// skip: table already exists — idempotent boot + test-setup call
|
|
75
75
|
if (await tableExists(db, "public.kumiko_snapshots")) return;
|
|
76
|
-
await
|
|
76
|
+
await unsafePushTables(db, { kumikoSnapshots: snapshotsTable });
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export type Snapshot<TState extends Record<string, unknown> = Record<string, unknown>> = {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { bigint, index, integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
16
16
|
import type { DbConnection, DbRunner } from "../db/connection";
|
|
17
17
|
import { tableExists } from "../db/schema-inspection";
|
|
18
|
-
import {
|
|
18
|
+
import { unsafePushTables } from "../stack";
|
|
19
19
|
import type { StoredEvent } from "./event-store";
|
|
20
20
|
|
|
21
21
|
export const upcasterDeadLetterTable = pgTable(
|
|
@@ -50,7 +50,7 @@ export const upcasterDeadLetterTable = pgTable(
|
|
|
50
50
|
export async function createUpcasterDeadLetterTable(db: DbConnection): Promise<void> {
|
|
51
51
|
// skip: table already exists — bootstrap called from multiple paths
|
|
52
52
|
if (await tableExists(db, "public.kumiko_upcaster_dead_letters")) return;
|
|
53
|
-
await
|
|
53
|
+
await unsafePushTables(db, { kumikoUpcasterDeadLetters: upcasterDeadLetterTable });
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// Writes a dead-letter row. Called by upcastStoredEvent when errorPolicy
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { sql } from "drizzle-orm";
|
|
11
11
|
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
12
12
|
import { createEntity, createFileField, createImageField } from "../../engine";
|
|
13
|
-
import {
|
|
13
|
+
import { createTestDb, type TestDb, unsafeCreateEntityTable, unsafePushTables } from "../../stack";
|
|
14
14
|
import { generateId } from "../../utils";
|
|
15
15
|
import { fileRefsTable } from "../file-ref-table";
|
|
16
16
|
|
|
@@ -28,8 +28,8 @@ let testDb: TestDb;
|
|
|
28
28
|
|
|
29
29
|
beforeAll(async () => {
|
|
30
30
|
testDb = await createTestDb();
|
|
31
|
-
await
|
|
32
|
-
await
|
|
31
|
+
await unsafePushTables(testDb.db, { fileRefsTable });
|
|
32
|
+
await unsafeCreateEntityTable(testDb.db, documentEntity);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
afterAll(async () => {
|
|
@@ -40,7 +40,7 @@ describe("file-field entity-column type", () => {
|
|
|
40
40
|
test("`file` and `image` fields generate UUID columns (not integer)", async () => {
|
|
41
41
|
// Pull the actual column type from information_schema. This is the
|
|
42
42
|
// load-bearing assertion: the type emitted by drizzle-kit during
|
|
43
|
-
// `
|
|
43
|
+
// `unsafeCreateEntityTable` must be `uuid`. A regression to `integer` would
|
|
44
44
|
// fail here even if higher-level code happened to still work through
|
|
45
45
|
// implicit casts.
|
|
46
46
|
const rows = await testDb.db.execute<{ column_name: string; data_type: string }>(sql`
|
|
@@ -30,11 +30,11 @@ import {
|
|
|
30
30
|
defineFeature,
|
|
31
31
|
} from "../../engine";
|
|
32
32
|
import {
|
|
33
|
-
createEntityTable,
|
|
34
33
|
createTestUser,
|
|
35
34
|
setupTestStack,
|
|
36
35
|
type TestStack,
|
|
37
36
|
testTenantId,
|
|
37
|
+
unsafeCreateEntityTable,
|
|
38
38
|
} from "../../stack";
|
|
39
39
|
import { createLocalProvider } from "../local-provider";
|
|
40
40
|
|
|
@@ -74,7 +74,7 @@ beforeAll(async () => {
|
|
|
74
74
|
features: [documentFeature],
|
|
75
75
|
files: { storageProvider: createLocalProvider(storagePath) },
|
|
76
76
|
});
|
|
77
|
-
await
|
|
77
|
+
await unsafeCreateEntityTable(stack.db, documentEntity);
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
afterAll(async () => {
|
|
@@ -15,12 +15,12 @@ import {
|
|
|
15
15
|
} from "../../engine";
|
|
16
16
|
import { createEventsTable, loadAggregate } from "../../event-store";
|
|
17
17
|
import {
|
|
18
|
-
createEntityTable,
|
|
19
18
|
createTestDb,
|
|
20
19
|
createTestUser,
|
|
21
|
-
pushTables,
|
|
22
20
|
type TestDb,
|
|
23
21
|
TestUsers,
|
|
22
|
+
unsafeCreateEntityTable,
|
|
23
|
+
unsafePushTables,
|
|
24
24
|
} from "../../stack";
|
|
25
25
|
import { expectErrorIncludes } from "../../testing";
|
|
26
26
|
import { fileRefsTable } from "../file-ref-table";
|
|
@@ -64,8 +64,8 @@ beforeAll(async () => {
|
|
|
64
64
|
storagePath = await mkdtemp(join(tmpdir(), "kumiko-files-test-"));
|
|
65
65
|
|
|
66
66
|
// Create tables
|
|
67
|
-
await
|
|
68
|
-
await
|
|
67
|
+
await unsafePushTables(testDb.db, { fileRefsTable });
|
|
68
|
+
await unsafeCreateEntityTable(testDb.db, testTenantEntity);
|
|
69
69
|
// Event-store table: the upload route appends files:event:uploaded in the
|
|
70
70
|
// same tx as the FileRef insert. Without events, upload would 500.
|
|
71
71
|
await createEventsTable(testDb.db);
|
|
@@ -402,8 +402,8 @@ describe("custom file access guard", () => {
|
|
|
402
402
|
): Promise<void> {
|
|
403
403
|
const { storageProvider: providerOverride, ...routeOptions } = options;
|
|
404
404
|
const isolatedDb = await createTestDb();
|
|
405
|
-
await
|
|
406
|
-
await
|
|
405
|
+
await unsafePushTables(isolatedDb.db, { fileRefsTable });
|
|
406
|
+
await unsafeCreateEntityTable(isolatedDb.db, testTenantEntity);
|
|
407
407
|
const storagePath = await mkdtemp(join(tmpdir(), "kumiko-files-custom-"));
|
|
408
408
|
const provider = providerOverride ?? createLocalProvider(storagePath);
|
|
409
409
|
const isolatedRegistry = createRegistry([tenantFeature]);
|
|
@@ -716,8 +716,8 @@ describe("download-url endpoint", () => {
|
|
|
716
716
|
}) => Promise<void>,
|
|
717
717
|
): Promise<void> {
|
|
718
718
|
const isolatedDb = await createTestDb();
|
|
719
|
-
await
|
|
720
|
-
await
|
|
719
|
+
await unsafePushTables(isolatedDb.db, { fileRefsTable });
|
|
720
|
+
await unsafeCreateEntityTable(isolatedDb.db, testTenantEntity);
|
|
721
721
|
const isolatedRegistry = createRegistry([tenantFeature]);
|
|
722
722
|
const isolatedServer = buildServer({
|
|
723
723
|
registry: isolatedRegistry,
|
|
@@ -6,11 +6,11 @@ import type { AppContext, SaveContext } from "../../engine/types";
|
|
|
6
6
|
import { createJobRunner } from "../../jobs";
|
|
7
7
|
import { createLogger } from "../../logging/pino-logger";
|
|
8
8
|
import {
|
|
9
|
-
createEntityTable,
|
|
10
9
|
createTestRedis,
|
|
11
10
|
setupTestStack,
|
|
12
11
|
type TestRedis,
|
|
13
12
|
type TestStack,
|
|
13
|
+
unsafeCreateEntityTable,
|
|
14
14
|
} from "../../stack";
|
|
15
15
|
import { createRecordingProvider, type RecordingProvider, waitFor } from "../../testing";
|
|
16
16
|
|
|
@@ -194,7 +194,7 @@ describe("Observability (integration) — DB + pipeline hook spans", () => {
|
|
|
194
194
|
observability: provider,
|
|
195
195
|
systemHooks: [],
|
|
196
196
|
});
|
|
197
|
-
await
|
|
197
|
+
await unsafeCreateEntityTable(stack.db, todoEntity, "todo");
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
afterEach(async () => {
|
|
@@ -16,11 +16,11 @@ import {
|
|
|
16
16
|
loadAggregate as loadAggregateRaw,
|
|
17
17
|
} from "../../event-store";
|
|
18
18
|
import {
|
|
19
|
-
createEntityTable,
|
|
20
19
|
resetEventStore,
|
|
21
20
|
setupTestStack,
|
|
22
21
|
type TestStack,
|
|
23
22
|
TestUsers,
|
|
23
|
+
unsafeCreateEntityTable,
|
|
24
24
|
} from "../../stack";
|
|
25
25
|
|
|
26
26
|
const itemEntity = createEntity({
|
|
@@ -103,7 +103,7 @@ const admin = TestUsers.admin;
|
|
|
103
103
|
|
|
104
104
|
beforeAll(async () => {
|
|
105
105
|
stack = await setupTestStack({ features: [archFeature], systemHooks: [] });
|
|
106
|
-
await
|
|
106
|
+
await unsafeCreateEntityTable(stack.db, itemEntity, "arch-item");
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
afterAll(async () => {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type Registry,
|
|
12
12
|
} from "../../engine";
|
|
13
13
|
import { createEventsTable } from "../../event-store";
|
|
14
|
-
import {
|
|
14
|
+
import { createTestDb, type TestDb, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
15
15
|
import { createCascadeDeleteHook } from "../cascade-handler";
|
|
16
16
|
|
|
17
17
|
// biome-ignore lint/suspicious/noExplicitAny: Drizzle dynamic tables
|
|
@@ -81,14 +81,14 @@ beforeAll(async () => {
|
|
|
81
81
|
await createEventsTable(testDb.db);
|
|
82
82
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
83
83
|
|
|
84
|
-
await
|
|
85
|
-
await
|
|
86
|
-
await
|
|
87
|
-
await
|
|
88
|
-
await
|
|
89
|
-
await
|
|
90
|
-
await
|
|
91
|
-
await
|
|
84
|
+
await unsafeCreateEntityTable(testDb.db, departmentEntity);
|
|
85
|
+
await unsafeCreateEntityTable(testDb.db, userEntity);
|
|
86
|
+
await unsafeCreateEntityTable(testDb.db, sessionEntity);
|
|
87
|
+
await unsafeCreateEntityTable(testDb.db, groupEntity);
|
|
88
|
+
await unsafeCreateEntityTable(testDb.db, userGroupRestrictEntity);
|
|
89
|
+
await unsafeCreateEntityTable(testDb.db, userGroupCascadeEntity);
|
|
90
|
+
await unsafeCreateEntityTable(testDb.db, teamEntity);
|
|
91
|
+
await unsafeCreateEntityTable(testDb.db, memberEntity);
|
|
92
92
|
|
|
93
93
|
departmentTable = buildDrizzleTable("department", departmentEntity);
|
|
94
94
|
userTable = buildDrizzleTable("user", userEntity);
|
|
@@ -23,11 +23,11 @@ import { buildDrizzleTable } from "../../db/table-builder";
|
|
|
23
23
|
import { createEntity, createTextField, defineFeature } from "../../engine";
|
|
24
24
|
import { eventsTable } from "../../event-store";
|
|
25
25
|
import {
|
|
26
|
-
createEntityTable,
|
|
27
26
|
resetEventStore,
|
|
28
27
|
setupTestStack,
|
|
29
28
|
type TestStack,
|
|
30
29
|
TestUsers,
|
|
30
|
+
unsafeCreateEntityTable,
|
|
31
31
|
} from "../../stack";
|
|
32
32
|
|
|
33
33
|
// --- Feature ---
|
|
@@ -104,7 +104,7 @@ beforeAll(async () => {
|
|
|
104
104
|
features: [causationFeature],
|
|
105
105
|
systemHooks: [],
|
|
106
106
|
});
|
|
107
|
-
await
|
|
107
|
+
await unsafeCreateEntityTable(stack.db, orderEntity, "causation-order");
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
afterAll(async () => {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
defineFeature,
|
|
12
12
|
} from "../../engine";
|
|
13
13
|
import { UnprocessableError, writeFailure } from "../../errors";
|
|
14
|
-
import {
|
|
14
|
+
import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
15
15
|
|
|
16
16
|
// Two entities: `bag` (outer) + `secret` (inner). The outer handler calls
|
|
17
17
|
// the inner via ctx.queryAs / ctx.writeAs. We verify:
|
|
@@ -144,8 +144,8 @@ const bridgeFeature = defineFeature("ctxbridge", (r) => {
|
|
|
144
144
|
|
|
145
145
|
beforeAll(async () => {
|
|
146
146
|
stack = await setupTestStack({ features: [bridgeFeature] });
|
|
147
|
-
await
|
|
148
|
-
await
|
|
147
|
+
await unsafeCreateEntityTable(stack.db, bagEntity);
|
|
148
|
+
await unsafeCreateEntityTable(stack.db, secretEntity);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
afterAll(async () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
4
|
+
import type { TenantId } from "../../engine/types/identifiers";
|
|
4
5
|
import { createTestUser } from "../../stack";
|
|
5
6
|
import { createDispatcher } from "../dispatcher";
|
|
6
7
|
|
|
@@ -321,6 +322,78 @@ describe("dispatcher feature-gate", () => {
|
|
|
321
322
|
items: [],
|
|
322
323
|
});
|
|
323
324
|
});
|
|
325
|
+
|
|
326
|
+
test("Sprint 8a: per-tenant gating — Tenant A passes, Tenant B gets feature_disabled", async () => {
|
|
327
|
+
// Beweist die Phase-1-Architektur: dispatcher ruft effectiveFeatures
|
|
328
|
+
// mit user.tenantId, resolver kann pro Tenant unterschiedliche Sets
|
|
329
|
+
// returnen → Tier-A sieht feature, Tier-B nicht.
|
|
330
|
+
const registry = createRegistry([toggled()]);
|
|
331
|
+
const tenantA = "00000000-0000-4000-8000-0000000000a1" as TenantId;
|
|
332
|
+
const tenantB = "00000000-0000-4000-8000-0000000000b2" as TenantId;
|
|
333
|
+
|
|
334
|
+
const dispatcher = createDispatcher(
|
|
335
|
+
registry,
|
|
336
|
+
{},
|
|
337
|
+
{
|
|
338
|
+
effectiveFeatures: (tenantId) => (tenantId === tenantA ? new Set(["toggled"]) : new Set()),
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const userA = createTestUser({ id: "u-a", tenantId: tenantA, roles: ["Admin"] });
|
|
343
|
+
const userB = createTestUser({ id: "u-b", tenantId: tenantB, roles: ["Admin"] });
|
|
344
|
+
|
|
345
|
+
await expect(dispatcher.query("toggled:query:widget:list", {}, userA)).resolves.toEqual({
|
|
346
|
+
items: [],
|
|
347
|
+
});
|
|
348
|
+
await expect(dispatcher.query("toggled:query:widget:list", {}, userB)).rejects.toThrow(
|
|
349
|
+
/feature toggled is disabled/,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const writeA = await dispatcher.write("toggled:write:widget:create", { name: "from-a" }, userA);
|
|
353
|
+
expect(writeA.isSuccess).toBe(true);
|
|
354
|
+
|
|
355
|
+
const writeB = await dispatcher.write("toggled:write:widget:create", { name: "from-b" }, userB);
|
|
356
|
+
expect(writeB.isSuccess).toBe(false);
|
|
357
|
+
if (!writeB.isSuccess) {
|
|
358
|
+
expect(writeB.error.code).toBe("feature_disabled");
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("Sprint 8a: ctx.hasFeature is current-user-scoped", async () => {
|
|
363
|
+
// Pin: hasFeature() in handler-bodies resolves against ctx.user.tenantId,
|
|
364
|
+
// NICHT gegen einen globalen Set. Two tenants call same handler,
|
|
365
|
+
// beide rufen hasFeature("toggled") — A bekommt true, B false.
|
|
366
|
+
const tenantA = "00000000-0000-4000-8000-0000000000a3" as TenantId;
|
|
367
|
+
const tenantB = "00000000-0000-4000-8000-0000000000b4" as TenantId;
|
|
368
|
+
|
|
369
|
+
const probe = defineFeature("probe", (r) => {
|
|
370
|
+
r.queryHandler(
|
|
371
|
+
"check",
|
|
372
|
+
z.object({}).passthrough(),
|
|
373
|
+
async (_event, ctx) => ({ enabled: ctx.hasFeature("toggled") }),
|
|
374
|
+
{ access: { openToAll: true } },
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
const registry = createRegistry([toggled(), probe]);
|
|
378
|
+
const dispatcher = createDispatcher(
|
|
379
|
+
registry,
|
|
380
|
+
{},
|
|
381
|
+
{
|
|
382
|
+
effectiveFeatures: (tenantId) =>
|
|
383
|
+
tenantId === tenantA ? new Set(["toggled", "probe"]) : new Set(["probe"]),
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const userA = createTestUser({ id: "u-a2", tenantId: tenantA, roles: ["Admin"] });
|
|
388
|
+
const userB = createTestUser({ id: "u-b2", tenantId: tenantB, roles: ["Admin"] });
|
|
389
|
+
|
|
390
|
+
await expect(dispatcher.query("probe:query:check", {}, userA)).resolves.toEqual({
|
|
391
|
+
enabled: true,
|
|
392
|
+
});
|
|
393
|
+
await expect(dispatcher.query("probe:query:check", {}, userB)).resolves.toEqual({
|
|
394
|
+
enabled: false,
|
|
395
|
+
});
|
|
396
|
+
});
|
|
324
397
|
});
|
|
325
398
|
|
|
326
399
|
describe("write-handler shape guard", () => {
|
|
@@ -24,11 +24,11 @@ import { buildDrizzleTable } from "../../db/table-builder";
|
|
|
24
24
|
import { createEntity, createTextField, defineFeature } from "../../engine";
|
|
25
25
|
import { loadAggregate } from "../../event-store";
|
|
26
26
|
import {
|
|
27
|
-
createEntityTable,
|
|
28
27
|
resetEventStore,
|
|
29
28
|
setupTestStack,
|
|
30
29
|
type TestStack,
|
|
31
30
|
TestUsers,
|
|
31
|
+
unsafeCreateEntityTable,
|
|
32
32
|
} from "../../stack";
|
|
33
33
|
|
|
34
34
|
// --- Entity ---
|
|
@@ -175,7 +175,7 @@ beforeAll(async () => {
|
|
|
175
175
|
features: [shippingFeature],
|
|
176
176
|
systemHooks: [],
|
|
177
177
|
});
|
|
178
|
-
await
|
|
178
|
+
await unsafeCreateEntityTable(stack.db, shipmentEntity, "domain-shipment");
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
afterAll(async () => {
|
|
@@ -3,12 +3,12 @@ import { z } from "zod";
|
|
|
3
3
|
import { createEventStoreExecutor } from "../../db/event-store-executor";
|
|
4
4
|
import { defineFeature, type SaveContext } from "../../engine";
|
|
5
5
|
import {
|
|
6
|
-
createEntityTable,
|
|
7
6
|
createTestRedis,
|
|
8
7
|
setupTestStack,
|
|
9
8
|
type TestRedis,
|
|
10
9
|
type TestStack,
|
|
11
10
|
TestUsers,
|
|
11
|
+
unsafeCreateEntityTable,
|
|
12
12
|
} from "../../stack";
|
|
13
13
|
import { sharedItemEntity, sharedItemTable } from "../../testing";
|
|
14
14
|
import { createEventDedup } from "../event-dedup";
|
|
@@ -71,7 +71,7 @@ beforeAll(async () => {
|
|
|
71
71
|
systemHooks: [],
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
await
|
|
74
|
+
await unsafeCreateEntityTable(stack.db, sharedItemEntity, "item");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
afterAll(async () => {
|
|
@@ -17,11 +17,11 @@ import { z } from "zod";
|
|
|
17
17
|
import { defineFeature } from "../../engine";
|
|
18
18
|
import { eventsTable } from "../../event-store";
|
|
19
19
|
import {
|
|
20
|
-
createEntityTable,
|
|
21
20
|
resetEventStore,
|
|
22
21
|
setupTestStack,
|
|
23
22
|
type TestStack,
|
|
24
23
|
TestUsers,
|
|
24
|
+
unsafeCreateEntityTable,
|
|
25
25
|
} from "../../stack";
|
|
26
26
|
import { sharedWidgetEntity } from "../../testing";
|
|
27
27
|
import { generateId } from "../../utils";
|
|
@@ -120,7 +120,7 @@ beforeAll(async () => {
|
|
|
120
120
|
features: [emitterFeature, neighborFeature],
|
|
121
121
|
systemHooks: [],
|
|
122
122
|
});
|
|
123
|
-
await
|
|
123
|
+
await unsafeCreateEntityTable(stack.db, sharedWidgetEntity, "widget");
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
afterEach(async () => {
|
|
@@ -25,11 +25,11 @@ import {
|
|
|
25
25
|
RecordingTracer,
|
|
26
26
|
} from "../../observability";
|
|
27
27
|
import {
|
|
28
|
-
createEntityTable,
|
|
29
28
|
resetEventStore,
|
|
30
29
|
setupTestStack,
|
|
31
30
|
type TestStack,
|
|
32
31
|
TestUsers,
|
|
32
|
+
unsafeCreateEntityTable,
|
|
33
33
|
} from "../../stack";
|
|
34
34
|
import { sharedWidgetEntity, sharedWidgetTable } from "../../testing";
|
|
35
35
|
|
|
@@ -85,7 +85,7 @@ beforeAll(async () => {
|
|
|
85
85
|
features: [wiringFeature],
|
|
86
86
|
systemHooks: [],
|
|
87
87
|
});
|
|
88
|
-
await
|
|
88
|
+
await unsafeCreateEntityTable(stack.db, sharedWidgetEntity, "widget");
|
|
89
89
|
tdb = createTenantDb(stack.db, admin.tenantId);
|
|
90
90
|
});
|
|
91
91
|
|
|
@@ -181,7 +181,7 @@ describe("E.1 — consumer-lag metric", () => {
|
|
|
181
181
|
observability: recordingProvider,
|
|
182
182
|
});
|
|
183
183
|
try {
|
|
184
|
-
await
|
|
184
|
+
await unsafeCreateEntityTable(recStack.db, sharedWidgetEntity, "widget");
|
|
185
185
|
const recTdb = createTenantDb(recStack.db, admin.tenantId);
|
|
186
186
|
await executor.create({ name: "lag-check" }, admin, recTdb);
|
|
187
187
|
|
|
@@ -30,11 +30,11 @@ import {
|
|
|
30
30
|
getConsumerState,
|
|
31
31
|
} from "../../pipeline";
|
|
32
32
|
import {
|
|
33
|
-
createEntityTable,
|
|
34
33
|
resetEventStore,
|
|
35
34
|
setupTestStack,
|
|
36
35
|
type TestStack,
|
|
37
36
|
TestUsers,
|
|
37
|
+
unsafeCreateEntityTable,
|
|
38
38
|
} from "../../stack";
|
|
39
39
|
import { sharedWidgetEntity, sharedWidgetTable } from "../../testing";
|
|
40
40
|
import { generateId } from "../../utils";
|
|
@@ -63,7 +63,7 @@ beforeAll(async () => {
|
|
|
63
63
|
features: [multiFeature],
|
|
64
64
|
systemHooks: [],
|
|
65
65
|
});
|
|
66
|
-
await
|
|
66
|
+
await unsafeCreateEntityTable(stack.db, sharedWidgetEntity, "widget");
|
|
67
67
|
tdb = createTenantDb(stack.db, admin.tenantId);
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -15,7 +15,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
|
15
15
|
import { createEventStoreExecutor } from "../../db/event-store-executor";
|
|
16
16
|
import { createTenantDb, type TenantDb } from "../../db/tenant-db";
|
|
17
17
|
import { defineFeature } from "../../engine";
|
|
18
|
-
import {
|
|
18
|
+
import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
19
19
|
import { sharedWidgetEntity, sharedWidgetTable } from "../../testing";
|
|
20
20
|
|
|
21
21
|
// --- Fixture ---
|
|
@@ -48,7 +48,7 @@ beforeAll(async () => {
|
|
|
48
48
|
features: [listenFeature],
|
|
49
49
|
systemHooks: [],
|
|
50
50
|
});
|
|
51
|
-
await
|
|
51
|
+
await unsafeCreateEntityTable(stack.db, sharedWidgetEntity, "widget");
|
|
52
52
|
tdb = createTenantDb(stack.db, admin.tenantId);
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -26,11 +26,11 @@ import {
|
|
|
26
26
|
skipPoisonEvent,
|
|
27
27
|
} from "../../pipeline";
|
|
28
28
|
import {
|
|
29
|
-
createEntityTable,
|
|
30
29
|
resetEventStore,
|
|
31
30
|
setupTestStack,
|
|
32
31
|
type TestStack,
|
|
33
32
|
TestUsers,
|
|
33
|
+
unsafeCreateEntityTable,
|
|
34
34
|
} from "../../stack";
|
|
35
35
|
import { sharedWidgetEntity, sharedWidgetTable } from "../../testing";
|
|
36
36
|
|
|
@@ -71,7 +71,7 @@ beforeAll(async () => {
|
|
|
71
71
|
features: [recoveryFeature],
|
|
72
72
|
systemHooks: [],
|
|
73
73
|
});
|
|
74
|
-
await
|
|
74
|
+
await unsafeCreateEntityTable(stack.db, sharedWidgetEntity, "widget");
|
|
75
75
|
tdb = createTenantDb(stack.db, admin.tenantId);
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -27,11 +27,11 @@ import {
|
|
|
27
27
|
} from "../../observability";
|
|
28
28
|
import { ConsumerLagError, eventConsumerStateTable, pruneEvents } from "../../pipeline";
|
|
29
29
|
import {
|
|
30
|
-
createEntityTable,
|
|
31
30
|
resetEventStore,
|
|
32
31
|
setupTestStack,
|
|
33
32
|
type TestStack,
|
|
34
33
|
TestUsers,
|
|
34
|
+
unsafeCreateEntityTable,
|
|
35
35
|
} from "../../stack";
|
|
36
36
|
import { sharedWidgetEntity } from "../../testing";
|
|
37
37
|
import { generateId } from "../../utils";
|
|
@@ -62,7 +62,7 @@ beforeAll(async () => {
|
|
|
62
62
|
features: [auditFeature],
|
|
63
63
|
systemHooks: [],
|
|
64
64
|
});
|
|
65
|
-
await
|
|
65
|
+
await unsafeCreateEntityTable(stack.db, sharedWidgetEntity, "widget");
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
afterEach(async () => {
|
|
@@ -179,7 +179,7 @@ describe("Second audit — LISTEN gauge", () => {
|
|
|
179
179
|
observability: recordingProvider,
|
|
180
180
|
});
|
|
181
181
|
try {
|
|
182
|
-
await
|
|
182
|
+
await unsafeCreateEntityTable(recStack.db, sharedWidgetEntity, "widget");
|
|
183
183
|
|
|
184
184
|
await recStack.eventDispatcher?.start();
|
|
185
185
|
try {
|
|
@@ -233,7 +233,7 @@ describe("Second audit — LISTEN gauge", () => {
|
|
|
233
233
|
observability: recordingProvider,
|
|
234
234
|
});
|
|
235
235
|
try {
|
|
236
|
-
await
|
|
236
|
+
await unsafeCreateEntityTable(recStack.db, sharedWidgetEntity, "widget");
|
|
237
237
|
|
|
238
238
|
await recStack.eventDispatcher?.start();
|
|
239
239
|
try {
|
|
@@ -26,12 +26,12 @@ import { defineFeature, type FeatureDefinition } from "../../engine";
|
|
|
26
26
|
import type { StoredEvent } from "../../event-store";
|
|
27
27
|
import { eventConsumerStateTable, getAllConsumerProgress, getConsumerState } from "../../pipeline";
|
|
28
28
|
import {
|
|
29
|
-
createEntityTable,
|
|
30
|
-
pushTables,
|
|
31
29
|
resetEventStore,
|
|
32
30
|
setupTestStack,
|
|
33
31
|
type TestStack,
|
|
34
32
|
TestUsers,
|
|
33
|
+
unsafeCreateEntityTable,
|
|
34
|
+
unsafePushTables,
|
|
35
35
|
} from "../../stack";
|
|
36
36
|
import { sharedWidgetEntity, sharedWidgetTable } from "../../testing";
|
|
37
37
|
|
|
@@ -98,8 +98,8 @@ beforeAll(async () => {
|
|
|
98
98
|
// hook chain. SSE / search are irrelevant to cursor behaviour.
|
|
99
99
|
systemHooks: [],
|
|
100
100
|
});
|
|
101
|
-
await
|
|
102
|
-
await
|
|
101
|
+
await unsafeCreateEntityTable(stack.db, sharedWidgetEntity, "widget");
|
|
102
|
+
await unsafePushTables(stack.db, { subscriberLog: subscriberLogTable });
|
|
103
103
|
tdb = createTenantDb(stack.db, admin.tenantId);
|
|
104
104
|
});
|
|
105
105
|
|
|
@@ -23,11 +23,11 @@ import {
|
|
|
23
23
|
pruneEvents,
|
|
24
24
|
} from "../../pipeline";
|
|
25
25
|
import {
|
|
26
|
-
createEntityTable,
|
|
27
26
|
resetEventStore,
|
|
28
27
|
setupTestStack,
|
|
29
28
|
type TestStack,
|
|
30
29
|
TestUsers,
|
|
30
|
+
unsafeCreateEntityTable,
|
|
31
31
|
} from "../../stack";
|
|
32
32
|
import { sharedWidgetEntity, sharedWidgetTable } from "../../testing";
|
|
33
33
|
import { generateId } from "../../utils";
|
|
@@ -61,7 +61,7 @@ beforeAll(async () => {
|
|
|
61
61
|
features: [retentionFeature],
|
|
62
62
|
systemHooks: [],
|
|
63
63
|
});
|
|
64
|
-
await
|
|
64
|
+
await unsafeCreateEntityTable(stack.db, sharedWidgetEntity, "widget");
|
|
65
65
|
tdb = createTenantDb(stack.db, admin.tenantId);
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -15,7 +15,7 @@ import { buildDrizzleTable } from "../../db/table-builder";
|
|
|
15
15
|
import { createEntity, createTextField, defineFeature } from "../../engine";
|
|
16
16
|
import { UnprocessableError, writeFailure } from "../../errors";
|
|
17
17
|
import { loadAggregate } from "../../event-store";
|
|
18
|
-
import {
|
|
18
|
+
import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
19
19
|
|
|
20
20
|
// --- Feature ---
|
|
21
21
|
|
|
@@ -136,7 +136,7 @@ const admin = TestUsers.admin;
|
|
|
136
136
|
|
|
137
137
|
beforeAll(async () => {
|
|
138
138
|
stack = await setupTestStack({ features: [cartFeature], systemHooks: [] });
|
|
139
|
-
await
|
|
139
|
+
await unsafeCreateEntityTable(stack.db, cartEntity, "f4wCart");
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
afterAll(async () => {
|