@cosmicdrift/kumiko-framework 0.292.0 → 0.294.0
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/package.json +4 -4
- package/src/__tests__/schema-cli-rebuild.integration.test.ts +0 -2
- package/src/changes.json +12 -0
- package/src/crypto/__tests__/kek-source.test.ts +143 -0
- package/src/crypto/kek-source.ts +34 -43
- package/src/db/__tests__/apply-entity-event-tenant.integration.test.ts +0 -2
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +1 -2
- package/src/db/__tests__/blind-index.integration.test.ts +0 -2
- package/src/db/__tests__/entity-field-encryption.test.ts +29 -0
- package/src/db/__tests__/event-store-executor-list.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor-money-rehydrate.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor-reference-search.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor-reference-sort.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor.integration.test.ts +1 -2
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +0 -2
- package/src/db/__tests__/unique-violation-mapping.integration.test.ts +0 -2
- package/src/db/entity-field-encryption.ts +5 -3
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +0 -2
- package/src/engine/boot-validator/index.ts +5 -1
- package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +1 -2
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +1 -2
- package/src/entrypoint/__tests__/split-deploy.integration.test.ts +1 -2
- package/src/env/__tests__/compose-env-schema.test.ts +97 -1
- package/src/env/index.ts +63 -3
- package/src/es-ops/__tests__/context.integration.test.ts +0 -2
- package/src/es-ops/__tests__/runner.integration.test.ts +0 -2
- package/src/event-store/__tests__/admin-api.integration.test.ts +1 -2
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +0 -2
- package/src/event-store/__tests__/event-store.integration.test.ts +0 -2
- package/src/event-store/__tests__/get-stream-version-perf.integration.test.ts +1 -2
- package/src/event-store/__tests__/perf.integration.test.ts +1 -8
- package/src/event-store/__tests__/provenance-append.integration.test.ts +1 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +0 -2
- package/src/event-store/__tests__/upcaster-dead-letter.integration.test.ts +1 -2
- package/src/event-store/__tests__/upcaster.integration.test.ts +1 -2
- package/src/files/__tests__/files.integration.test.ts +1 -2
- package/src/jobs/__tests__/job-write-as.integration.test.ts +1 -2
- package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +0 -2
- package/src/pipeline/__tests__/blind-index-rebuild-guard.integration.test.ts +0 -2
- package/src/pipeline/__tests__/cascade-handler.integration.test.ts +0 -2
- package/src/pipeline/__tests__/perf-rebuild.integration.test.ts +0 -2
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +1 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +1 -2
- package/src/search/__tests__/reindex-entity.integration.test.ts +0 -2
- package/src/search/__tests__/search-pii-derived-index.integration.test.ts +0 -2
|
@@ -14,7 +14,7 @@ import { z } from "zod";
|
|
|
14
14
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
15
15
|
import { asRawClient } from "../../db/query";
|
|
16
16
|
import { createRegistry, defineFeature } from "../../engine";
|
|
17
|
-
import { createArchivedStreamsTable
|
|
17
|
+
import { createArchivedStreamsTable } from "../../event-store";
|
|
18
18
|
import { createJobRunner } from "../../jobs/job-runner";
|
|
19
19
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
20
20
|
import { createTestRedis, type TestRedis } from "../../stack";
|
|
@@ -63,7 +63,6 @@ let testRedis: TestRedis;
|
|
|
63
63
|
|
|
64
64
|
beforeAll(async () => {
|
|
65
65
|
[testDb, testRedis] = await Promise.all([createTestDb(), createTestRedis()]);
|
|
66
|
-
await createEventsTable(testDb.db);
|
|
67
66
|
await createArchivedStreamsTable(testDb.db);
|
|
68
67
|
await createEventConsumerStateTable(testDb.db);
|
|
69
68
|
});
|
|
@@ -16,7 +16,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
18
|
import { createRegistry, defineFeature } from "../../engine";
|
|
19
|
-
import { createArchivedStreamsTable
|
|
19
|
+
import { createArchivedStreamsTable } from "../../event-store";
|
|
20
20
|
import { createNoopProvider, createPrometheusMeter } from "../../observability";
|
|
21
21
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
22
22
|
import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
|
|
@@ -79,7 +79,6 @@ let testRedis: TestRedis;
|
|
|
79
79
|
|
|
80
80
|
beforeAll(async () => {
|
|
81
81
|
[testDb, testRedis] = await Promise.all([createTestDb(), createTestRedis()]);
|
|
82
|
-
await createEventsTable(testDb.db);
|
|
83
82
|
await createArchivedStreamsTable(testDb.db);
|
|
84
83
|
await createEventConsumerStateTable(testDb.db);
|
|
85
84
|
});
|
|
@@ -17,7 +17,7 @@ import { z } from "zod";
|
|
|
17
17
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
18
|
import { asRawClient } from "../../db/query";
|
|
19
19
|
import { createRegistry, defineFeature } from "../../engine";
|
|
20
|
-
import { createArchivedStreamsTable
|
|
20
|
+
import { createArchivedStreamsTable } from "../../event-store";
|
|
21
21
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
22
22
|
import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
|
|
23
23
|
import { createAllInOneEntrypoint, createApiEntrypoint, createWorkerEntrypoint } from "../index";
|
|
@@ -125,7 +125,6 @@ let testRedis: TestRedis;
|
|
|
125
125
|
|
|
126
126
|
beforeAll(async () => {
|
|
127
127
|
[testDb, testRedis] = await Promise.all([createTestDb(), createTestRedis()]);
|
|
128
|
-
await createEventsTable(testDb.db);
|
|
129
128
|
await createArchivedStreamsTable(testDb.db);
|
|
130
129
|
await createEventConsumerStateTable(testDb.db);
|
|
131
130
|
});
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { defineFeature } from "../../engine/define-feature";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
camelCase,
|
|
6
|
+
composeEnvSchema,
|
|
7
|
+
KumikoBootError,
|
|
8
|
+
kmsSlotsOf,
|
|
9
|
+
parseEnv,
|
|
10
|
+
pulumiConfigKey,
|
|
11
|
+
readKumikoMeta,
|
|
12
|
+
} from "../index";
|
|
5
13
|
|
|
6
14
|
describe("composeEnvSchema", () => {
|
|
7
15
|
it("merges per-feature schemas and tags sources", () => {
|
|
@@ -295,3 +303,91 @@ describe("pulumiConfigKey + camelCase", () => {
|
|
|
295
303
|
expect(pulumiConfigKey("JWT_SECRET", undefined, undefined)).toBe("jwtSecret");
|
|
296
304
|
});
|
|
297
305
|
});
|
|
306
|
+
|
|
307
|
+
describe("kms env slots", () => {
|
|
308
|
+
const schema = z.object({
|
|
309
|
+
MASTER_KEY: z
|
|
310
|
+
.string()
|
|
311
|
+
.min(1)
|
|
312
|
+
.meta({ kumiko: { kms: true } }),
|
|
313
|
+
MASTER_KEY_CIPHERTEXT: z.string().min(1).optional(),
|
|
314
|
+
PLAIN: z.string().min(1).optional(),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("kmsSlotsOf lists exactly the fields declaring kms", () => {
|
|
318
|
+
expect(kmsSlotsOf(schema)).toEqual(["MASTER_KEY"]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("rejects a non-true kms value in the meta", () => {
|
|
322
|
+
const bad = z.string().meta({ kumiko: { kms: "yes" } });
|
|
323
|
+
expect(readKumikoMeta(bad)).toEqual({});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("parseEnv accepts a required kms slot that is present only as ciphertext", () => {
|
|
327
|
+
expect(() => parseEnv(schema, { MASTER_KEY_CIPHERTEXT: "abc" })).not.toThrow();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("parseEnv still requires a kms slot when no ciphertext twin is set", () => {
|
|
331
|
+
expect(() => parseEnv(schema, {})).toThrow(KumikoBootError);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("parseEnv still validates a kms slot that is set", () => {
|
|
335
|
+
expect(() => parseEnv(schema, { MASTER_KEY: "" })).toThrow(KumikoBootError);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("composeEnvSchema kms twins", () => {
|
|
340
|
+
const kmsField = z
|
|
341
|
+
.string()
|
|
342
|
+
.min(1)
|
|
343
|
+
.meta({ kumiko: { kms: true } });
|
|
344
|
+
|
|
345
|
+
it("declares the ciphertext twin of every kms field, owned by the same source", () => {
|
|
346
|
+
const secrets = defineFeature("secrets", (r) => {
|
|
347
|
+
r.envSchema(z.object({ KEY_V1: kmsField }));
|
|
348
|
+
});
|
|
349
|
+
const { schema, sources } = composeEnvSchema({
|
|
350
|
+
features: [secrets],
|
|
351
|
+
extend: z.object({ KEY_V2: kmsField }),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(Object.keys(schema.shape).sort()).toEqual([
|
|
355
|
+
"KEY_V1",
|
|
356
|
+
"KEY_V1_CIPHERTEXT",
|
|
357
|
+
"KEY_V2",
|
|
358
|
+
"KEY_V2_CIPHERTEXT",
|
|
359
|
+
]);
|
|
360
|
+
expect(sources["KEY_V1_CIPHERTEXT"]).toBe("secrets");
|
|
361
|
+
expect(sources["KEY_V2_CIPHERTEXT"]).toBe("app");
|
|
362
|
+
expect(() => parseEnv(schema, {})).toThrow(KumikoBootError);
|
|
363
|
+
expect(() =>
|
|
364
|
+
parseEnv(schema, { KEY_V1_CIPHERTEXT: "a", KEY_V2_CIPHERTEXT: "b" }),
|
|
365
|
+
).not.toThrow();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("keeps a twin the app already declared instead of conflicting", () => {
|
|
369
|
+
const declared = z.string().min(1).optional().describe("hand-written twin");
|
|
370
|
+
const { schema } = composeEnvSchema({
|
|
371
|
+
features: [],
|
|
372
|
+
extend: z.object({ KEY: kmsField, KEY_CIPHERTEXT: declared }),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(schema.shape["KEY_CIPHERTEXT"]).toBe(declared);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("adds the twin for a kms field of an optional feature too", () => {
|
|
379
|
+
const secrets = defineFeature("secrets", (r) => {
|
|
380
|
+
r.envSchema(z.object({ KEY: kmsField }));
|
|
381
|
+
});
|
|
382
|
+
const { schema } = composeEnvSchema({ features: [secrets], optionalFeatures: ["secrets"] });
|
|
383
|
+
|
|
384
|
+
expect(Object.keys(schema.shape).sort()).toEqual(["KEY", "KEY_CIPHERTEXT"]);
|
|
385
|
+
expect(kmsSlotsOf(schema)).toEqual(["KEY"]);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("adds nothing for a schema without kms fields", () => {
|
|
389
|
+
const { schema } = composeEnvSchema({ features: [], extend: z.object({ PLAIN: z.string() }) });
|
|
390
|
+
|
|
391
|
+
expect(Object.keys(schema.shape)).toEqual(["PLAIN"]);
|
|
392
|
+
});
|
|
393
|
+
});
|
package/src/env/index.ts
CHANGED
|
@@ -34,12 +34,18 @@ export type KumikoEnvMeta = {
|
|
|
34
34
|
/** Force the `--secret` flag in `pulumi config set`. Default false. */
|
|
35
35
|
readonly secret?: boolean;
|
|
36
36
|
};
|
|
37
|
+
/** The value may be delivered as a Scaleway Key Manager ciphertext in
|
|
38
|
+
* `<NAME>_CIPHERTEXT`. `runProdApp` decrypts it at boot when `<NAME>` itself
|
|
39
|
+
* is unset; a set plaintext always wins. A flag, not an object: every slot
|
|
40
|
+
* uses the same `_CIPHERTEXT` twin and one shared key (`PLATFORM_KEK_KMS_*`). */
|
|
41
|
+
readonly kms?: true;
|
|
37
42
|
};
|
|
38
43
|
|
|
39
44
|
function isKumikoMeta(value: unknown): value is KumikoEnvMeta {
|
|
40
45
|
if (value === null || typeof value !== "object") return false;
|
|
41
46
|
// @cast-boundary schema-walk — runtime-shape narrowing of zod-meta payload
|
|
42
|
-
const v = value as { pulumi?: unknown };
|
|
47
|
+
const v = value as { pulumi?: unknown; kms?: unknown };
|
|
48
|
+
if (v.kms !== undefined && v.kms !== true) return false;
|
|
43
49
|
if (v.pulumi === undefined) return true;
|
|
44
50
|
if (v.pulumi === null || typeof v.pulumi !== "object") return false;
|
|
45
51
|
// @cast-boundary schema-walk
|
|
@@ -60,6 +66,31 @@ export function readKumikoMeta(field: z.ZodType): KumikoEnvMeta {
|
|
|
60
66
|
return {};
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
/** Env names whose schema field declares `kms: true` — the slots
|
|
70
|
+
* `resolvePlatformKeks` resolves from a ciphertext. */
|
|
71
|
+
export function kmsSlotsOf(schema: z.ZodObject<z.ZodRawShape>): readonly string[] {
|
|
72
|
+
return Object.entries(zodShape(schema))
|
|
73
|
+
.filter(([, field]) => readKumikoMeta(field).kms === true)
|
|
74
|
+
.map(([name]) => name);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A ciphertext-only slot is satisfied by its `_CIPHERTEXT` twin: the plaintext
|
|
78
|
+
// only exists after the boot-time decrypt, so requiring it here would reject
|
|
79
|
+
// exactly the deployment this meta enables.
|
|
80
|
+
function relaxCiphertextOnlySlots<S extends z.ZodObject<z.ZodRawShape>>(
|
|
81
|
+
schema: S,
|
|
82
|
+
env: Readonly<Record<string, string>>,
|
|
83
|
+
): z.ZodObject<z.ZodRawShape> {
|
|
84
|
+
const shape = zodShape(schema);
|
|
85
|
+
const relaxed: Record<string, z.ZodType> = {};
|
|
86
|
+
for (const name of kmsSlotsOf(schema)) {
|
|
87
|
+
const field = shape[name];
|
|
88
|
+
if (field && env[name] === undefined && env[`${name}_CIPHERTEXT`])
|
|
89
|
+
relaxed[name] = field.optional();
|
|
90
|
+
}
|
|
91
|
+
return Object.keys(relaxed).length === 0 ? schema : schema.extend(relaxed);
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
// --- Field-classification helpers (Zod v4 introspection) ---
|
|
64
95
|
|
|
65
96
|
export type EnvFieldClass = "required" | "optional" | "withDefault";
|
|
@@ -144,10 +175,23 @@ export type ComposedEnvSchema = {
|
|
|
144
175
|
readonly sources: Readonly<Record<string, string>>;
|
|
145
176
|
};
|
|
146
177
|
|
|
178
|
+
// `.optional()` returns a new Zod instance that drops `.meta()`, which would
|
|
179
|
+
// hide the slot from `kmsSlotsOf` on the composed schema.
|
|
180
|
+
function optionalKeepingKmsMeta(field: z.ZodType): z.ZodType {
|
|
181
|
+
const optional = field.optional();
|
|
182
|
+
return readKumikoMeta(field).kms === true
|
|
183
|
+
? optional.meta({ kumiko: readKumikoMeta(field) })
|
|
184
|
+
: optional;
|
|
185
|
+
}
|
|
186
|
+
|
|
147
187
|
export function composeEnvSchema(options: ComposeEnvSchemaOptions): ComposedEnvSchema {
|
|
148
188
|
const optionalSet = new Set(options.optionalFeatures ?? []);
|
|
149
189
|
const merged: Record<string, z.ZodType> = {};
|
|
150
190
|
const sources: Record<string, string> = {};
|
|
191
|
+
const kmsFields: { readonly name: string; readonly source: string }[] = [];
|
|
192
|
+
const noteKms = (name: string, field: z.ZodType, source: string): void => {
|
|
193
|
+
if (readKumikoMeta(field).kms === true) kmsFields.push({ name, source });
|
|
194
|
+
};
|
|
151
195
|
|
|
152
196
|
// Framework-core first so a feature that accidentally declares the same
|
|
153
197
|
// var (e.g. PORT) gets a clear conflict error citing "framework-core"
|
|
@@ -159,6 +203,7 @@ export function composeEnvSchema(options: ComposeEnvSchemaOptions): ComposedEnvS
|
|
|
159
203
|
for (const [key, field] of Object.entries(zodShape(options.core))) {
|
|
160
204
|
merged[key] = field;
|
|
161
205
|
sources[key] = "framework-core";
|
|
206
|
+
noteKms(key, field, "framework-core");
|
|
162
207
|
}
|
|
163
208
|
}
|
|
164
209
|
|
|
@@ -178,8 +223,9 @@ export function composeEnvSchema(options: ComposeEnvSchemaOptions): ComposedEnvS
|
|
|
178
223
|
},
|
|
179
224
|
]);
|
|
180
225
|
}
|
|
181
|
-
merged[key] = wrap ? field
|
|
226
|
+
merged[key] = wrap ? optionalKeepingKmsMeta(field) : field;
|
|
182
227
|
sources[key] = feature.name;
|
|
228
|
+
noteKms(key, field, feature.name);
|
|
183
229
|
}
|
|
184
230
|
}
|
|
185
231
|
|
|
@@ -198,9 +244,23 @@ export function composeEnvSchema(options: ComposeEnvSchemaOptions): ComposedEnvS
|
|
|
198
244
|
}
|
|
199
245
|
merged[key] = field;
|
|
200
246
|
sources[key] = "app";
|
|
247
|
+
noteKms(key, field, "app");
|
|
201
248
|
}
|
|
202
249
|
}
|
|
203
250
|
|
|
251
|
+
// The twin follows from the slot, so a versioned family (`…_V2`) needs no
|
|
252
|
+
// second declaration. An already declared twin (apps that predate this) stays.
|
|
253
|
+
for (const { name, source } of kmsFields) {
|
|
254
|
+
const twin = `${name}_CIPHERTEXT`;
|
|
255
|
+
if (merged[twin] !== undefined) continue;
|
|
256
|
+
merged[twin] = z
|
|
257
|
+
.string()
|
|
258
|
+
.min(1)
|
|
259
|
+
.optional()
|
|
260
|
+
.describe(`Key-Manager ciphertext of ${name}; used when ${name} is unset.`);
|
|
261
|
+
sources[twin] = source;
|
|
262
|
+
}
|
|
263
|
+
|
|
204
264
|
return {
|
|
205
265
|
schema: z.object(merged),
|
|
206
266
|
sources,
|
|
@@ -282,7 +342,7 @@ export function parseEnv<S extends z.ZodObject<z.ZodRawShape>>(
|
|
|
282
342
|
if (v !== undefined) cleaned[k] = v;
|
|
283
343
|
}
|
|
284
344
|
|
|
285
|
-
const result = schema.safeParse(cleaned);
|
|
345
|
+
const result = relaxCiphertextOnlySlots(schema, cleaned).safeParse(cleaned);
|
|
286
346
|
if (result.success) {
|
|
287
347
|
// @cast-boundary schema-walk — z.infer<S> erasure across safeParse result
|
|
288
348
|
return result.data as z.infer<S>;
|
|
@@ -21,7 +21,6 @@ import { join } from "node:path";
|
|
|
21
21
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
22
22
|
import { asRawClient, selectMany } from "../../db/query";
|
|
23
23
|
import { createRegistry } from "../../engine";
|
|
24
|
-
import { createEventsTable } from "../../event-store";
|
|
25
24
|
import { createDispatcher, type Dispatcher } from "../../pipeline";
|
|
26
25
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
27
26
|
import { createSeedMigrationContext } from "../context";
|
|
@@ -38,7 +37,6 @@ let dispatcher: Dispatcher;
|
|
|
38
37
|
beforeAll(async () => {
|
|
39
38
|
await ensureTemporalPolyfill();
|
|
40
39
|
testDb = await createTestDb();
|
|
41
|
-
await createEventsTable(testDb.db);
|
|
42
40
|
await createEsOperationsTable(testDb.db);
|
|
43
41
|
|
|
44
42
|
const registry = createRegistry([]);
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
SYSTEM_TENANT_ID,
|
|
30
30
|
} from "../../engine";
|
|
31
31
|
import { VersionConflictError } from "../../errors";
|
|
32
|
-
import { createEventsTable } from "../../event-store";
|
|
33
32
|
import { createDispatcher, type Dispatcher } from "../../pipeline";
|
|
34
33
|
import { createTestDb, type TestDb, unsafeCreateEntityTable } from "../../stack";
|
|
35
34
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
@@ -89,7 +88,6 @@ let registry: ReturnType<typeof createRegistry>;
|
|
|
89
88
|
beforeAll(async () => {
|
|
90
89
|
await ensureTemporalPolyfill();
|
|
91
90
|
testDb = await createTestDb();
|
|
92
|
-
await createEventsTable(testDb.db);
|
|
93
91
|
await createEsOperationsTable(testDb.db);
|
|
94
92
|
await unsafeCreateEntityTable(testDb.db, probeEntity, "esops-probe");
|
|
95
93
|
registry = createRegistry([seedTestFeature]);
|
|
@@ -17,7 +17,7 @@ import { generateId as uuid } from "../../utils";
|
|
|
17
17
|
import { appendRaw, appendRawBatch, type RawEventToAppend } from "../admin-api";
|
|
18
18
|
import { IdempotentAppendConflictError, VersionConflictError } from "../errors";
|
|
19
19
|
import { append, loadAggregate } from "../event-store";
|
|
20
|
-
import {
|
|
20
|
+
import { eventsTable } from "../events-schema";
|
|
21
21
|
|
|
22
22
|
// Test-only spy: wrap a DbConnection's `.unsafe()` to capture the SQL
|
|
23
23
|
// string of every query the framework runs. Used to assert batching
|
|
@@ -47,7 +47,6 @@ const legacyUser = "legacy-user-42";
|
|
|
47
47
|
|
|
48
48
|
beforeAll(async () => {
|
|
49
49
|
testDb = await createTestDb();
|
|
50
|
-
await createEventsTable(testDb.db);
|
|
51
50
|
});
|
|
52
51
|
|
|
53
52
|
afterAll(async () => {
|
|
@@ -34,7 +34,6 @@ import type { Registry, TenantId } from "../../engine/types";
|
|
|
34
34
|
import { createTestDb, type TestDb, unsafeCreateEntityTable } from "../../stack";
|
|
35
35
|
import { generateId } from "../../utils";
|
|
36
36
|
import { append, loadAggregate } from "../event-store";
|
|
37
|
-
import { createEventsTable } from "../events-schema";
|
|
38
37
|
|
|
39
38
|
const TENANT = "00000000-0000-4000-8000-000000000001" as TenantId;
|
|
40
39
|
const BIDX_KEY = Buffer.alloc(32, 5).toString("base64");
|
|
@@ -145,7 +144,6 @@ beforeAll(async () => {
|
|
|
145
144
|
surveyFeature,
|
|
146
145
|
signalsFeature,
|
|
147
146
|
]);
|
|
148
|
-
await createEventsTable(testDb.db);
|
|
149
147
|
await unsafeCreateEntityTable(testDb.db, contactEntity, "contact");
|
|
150
148
|
await unsafeCreateEntityTable(testDb.db, noteEntity, "note");
|
|
151
149
|
await unsafeCreateEntityTable(testDb.db, surveyEntity, "survey");
|
|
@@ -5,7 +5,6 @@ import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
|
5
5
|
import { generateId as uuid } from "../../utils";
|
|
6
6
|
import {
|
|
7
7
|
append,
|
|
8
|
-
createEventsTable,
|
|
9
8
|
IdempotentAppendConflictError,
|
|
10
9
|
loadAggregate,
|
|
11
10
|
loadAggregateAsOf,
|
|
@@ -25,7 +24,6 @@ const userA = uuid();
|
|
|
25
24
|
beforeAll(async () => {
|
|
26
25
|
await ensureTemporalPolyfill();
|
|
27
26
|
testDb = await createTestDb();
|
|
28
|
-
await createEventsTable(testDb.db);
|
|
29
27
|
});
|
|
30
28
|
|
|
31
29
|
afterAll(async () => {
|
|
@@ -15,7 +15,7 @@ import { insertMany } from "../../bun-db/query";
|
|
|
15
15
|
import type { TenantId } from "../../engine/types";
|
|
16
16
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
17
17
|
import { generateId as uuid } from "../../utils";
|
|
18
|
-
import {
|
|
18
|
+
import { eventsTable, getStreamVersion } from "../index";
|
|
19
19
|
|
|
20
20
|
let testDb: BunTestDb;
|
|
21
21
|
const tenantId: TenantId = uuid();
|
|
@@ -24,7 +24,6 @@ const userId = uuid();
|
|
|
24
24
|
beforeAll(async () => {
|
|
25
25
|
await ensureTemporalPolyfill();
|
|
26
26
|
testDb = await createTestDb();
|
|
27
|
-
await createEventsTable(testDb.db);
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
afterAll(async () => {
|
|
@@ -24,13 +24,7 @@ import { asRawClient } from "../../db/query";
|
|
|
24
24
|
import type { TenantId } from "../../engine/types";
|
|
25
25
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
26
26
|
import { generateId as uuid } from "../../utils";
|
|
27
|
-
import {
|
|
28
|
-
append,
|
|
29
|
-
createEventsTable,
|
|
30
|
-
loadAggregate,
|
|
31
|
-
loadAggregateWithSnapshot,
|
|
32
|
-
saveSnapshot,
|
|
33
|
-
} from "../index";
|
|
27
|
+
import { append, loadAggregate, loadAggregateWithSnapshot, saveSnapshot } from "../index";
|
|
34
28
|
|
|
35
29
|
let testDb: BunTestDb;
|
|
36
30
|
const tenantId = uuid() as TenantId;
|
|
@@ -39,7 +33,6 @@ const userId = uuid();
|
|
|
39
33
|
beforeAll(async () => {
|
|
40
34
|
await ensureTemporalPolyfill();
|
|
41
35
|
testDb = await createTestDb();
|
|
42
|
-
await createEventsTable(testDb.db);
|
|
43
36
|
});
|
|
44
37
|
|
|
45
38
|
afterAll(async () => {
|
|
@@ -5,7 +5,7 @@ import { createTenantDb, type TenantDb } from "../../db/tenant-db";
|
|
|
5
5
|
import { AccessDeniedError, InternalError } from "../../errors";
|
|
6
6
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
7
7
|
import { generateId as uuid } from "../../utils";
|
|
8
|
-
import {
|
|
8
|
+
import { loadAggregate, VersionConflictError } from "../index";
|
|
9
9
|
import { appendProvenanceEvent, type ProvenanceEventInput } from "../provenance-append";
|
|
10
10
|
|
|
11
11
|
let testDb: BunTestDb;
|
|
@@ -37,7 +37,6 @@ function provenanceEvent(overrides: Partial<ProvenanceEventInput>): ProvenanceEv
|
|
|
37
37
|
beforeAll(async () => {
|
|
38
38
|
await ensureTemporalPolyfill();
|
|
39
39
|
testDb = await createTestDb();
|
|
40
|
-
await createEventsTable(testDb.db);
|
|
41
40
|
tdb = createTenantDb(testDb.db, tenantA);
|
|
42
41
|
});
|
|
43
42
|
|
|
@@ -22,7 +22,6 @@ import { generateId as uuid } from "../../utils";
|
|
|
22
22
|
import {
|
|
23
23
|
append,
|
|
24
24
|
archiveStream,
|
|
25
|
-
createEventsTable,
|
|
26
25
|
loadAggregate,
|
|
27
26
|
loadAggregateWithSnapshot,
|
|
28
27
|
loadLatestSnapshot,
|
|
@@ -56,7 +55,6 @@ const reducer: SnapshotReducer<CounterState> = (state, event) => {
|
|
|
56
55
|
beforeAll(async () => {
|
|
57
56
|
await ensureTemporalPolyfill();
|
|
58
57
|
bun = await createTestDb();
|
|
59
|
-
await createEventsTable(bun.db);
|
|
60
58
|
});
|
|
61
59
|
|
|
62
60
|
afterAll(async () => {
|
|
@@ -11,7 +11,7 @@ import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db
|
|
|
11
11
|
import { asRawClient, insertOne, selectMany } from "../../db/query";
|
|
12
12
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
13
13
|
import type { StoredEvent } from "../event-store";
|
|
14
|
-
import {
|
|
14
|
+
import { eventsTable } from "../events-schema";
|
|
15
15
|
import { type EventUpcasters, makeUpcastCtx, upcastStoredEvents } from "../upcaster";
|
|
16
16
|
import {
|
|
17
17
|
createUpcasterDeadLetterTable,
|
|
@@ -81,7 +81,6 @@ const passthroughUpcasters: EventUpcasters = new Map([
|
|
|
81
81
|
beforeAll(async () => {
|
|
82
82
|
await ensureTemporalPolyfill();
|
|
83
83
|
testDb = await createTestDb();
|
|
84
|
-
await createEventsTable(testDb.db);
|
|
85
84
|
await createUpcasterDeadLetterTable(testDb.db);
|
|
86
85
|
});
|
|
87
86
|
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
unsafeCreateEntityTable,
|
|
26
26
|
unsafePushTables,
|
|
27
27
|
} from "../../stack";
|
|
28
|
-
import { append
|
|
28
|
+
import { append } from "../index";
|
|
29
29
|
import { upcastStoredEvent } from "../upcaster";
|
|
30
30
|
|
|
31
31
|
// --- Fixture entity + projection table ---
|
|
@@ -113,7 +113,6 @@ const orderExecutor = createEventStoreExecutor(orderTable, orderEntity, {
|
|
|
113
113
|
beforeAll(async () => {
|
|
114
114
|
testDb = await createTestDb();
|
|
115
115
|
await unsafeCreateEntityTable(testDb.db, orderEntity, "upcast-order");
|
|
116
|
-
await createEventsTable(testDb.db);
|
|
117
116
|
const { createProjectionStateTable } = await import("../../pipeline");
|
|
118
117
|
await createProjectionStateTable(testDb.db);
|
|
119
118
|
await unsafePushTables(testDb.db, { upcastOrderSummary: orderSummaryTable });
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
defineFeature,
|
|
15
15
|
type SessionUser,
|
|
16
16
|
} from "../../engine";
|
|
17
|
-
import {
|
|
17
|
+
import { loadAggregate } from "../../event-store";
|
|
18
18
|
import {
|
|
19
19
|
createTestDb,
|
|
20
20
|
createTestUser,
|
|
@@ -79,7 +79,6 @@ beforeAll(async () => {
|
|
|
79
79
|
await unsafeCreateEntityTable(testDb.db, testTenantEntity);
|
|
80
80
|
// Event-store table: the upload route appends files:event:uploaded in the
|
|
81
81
|
// same tx as the FileRef insert. Without events, upload would 500.
|
|
82
|
-
await createEventsTable(testDb.db);
|
|
83
82
|
|
|
84
83
|
const registry = createRegistry([tenantFeature]);
|
|
85
84
|
const storageProvider = createLocalProvider(storagePath);
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type WriteResult,
|
|
22
22
|
} from "../../engine";
|
|
23
23
|
import { createWorkerEntrypoint } from "../../entrypoint";
|
|
24
|
-
import { createArchivedStreamsTable
|
|
24
|
+
import { createArchivedStreamsTable } from "../../event-store";
|
|
25
25
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
26
26
|
import { createTestRedis, type TestRedis } from "../../stack";
|
|
27
27
|
import { waitFor } from "../../testing";
|
|
@@ -110,7 +110,6 @@ let testRedis: TestRedis;
|
|
|
110
110
|
|
|
111
111
|
beforeAll(async () => {
|
|
112
112
|
[testDb, testRedis] = await Promise.all([createTestDb(), createTestRedis()]);
|
|
113
|
-
await createEventsTable(testDb.db);
|
|
114
113
|
await createArchivedStreamsTable(testDb.db);
|
|
115
114
|
await createEventConsumerStateTable(testDb.db);
|
|
116
115
|
});
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
defineFeature,
|
|
21
21
|
type ProjectionDefinition,
|
|
22
22
|
} from "../../engine";
|
|
23
|
-
import { createEventsTable } from "../../event-store";
|
|
24
23
|
import type { JobRunner } from "../../jobs/job-runner";
|
|
25
24
|
import { createProjectionStateTable } from "../../pipeline";
|
|
26
25
|
import {
|
|
@@ -97,7 +96,6 @@ let markerDir: string;
|
|
|
97
96
|
beforeAll(async () => {
|
|
98
97
|
testDb = await createTestDb();
|
|
99
98
|
await unsafeCreateEntityTable(testDb.db, itemEntity, "pending-item");
|
|
100
|
-
await createEventsTable(testDb.db);
|
|
101
99
|
await createProjectionStateTable(testDb.db);
|
|
102
100
|
await unsafePushTables(testDb.db, {
|
|
103
101
|
readPendingCounts: countsTable,
|
|
@@ -10,7 +10,6 @@ import { asRawClient } from "../../db/query";
|
|
|
10
10
|
import { buildEntityTable } from "../../db/table-builder";
|
|
11
11
|
import { createTenantDb, type TenantDb } from "../../db/tenant-db";
|
|
12
12
|
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
13
|
-
import { createEventsTable } from "../../event-store";
|
|
14
13
|
import { createProjectionStateTable, rebuildProjection } from "../../pipeline";
|
|
15
14
|
import { createTestDb, type TestDb, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
16
15
|
|
|
@@ -38,7 +37,6 @@ const crud = createEventStoreExecutor(personTable, personEntity, { entityName: "
|
|
|
38
37
|
beforeAll(async () => {
|
|
39
38
|
testDb = await createTestDb();
|
|
40
39
|
await unsafeCreateEntityTable(testDb.db, personEntity, "person");
|
|
41
|
-
await createEventsTable(testDb.db);
|
|
42
40
|
await createProjectionStateTable(testDb.db);
|
|
43
41
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
44
42
|
});
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
defineFeature,
|
|
12
12
|
type Registry,
|
|
13
13
|
} from "../../engine";
|
|
14
|
-
import { createEventsTable } from "../../event-store";
|
|
15
14
|
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
16
15
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
17
16
|
import { createCascadeDeleteHook } from "../cascade-handler";
|
|
@@ -96,7 +95,6 @@ const memberEntity = createEntity({
|
|
|
96
95
|
beforeAll(async () => {
|
|
97
96
|
await ensureTemporalPolyfill();
|
|
98
97
|
testDb = await createTestDb();
|
|
99
|
-
await createEventsTable(testDb.db);
|
|
100
98
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
101
99
|
|
|
102
100
|
await unsafeCreateEntityTable(testDb.db, departmentEntity);
|
|
@@ -16,7 +16,6 @@ import { integer, table as pgTable, uuid as pgUuid } from "../../db/dialect";
|
|
|
16
16
|
import { asRawClient } from "../../db/query";
|
|
17
17
|
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
18
18
|
import type { ProjectionDefinition } from "../../engine/types";
|
|
19
|
-
import { createEventsTable } from "../../event-store";
|
|
20
19
|
import { createProjectionStateTable, rebuildProjection } from "../../pipeline";
|
|
21
20
|
import { TestUsers, unsafePushTables } from "../../stack";
|
|
22
21
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
@@ -68,7 +67,6 @@ const qualifiedProjectionName = "perfrebuild:projection:task-count";
|
|
|
68
67
|
beforeAll(async () => {
|
|
69
68
|
await ensureTemporalPolyfill();
|
|
70
69
|
testDb = await createTestDb();
|
|
71
|
-
await createEventsTable(testDb.db);
|
|
72
70
|
await createProjectionStateTable(testDb.db);
|
|
73
71
|
await unsafePushTables(testDb.db, { perf_rebuild_task_count: taskCountTable });
|
|
74
72
|
});
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
defineFeature,
|
|
27
27
|
} from "../../engine";
|
|
28
28
|
import type { ProjectionDefinition } from "../../engine/types";
|
|
29
|
-
import { archiveStream
|
|
29
|
+
import { archiveStream } from "../../event-store";
|
|
30
30
|
import {
|
|
31
31
|
createProjectionStateTable,
|
|
32
32
|
getAllProjectionProgress,
|
|
@@ -104,7 +104,6 @@ const executor = createEventStoreExecutor(itemTable, itemEntity, { entityName: "
|
|
|
104
104
|
beforeAll(async () => {
|
|
105
105
|
testDb = await createTestDb();
|
|
106
106
|
await unsafeCreateEntityTable(testDb.db, itemEntity, "rebuild-item");
|
|
107
|
-
await createEventsTable(testDb.db);
|
|
108
107
|
await createProjectionStateTable(testDb.db);
|
|
109
108
|
await unsafePushTables(testDb.db, { rebuildItemsPerGroup: itemsPerGroupTable });
|
|
110
109
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
@@ -23,7 +23,7 @@ import { buildEntityTable } from "../../db/table-builder";
|
|
|
23
23
|
import { createTenantDb, type TenantDb } from "../../db/tenant-db";
|
|
24
24
|
import { createEntity, createTextField, defineApply, defineFeature } from "../../engine";
|
|
25
25
|
import type { ProjectionDefinition } from "../../engine/types";
|
|
26
|
-
import { append
|
|
26
|
+
import { append } from "../../event-store";
|
|
27
27
|
import { listRebuildDeadLetters } from "../../event-store/rebuild-dead-letter";
|
|
28
28
|
import {
|
|
29
29
|
createProjectionStateTable,
|
|
@@ -140,7 +140,6 @@ const executor = createEventStoreExecutor(itemTable, itemEntity, { entityName: "
|
|
|
140
140
|
beforeAll(async () => {
|
|
141
141
|
stack = await setupTestStack({ features: [feature] });
|
|
142
142
|
await unsafeCreateEntityTable(stack.db, itemEntity, "poison-item");
|
|
143
|
-
await createEventsTable(stack.db);
|
|
144
143
|
await createProjectionStateTable(stack.db);
|
|
145
144
|
await unsafePushTables(stack.db, {
|
|
146
145
|
poisonCounter: counterTable,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
createTenantDb,
|
|
18
18
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
19
19
|
import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
-
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
21
20
|
import { reindexEntity } from "@cosmicdrift/kumiko-framework/search";
|
|
22
21
|
import {
|
|
23
22
|
setupTestStack,
|
|
@@ -67,7 +66,6 @@ beforeAll(async () => {
|
|
|
67
66
|
stack = await setupTestStack({ features: [widgetFeature, contactFeature] });
|
|
68
67
|
await unsafeCreateEntityTable(stack.db, widgetEntity);
|
|
69
68
|
await unsafeCreateEntityTable(stack.db, contactEntity, "contact");
|
|
70
|
-
await createEventsTable(stack.db);
|
|
71
69
|
// Shared across both pii tests below (not per-test) — reindexEntity scans
|
|
72
70
|
// the whole tenant table regardless of which test created which row, so a
|
|
73
71
|
// fresh KMS instance per test would make earlier rows' subject keys
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
} from "../../crypto";
|
|
12
12
|
import { asRawClient, buildEntityTable, createEventStoreExecutor, createTenantDb } from "../../db";
|
|
13
13
|
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
14
|
-
import { createEventsTable } from "../../event-store";
|
|
15
14
|
import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
16
15
|
import { purgeSearchDocumentsForSubject } from "../purge-subject";
|
|
17
16
|
|
|
@@ -53,7 +52,6 @@ const admin = TestUsers.admin;
|
|
|
53
52
|
beforeAll(async () => {
|
|
54
53
|
stack = await setupTestStack({ features: [contactFeature] });
|
|
55
54
|
await unsafeCreateEntityTable(stack.db, contactEntity, "contact");
|
|
56
|
-
await createEventsTable(stack.db);
|
|
57
55
|
});
|
|
58
56
|
|
|
59
57
|
afterAll(async () => {
|