@cosmicdrift/kumiko-framework 0.367.0 → 0.368.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/AGENTS.md +1 -7
- package/README.md +1 -1
- package/agents/examples/access-control.md +2 -2
- package/agents/examples/anonymous-access-multitenant.md +2 -2
- package/agents/examples/anonymous-access.md +3 -3
- package/agents/examples/apex-landing.md +1 -1
- package/agents/examples/apex-surface-auth.md +6 -7
- package/agents/examples/api-worker-split.md +7 -8
- package/agents/examples/auth-claims.md +2 -1
- package/agents/examples/auth-foundation-providers.md +3 -3
- package/agents/examples/auth-mfa.md +3 -4
- package/agents/examples/basic-entity.md +3 -4
- package/agents/examples/chat-channels.md +4 -4
- package/agents/examples/compliance-profiles-demo.md +2 -3
- package/agents/examples/cross-feature-events.md +4 -4
- package/agents/examples/crypto-shredding-hr.md +14 -9
- package/agents/examples/currencies-global.md +3 -4
- package/agents/examples/currencies-per-tenant.md +3 -4
- package/agents/examples/custom-fields-basic.md +2 -3
- package/agents/examples/custom-handlers.md +3 -5
- package/agents/examples/dashboard-stats.md +2 -3
- package/agents/examples/delivery-notifications.md +5 -6
- package/agents/examples/e2e-generator.md +1 -1
- package/agents/examples/embedded-entity-form.md +2 -3
- package/agents/examples/embedded.md +2 -3
- package/agents/examples/encrypted-tenant-config.md +5 -6
- package/agents/examples/error-contract.md +3 -4
- package/agents/examples/event-sourcing.md +19 -23
- package/agents/examples/feature-toggles.md +4 -4
- package/agents/examples/field-access.md +3 -4
- package/agents/examples/files-post-processing.md +12 -13
- package/agents/examples/golden-path.md +208 -0
- package/agents/examples/idempotency.md +2 -2
- package/agents/examples/inbound-mail-triage.md +7 -7
- package/agents/examples/lane-routing.md +3 -3
- package/agents/examples/ledger-banking.md +2 -3
- package/agents/examples/ledger-invoicing.md +2 -3
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +3 -3
- package/agents/examples/managed-config.md +3 -4
- package/agents/examples/managed-pages.md +2 -2
- package/agents/examples/notes-history-basic.md +4 -5
- package/agents/examples/ownership.md +3 -5
- package/agents/examples/pdf-audit-export.md +2 -3
- package/agents/examples/pipeline-basics.md +2 -2
- package/agents/examples/public-share-token.md +3 -3
- package/agents/examples/rate-limiting.md +2 -1
- package/agents/examples/realtime-sse.md +2 -3
- package/agents/examples/record-detail-layout.md +1 -1
- package/agents/examples/reference-data.md +3 -3
- package/agents/examples/relations.md +2 -2
- package/agents/examples/screens-nav.md +1 -1
- package/agents/examples/search.md +2 -2
- package/agents/examples/secrets-demo.md +8 -8
- package/agents/examples/security-baseline.md +2 -2
- package/agents/examples/session-revocation.md +7 -8
- package/agents/examples/state-machine.md +181 -165
- package/agents/examples/tags-basic.md +2 -3
- package/agents/examples/tenant-isolation.md +2 -3
- package/agents/examples/tenant-settings.md +2 -2
- package/agents/examples/tier-admin.md +3 -4
- package/agents/examples/timezones.md +3 -3
- package/agents/examples/user-data-rights.md +12 -8
- package/agents/examples/user-profile.md +3 -3
- package/agents/examples/webhook-step.md +2 -3
- package/agents/examples/wizard-form.md +4 -5
- package/agents/examples/workflow-engine.md +7 -3
- package/agents/examples/writeform-section.md +1 -1
- package/agents/examples.md +1 -0
- package/dist/consumer-cli.js +2 -3
- package/dist/crypto/index.d.ts +1 -1
- package/dist/crypto/index.js +1 -1
- package/dist/crypto/owner-transition-rekey.js +2 -0
- package/dist/crypto/pg-kms-adapter.js +1 -1
- package/dist/crypto/subject-resolver.d.ts +2 -0
- package/dist/crypto/subject-resolver.js +30 -15
- package/dist/db/bun-db-deprecated.d.ts +10 -0
- package/dist/{bun-db/index.js → db/bun-db-deprecated.js} +4 -3
- package/dist/db/config-seed.js +1 -1
- package/dist/db/connection.d.ts +23 -5
- package/dist/db/connection.js +10 -5
- package/dist/db/dialect.js +1 -1
- package/dist/db/entity-field-encryption.d.ts +5 -0
- package/dist/db/entity-field-encryption.js +29 -0
- package/dist/db/entity-table-meta.js +6 -5
- package/dist/db/event-store-executor-read.js +26 -5
- package/dist/db/index.d.ts +4 -4
- package/dist/db/index.js +1 -1
- package/dist/db/migrate-runner.js +1 -1
- package/dist/db/queries/backfill-pii.js +16 -9
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/event-store.d.ts +2 -0
- package/dist/db/queries/event-store.js +26 -0
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/query.d.ts +105 -1
- package/dist/db/query.js +1450 -3
- package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
- package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
- package/dist/db/tenant-db.d.ts +11 -0
- package/dist/db/tenant-db.js +51 -12
- package/dist/derivatives/derivatives-context.d.ts +1 -1
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/gdpr-storage.js +3 -3
- package/dist/engine/boot-validator/pii-retention.js +3 -3
- package/dist/engine/boot-validator/record-owned.js +3 -3
- package/dist/engine/boot-validator/screens.js +8 -1
- package/dist/engine/bridge-stub.d.ts +4 -0
- package/dist/engine/bridge-stub.js +62 -0
- package/dist/engine/event-helpers.d.ts +2 -1
- package/dist/engine/event-helpers.js +1 -0
- package/dist/engine/feature-config-events-jobs.js +2 -2
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/es-ops/operations-schema.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/archive.js +1 -1
- package/dist/event-store/errors.d.ts +2 -1
- package/dist/event-store/errors.js +6 -2
- package/dist/event-store/event-store.js +17 -9
- package/dist/event-store/events-schema.js +1 -1
- package/dist/event-store/rebuild-dead-letter.js +3 -3
- package/dist/event-store/snapshot.js +1 -1
- package/dist/event-store/upcaster-dead-letter.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +1 -1
- package/dist/files/file-routes.js +1 -1
- package/dist/internal/stack-support.d.ts +12 -0
- package/dist/internal/stack-support.js +12 -0
- package/dist/internal/testing.d.ts +4 -0
- package/dist/internal/testing.js +5 -0
- package/dist/jobs/job-runner.js +23 -11
- package/dist/migrations/pending-rebuilds.js +1 -1
- package/dist/migrations/pii-event-backfill.js +2 -2
- package/dist/pipeline/dispatch-shared.js +43 -23
- package/dist/pipeline/dispatch-write.js +6 -2
- package/dist/pipeline/dispatcher-utils.js +6 -2
- package/dist/pipeline/event-consumer-state.js +1 -1
- package/dist/pipeline/projection-rebuild.d.ts +1 -0
- package/dist/pipeline/projection-rebuild.js +1 -1
- package/dist/pipeline/projection-state.js +1 -1
- package/dist/pipeline/system-hooks.js +1 -1
- package/dist/schema-cli.js +3 -3
- package/dist/search/purge-subject.js +20 -10
- package/dist/seeding/index.d.ts +1 -0
- package/dist/seeding/index.js +1 -0
- package/dist/stack/index.d.ts +10 -9
- package/dist/stack/index.js +11 -19
- package/dist/testing/e2e-generator.d.ts +5 -49
- package/dist/testing/e2e-generator.js +5 -382
- package/dist/testing/handler-context.d.ts +5 -4
- package/dist/testing/handler-context.js +5 -69
- package/dist/testing/index.d.ts +10 -26
- package/dist/testing/index.js +10 -34
- package/dist/testing/real-providers.d.ts +5 -3
- package/dist/testing/real-providers.js +5 -21
- package/package.json +37 -9
- package/src/changes.json +58 -0
- package/src/scripts/codemod/README.md +43 -0
- package/src/scripts/codemod/codemod-support.ts +276 -0
- package/src/scripts/codemod/db-factory.ts +810 -0
- package/src/scripts/codemod/feature-factory.ts +429 -0
- package/src/scripts/codemod/s3-imports.ts +274 -0
- package/src/scripts/codemod/tenant-db-options.ts +454 -0
- package/src/scripts/codemod/testing-move-targets.ts +161 -0
- package/src/scripts/codemod/testing-move.ts +520 -0
- package/dist/bun-db/connection.d.ts +0 -17
- package/dist/bun-db/connection.js +0 -40
- package/dist/bun-db/index.d.ts +0 -4
- package/dist/bun-db/query.d.ts +0 -106
- package/dist/bun-db/query.js +0 -1457
- package/dist/db/api.d.ts +0 -19
- package/dist/db/api.js +0 -23
- package/dist/db/bun-provider.d.ts +0 -2
- package/dist/db/bun-provider.js +0 -28
- package/dist/db/postgres-provider.d.ts +0 -2
- package/dist/db/postgres-provider.js +0 -21
- package/dist/db/query-api.d.ts +0 -2
- package/dist/db/query-api.js +0 -6
- package/dist/stack/db.d.ts +0 -22
- package/dist/stack/db.js +0 -67
- package/dist/stack/drain-event-consumers.d.ts +0 -4
- package/dist/stack/drain-event-consumers.js +0 -51
- package/dist/stack/drain-jobs.d.ts +0 -22
- package/dist/stack/drain-jobs.js +0 -141
- package/dist/stack/event-collector.d.ts +0 -9
- package/dist/stack/event-collector.js +0 -12
- package/dist/stack/push-entity-projection-tables.d.ts +0 -24
- package/dist/stack/push-entity-projection-tables.js +0 -74
- package/dist/stack/redis.d.ts +0 -17
- package/dist/stack/redis.js +0 -52
- package/dist/stack/request-helper.d.ts +0 -35
- package/dist/stack/request-helper.js +0 -144
- package/dist/stack/test-stack.d.ts +0 -192
- package/dist/stack/test-stack.js +0 -429
- package/dist/testing/access-assertions.d.ts +0 -2
- package/dist/testing/access-assertions.js +0 -13
- package/dist/testing/assertions.d.ts +0 -10
- package/dist/testing/assertions.js +0 -22
- package/dist/testing/boot-validator-fixture.d.ts +0 -3
- package/dist/testing/boot-validator-fixture.js +0 -101
- package/dist/testing/closed-connection-error.d.ts +0 -7
- package/dist/testing/closed-connection-error.js +0 -46
- package/dist/testing/db-cleanup.d.ts +0 -9
- package/dist/testing/db-cleanup.js +0 -31
- package/dist/testing/expect-error.d.ts +0 -2
- package/dist/testing/expect-error.js +0 -22
- package/dist/testing/file-provider-contract.d.ts +0 -2
- package/dist/testing/file-provider-contract.js +0 -102
- package/dist/testing/http-cookies.d.ts +0 -7
- package/dist/testing/http-cookies.js +0 -47
- package/dist/testing/late-bound.d.ts +0 -9
- package/dist/testing/late-bound.js +0 -31
- package/dist/testing/manual-clock.d.ts +0 -14
- package/dist/testing/manual-clock.js +0 -12
- package/dist/testing/multipart-helper.d.ts +0 -22
- package/dist/testing/multipart-helper.js +0 -77
- package/dist/testing/mutable-master-key-provider.d.ts +0 -7
- package/dist/testing/mutable-master-key-provider.js +0 -35
- package/dist/testing/observability-recorder.d.ts +0 -9
- package/dist/testing/observability-recorder.js +0 -29
- package/dist/testing/seed.d.ts +0 -9
- package/dist/testing/seed.js +0 -21
- package/dist/testing/shared-entities.d.ts +0 -286
- package/dist/testing/shared-entities.js +0 -56
- package/dist/testing/utils.d.ts +0 -1
- package/dist/testing/utils.js +0 -1
- package/dist/testing/wait-for.d.ts +0 -20
- package/dist/testing/wait-for.js +0 -47
- package/dist/testing/without-ambient-temporal.d.ts +0 -5
- package/dist/testing/without-ambient-temporal.js +0 -17
- package/dist/testing/zip-fixture.d.ts +0 -1
- package/dist/testing/zip-fixture.js +0 -33
- /package/dist/{stack/test-users.d.ts → seeding/seed-users.d.ts} +0 -0
- /package/dist/{stack/test-users.js → seeding/seed-users.js} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
1
|
+
import type { EntityDefinition } from "../engine/types/index.js";
|
|
2
|
+
import type { DbConnection } from "./connection.js";
|
|
3
|
+
import type { ColumnMeta, EntityTableMeta, IndexMeta } from "./entity-table-meta.js";
|
|
4
4
|
/**
|
|
5
5
|
* Bypass: creates an entity-table directly without going through the
|
|
6
6
|
* full registry. Reserved for framework-internal meta-tables and
|
|
7
7
|
* test setup — apps declare data via `r.entity(...)`.
|
|
8
8
|
*/
|
|
9
|
-
export declare function unsafeCreateEntityTable(db: DbConnection, entity:
|
|
10
|
-
export declare function unsafeEnsureEntityTable(db: DbConnection, entity:
|
|
9
|
+
export declare function unsafeCreateEntityTable(db: DbConnection, entity: EntityDefinition, entityName?: string): Promise<void>;
|
|
10
|
+
export declare function unsafeEnsureEntityTable(db: DbConnection, entity: EntityDefinition, entityName?: string): Promise<boolean>;
|
|
11
11
|
export declare function tableToMeta(table: unknown): EntityTableMeta;
|
|
12
12
|
/** Applies ALTER TABLE ADD COLUMN for exactly the given columns. */
|
|
13
13
|
export declare function addMissingColumns(db: DbConnection, tableName: string, columns: readonly ColumnMeta[]): Promise<void>;
|
|
@@ -22,13 +22,3 @@ export declare function addMissingIndexes(db: DbConnection, tableName: string, i
|
|
|
22
22
|
* entities go through `kumiko schema apply` (committed SQL files).
|
|
23
23
|
*/
|
|
24
24
|
export declare function unsafePushTables(db: DbConnection, tables: Record<string, unknown>, prevTables?: Record<string, unknown>): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
* Wipes event store + framework-state + the given feature read-models in
|
|
27
|
-
* one TRUNCATE, then re-registers the event-consumer state rows. Used in
|
|
28
|
-
* test beforeEach-hooks to return the stack to a clean slate without
|
|
29
|
-
* rebuilding it.
|
|
30
|
-
*/
|
|
31
|
-
export declare function resetEventStore(stack: {
|
|
32
|
-
db: unknown;
|
|
33
|
-
eventDispatcher?: EventDispatcher;
|
|
34
|
-
}, extraTables?: readonly (unknown | string)[]): Promise<void>;
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { pgTypeToSqlType } from "
|
|
2
|
-
import { alterTableAddColumn, createIndexIfNotExists, executeDdlStatement
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { asEntityTableMeta, schemaTableName } from "../db/table-introspection.js";
|
|
8
|
-
function tableNameOf(table) {
|
|
9
|
-
if (typeof table !== "object" || table === null) {
|
|
10
|
-
throw new Error("table-helpers: table is not a SchemaTable object");
|
|
11
|
-
}
|
|
12
|
-
const symbolName = schemaTableName(table);
|
|
13
|
-
if (symbolName !== undefined)
|
|
14
|
-
return symbolName;
|
|
15
|
-
if ("tableName" in table && typeof table.tableName === "string")
|
|
16
|
-
return table.tableName;
|
|
17
|
-
throw new Error("table-helpers: table has no name");
|
|
18
|
-
}
|
|
1
|
+
import { pgTypeToSqlType } from "./dialect.js";
|
|
2
|
+
import { alterTableAddColumn, createIndexIfNotExists, executeDdlStatement } from "./queries/ddl.js";
|
|
3
|
+
import { renderTableDdl } from "./render-ddl.js";
|
|
4
|
+
import { tableExists } from "./schema-inspection.js";
|
|
5
|
+
import { buildEntityTable, toTableName } from "./table-builder.js";
|
|
6
|
+
import { asEntityTableMeta } from "./table-introspection.js";
|
|
19
7
|
/**
|
|
20
8
|
* Bypass: creates an entity-table directly without going through the
|
|
21
9
|
* full registry. Reserved for framework-internal meta-tables and
|
|
@@ -96,31 +84,3 @@ export async function unsafePushTables(db, tables, prevTables) {
|
|
|
96
84
|
function renderColumnType(col) {
|
|
97
85
|
return pgTypeToSqlType(col.pgType);
|
|
98
86
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Wipes event store + framework-state + the given feature read-models in
|
|
101
|
-
* one TRUNCATE, then re-registers the event-consumer state rows. Used in
|
|
102
|
-
* test beforeEach-hooks to return the stack to a clean slate without
|
|
103
|
-
* rebuilding it.
|
|
104
|
-
*/
|
|
105
|
-
export async function resetEventStore(stack, extraTables = []) {
|
|
106
|
-
const frameworkTables = [
|
|
107
|
-
"kumiko_events",
|
|
108
|
-
"kumiko_event_consumers",
|
|
109
|
-
"kumiko_archived_streams",
|
|
110
|
-
"kumiko_snapshots",
|
|
111
|
-
"kumiko_projections",
|
|
112
|
-
];
|
|
113
|
-
const extraNames = extraTables.map((t) => (typeof t === "string" ? t : tableNameOf(t)));
|
|
114
|
-
const truncateAndReregister = async () => {
|
|
115
|
-
await truncateTablesRestartIdentity(stack.db, [...frameworkTables, ...extraNames]);
|
|
116
|
-
await stack.eventDispatcher?.ensureRegistered();
|
|
117
|
-
};
|
|
118
|
-
// The dispatcher's own timer/LISTEN must not start a pass between the
|
|
119
|
-
// TRUNCATE and the consumer re-registration, so they are paused for the whole reset.
|
|
120
|
-
if (stack.eventDispatcher) {
|
|
121
|
-
await stack.eventDispatcher.withBackgroundPassesPaused(truncateAndReregister);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
await truncateAndReregister();
|
|
125
|
-
}
|
|
126
|
-
}
|
package/dist/db/tenant-db.d.ts
CHANGED
|
@@ -37,6 +37,17 @@ export declare function acknowledgeConventionCrossTenant(tenantDb: TenantDb, rea
|
|
|
37
37
|
}): TenantDb;
|
|
38
38
|
export declare function reportConventionCrossTenantUse(acknowledged: TenantDb, target?: EscapeHatchTarget): void;
|
|
39
39
|
export declare function runInOwnTransaction<T>(tenantDb: TenantDb, fn: (txDb: TenantDb) => Promise<T>): Promise<T>;
|
|
40
|
+
export type CreateTenantDbOptions = {
|
|
41
|
+
readonly db: DbRunner;
|
|
42
|
+
readonly tenantId: TenantId;
|
|
43
|
+
readonly mode?: TenantDbMode;
|
|
44
|
+
readonly tracer?: Tracer;
|
|
45
|
+
readonly meter?: Meter;
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
readonly grants?: TenantDbGrants;
|
|
48
|
+
};
|
|
49
|
+
export declare function createTenantDb(options: CreateTenantDbOptions): TenantDb;
|
|
50
|
+
/** @deprecated Positional form, use createTenantDb({ db, tenantId, ... }). Run `bun kumiko-upgrade --apply` to rewrite call sites. Removed in the first release after 2026-10-29. */
|
|
40
51
|
export declare function createTenantDb(db: DbRunner, tenantId: TenantId, mode?: TenantDbMode, tracer?: Tracer, meter?: Meter, signal?: AbortSignal, grants?: TenantDbGrants): TenantDb;
|
|
41
52
|
export declare function createProjectingTenantDb(db: DbRunner, tenantId: TenantId, projectionRegistry: Registry | undefined): TenantDb;
|
|
42
53
|
export { asRawClient };
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -225,7 +225,7 @@ export function createSystemDbView(db, dbOutsideTransaction, report = fallbackEs
|
|
|
225
225
|
// same connection/transaction. For code that must run caller-supplied callbacks (e.g. config
|
|
226
226
|
// validators) without handing them a cross-tenant view.
|
|
227
227
|
export function scopeTenantDbToTenant(db, tenantId) {
|
|
228
|
-
return createTenantDb(tenantDbRunner(db), tenantId, "tenant");
|
|
228
|
+
return createTenantDb({ db: tenantDbRunner(db), tenantId, mode: "tenant" });
|
|
229
229
|
}
|
|
230
230
|
// @cast-boundary tenant-db-row
|
|
231
231
|
export function castTenantRows(rows) {
|
|
@@ -311,7 +311,26 @@ export async function runInOwnTransaction(tenantDb, fn) {
|
|
|
311
311
|
}
|
|
312
312
|
return rebind(fn);
|
|
313
313
|
}
|
|
314
|
-
|
|
314
|
+
// Runners (postgres-js Sql/tx handles) are functions and RawDbClient mocks carry no own
|
|
315
|
+
// `tenantId`/`db`, so a runner is never mistaken for the options object.
|
|
316
|
+
function isCreateTenantDbOptions(value) {
|
|
317
|
+
return (typeof value === "object" &&
|
|
318
|
+
value !== null &&
|
|
319
|
+
Object.hasOwn(value, "tenantId") &&
|
|
320
|
+
Object.hasOwn(value, "db"));
|
|
321
|
+
}
|
|
322
|
+
export function createTenantDb(dbOrOptions, tenantId, mode, tracer, meter, signal, grants) {
|
|
323
|
+
if (isCreateTenantDbOptions(dbOrOptions))
|
|
324
|
+
return buildTenantDb(dbOrOptions);
|
|
325
|
+
return buildTenantDb({ db: dbOrOptions, tenantId, mode, tracer, meter, signal, grants });
|
|
326
|
+
}
|
|
327
|
+
// Untyped JS callers or an `as` cast can still hand in undefined; a scoped handle
|
|
328
|
+
// without a tenant would bind `tenant_id = NULL` instead of failing loudly.
|
|
329
|
+
function buildTenantDb(options) {
|
|
330
|
+
const { db, tenantId, mode = "tenant", tracer, meter, signal, grants } = options;
|
|
331
|
+
if (tenantId === undefined) {
|
|
332
|
+
throw new InternalError({ message: "createTenantDb: tenantId is required" });
|
|
333
|
+
}
|
|
315
334
|
if (meter)
|
|
316
335
|
registerStandardMetrics(meter);
|
|
317
336
|
const report = grants?.report ?? fallbackEscapeHatchReporter(tenantId);
|
|
@@ -585,16 +604,32 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
585
604
|
},
|
|
586
605
|
};
|
|
587
606
|
declaredUnsafeRawRunners.set(tenantDb, grantedUnsafeRawRunner);
|
|
588
|
-
unsafeRawRebinders.set(tenantDb, (grant) => createTenantDb(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
607
|
+
unsafeRawRebinders.set(tenantDb, (grant) => createTenantDb({
|
|
608
|
+
db,
|
|
609
|
+
tenantId,
|
|
610
|
+
mode,
|
|
611
|
+
tracer,
|
|
612
|
+
meter,
|
|
613
|
+
signal,
|
|
614
|
+
grants: {
|
|
615
|
+
...rebindGrants,
|
|
616
|
+
unsafeRaw: grant,
|
|
617
|
+
// db.global() writes are write-handler-only: a rebind (a hook's own grant) must not inherit them.
|
|
618
|
+
globalWrites: undefined,
|
|
619
|
+
},
|
|
593
620
|
}));
|
|
594
621
|
crossTenantRebinders.set(tenantDb, (reason, deferReport) => {
|
|
595
622
|
if (!deferReport)
|
|
596
623
|
report("acknowledge-cross-tenant", reason);
|
|
597
|
-
const acknowledged = createTenantDb(
|
|
624
|
+
const acknowledged = createTenantDb({
|
|
625
|
+
db,
|
|
626
|
+
tenantId,
|
|
627
|
+
mode: "system",
|
|
628
|
+
tracer,
|
|
629
|
+
meter,
|
|
630
|
+
signal,
|
|
631
|
+
grants: rebindGrants,
|
|
632
|
+
});
|
|
598
633
|
crossTenantUseReporters.set(acknowledged, (target) => report("acknowledge-cross-tenant", reason, target));
|
|
599
634
|
return acknowledged;
|
|
600
635
|
});
|
|
@@ -607,7 +642,7 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
607
642
|
}
|
|
608
643
|
// Carries a runner-bound gate (grants.personalDataGate unset, resolved via the fallback above) forward explicitly, since the fresh tx-handle isn't itself registered in runnerPersonalDataGates.
|
|
609
644
|
const txGrants = personalDataGate ? { ...rebindGrants, personalDataGate } : rebindGrants;
|
|
610
|
-
return runInNewTransaction(db, (tx) => fn(createTenantDb(tx, tenantId, mode, tracer, meter, signal, txGrants)));
|
|
645
|
+
return runInNewTransaction(db, (tx) => fn(createTenantDb({ db: tx, tenantId, mode, tracer, meter, signal, grants: txGrants })));
|
|
611
646
|
});
|
|
612
647
|
bindTenantDbRunner(tenantDb, db);
|
|
613
648
|
if (grants?.writesDeniedReason !== undefined) {
|
|
@@ -619,9 +654,13 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
619
654
|
bindProjectionRegistry(tenantDb, projectionRegistry);
|
|
620
655
|
return tenantDb;
|
|
621
656
|
}
|
|
622
|
-
// Tenant-mode TenantDb whose writes reach the registry's custom projections.
|
|
623
|
-
// undefineds of createTenantDb would hide a moved grants parameter from the type checker.
|
|
657
|
+
// Tenant-mode TenantDb whose writes reach the registry's custom projections.
|
|
624
658
|
export function createProjectingTenantDb(db, tenantId, projectionRegistry) {
|
|
625
|
-
return createTenantDb(
|
|
659
|
+
return createTenantDb({
|
|
660
|
+
db,
|
|
661
|
+
tenantId,
|
|
662
|
+
mode: "tenant",
|
|
663
|
+
grants: projectionRegistry ? { projectionRegistry } : undefined,
|
|
664
|
+
});
|
|
626
665
|
}
|
|
627
666
|
export { asRawClient };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DerivativeRendererPlugin, DerivativesContext } from "@cosmicdrift/kumiko-types/derivatives-types";
|
|
2
|
-
import { type AnyDb } from "../
|
|
2
|
+
import { type AnyDb } from "../db/query.js";
|
|
3
3
|
import type { Registry, TenantId } from "../engine/types/index.js";
|
|
4
4
|
import type { FileContext } from "../files/file-handle.js";
|
|
5
5
|
export type DerivativesContextDeps = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchOne } from "../
|
|
1
|
+
import { fetchOne } from "../db/query.js";
|
|
2
2
|
import { EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_RENDERER, } from "../engine/extension-names.js";
|
|
3
3
|
import { InternalError, NotFoundError } from "../errors/index.js";
|
|
4
4
|
import { fileRefsTable } from "../files/file-ref-table.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Providers whose bytes do not survive a process restart. Only "inmemory"
|
|
2
2
|
// today; extend if another ephemeral bundled provider lands.
|
|
3
3
|
const EPHEMERAL_PROVIDERS = new Set(["inmemory"]);
|
|
4
|
-
// Env vars
|
|
4
|
+
// Env vars createFileProviderS3Feature({ fromEnv: true }) reads (see file-provider-s3/lib/env-helper).
|
|
5
5
|
// Missing any → createS3ProviderFromEnv throws on the first file op.
|
|
6
6
|
const S3_ENV_REQUIRED = ["S3_BUCKET", "S3_REGION", "S3_ACCESS_KEY", "S3_SECRET_KEY"];
|
|
7
7
|
// Cross-feature GDPR storage guard (V1). Catches the failure class we shipped
|
|
@@ -31,7 +31,7 @@ export function validateGdprStoragePersistence(features) {
|
|
|
31
31
|
const persistent = [...registeredProviders].filter((p) => !EPHEMERAL_PROVIDERS.has(p));
|
|
32
32
|
if (persistent.length === 0) {
|
|
33
33
|
// biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
|
|
34
|
-
console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount
|
|
34
|
+
console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
|
|
35
35
|
// skip: ephemeral store already warned; the s3-env env-var check below is moot
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
@@ -42,7 +42,7 @@ export function validateGdprStoragePersistence(features) {
|
|
|
42
42
|
const missing = S3_ENV_REQUIRED.filter((v) => !process.env[v]);
|
|
43
43
|
if (missing.length > 0) {
|
|
44
44
|
// biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
|
|
45
|
-
console.warn(`[kumiko:boot]
|
|
45
|
+
console.warn(`[kumiko:boot] createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -204,11 +204,11 @@ export function validatePiiAndRetention(feature) {
|
|
|
204
204
|
throw new Error(`[Feature ${feature.name}] personal.of "${ownerName}" on entity "${entityName}" must be a reference or text (userId) field, got type "${ownerField.type}"`);
|
|
205
205
|
}
|
|
206
206
|
const { whenAbsent } = annot.userOwned;
|
|
207
|
-
if (whenAbsent !== undefined && whenAbsent !== "tenant") {
|
|
208
|
-
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" (no plaintext fallback for stored PII). Remove whenAbsent or set it to "tenant".`);
|
|
207
|
+
if (whenAbsent !== undefined && whenAbsent !== "tenant" && whenAbsent !== "self") {
|
|
208
|
+
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" or "self" (no plaintext fallback for stored PII). Remove whenAbsent or set it to "tenant" (tenant key) or "self" (the row's own record key).`);
|
|
209
209
|
}
|
|
210
210
|
if (ownerField.required !== true && whenAbsent === undefined) {
|
|
211
|
-
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" is personal: { of: "${ownerName}" }, but "${ownerName}" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: "${ownerName}", whenAbsent: "tenant" } } to encrypt those rows under the tenant key, or make "${ownerName}" required: true. Entity fields have no plaintext fallback (fw#3654).`);
|
|
211
|
+
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" is personal: { of: "${ownerName}" }, but "${ownerName}" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: "${ownerName}", whenAbsent: "tenant" } } to encrypt those rows under the tenant key, whenAbsent: "self" to encrypt them under the row's own record key (shreddable per record), or make "${ownerName}" required: true. Entity fields have no plaintext fallback (fw#3654).`);
|
|
212
212
|
}
|
|
213
213
|
// Soft-Warning wenn das reference-target nicht offensichtlich user
|
|
214
214
|
// ist — custom subject-entities (HR-Mitarbeiter, Patient) sind
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// A recordOwned field is encrypted under `record:<entity>:<id>` (kms-adapter-types.ts)
|
|
1
|
+
// A recordOwned field (or one with whenAbsent: "self") is encrypted under `record:<entity>:<id>` (kms-adapter-types.ts)
|
|
2
2
|
// and forgetSubject's subjectIdSchema (crypto/kms-adapter.ts) requires that id
|
|
3
3
|
// to be a UUID — an entity with idType: "serial" would encrypt the field but
|
|
4
4
|
// could never satisfy a forget-subject request for it.
|
|
@@ -8,9 +8,9 @@ export function validateRecordOwnedSubjects(feature) {
|
|
|
8
8
|
continue;
|
|
9
9
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
10
10
|
const annot = field; // @cast-boundary schema-walk
|
|
11
|
-
if (annot.recordOwned !== true)
|
|
11
|
+
if (annot.recordOwned !== true && annot.userOwned?.whenAbsent !== "self")
|
|
12
12
|
continue;
|
|
13
|
-
throw new Error(`[Feature ${feature.name}] entity "${entityName}" field "${fieldName}" is recordOwned but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field would be encrypted but could never be shredded. Set idType: "uuid" on entity "${entityName}", or give field "${fieldName}" a different personal stance (personal: "self" / "tenant" / { of: "<field>" }).`);
|
|
13
|
+
throw new Error(`[Feature ${feature.name}] entity "${entityName}" field "${fieldName}" is recordOwned (or whenAbsent: "self") but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field would be encrypted but could never be shredded. Set idType: "uuid" on entity "${entityName}", or give field "${fieldName}" a different personal stance (personal: "self" / "tenant" / { of: "<field>" }).`);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -7,6 +7,7 @@ import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
|
7
7
|
import { NO_WIDGET_FIELD_TYPES, optionsQueryFieldRefs, SELECT_OPTION_TONES, } from "@cosmicdrift/kumiko-types/fields";
|
|
8
8
|
import { NAV_ICON_KEYS } from "@cosmicdrift/kumiko-types/nav-icon";
|
|
9
9
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
10
|
+
import { isListFilterAllowed } from "../../db/entity-field-encryption.js";
|
|
10
11
|
import { rowMetaFieldNames } from "../../db/table-builder.js";
|
|
11
12
|
import { REFERENCE_LOOKUP_SOURCES } from "../../ui-types/list-row-meta.js";
|
|
12
13
|
import { isEncryptedAtRest } from "../config-helpers.js";
|
|
@@ -230,7 +231,7 @@ function validateWizardLayout(featureName, screenId, screenType, layout, feature
|
|
|
230
231
|
if (!featureMap.has("form-draft")) {
|
|
231
232
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) sets draft: true but the ` +
|
|
232
233
|
`bundled feature "form-draft" is not mounted — every resume would silently lose its ` +
|
|
233
|
-
`values. Add
|
|
234
|
+
`values. Add createFormDraftFeature() from @cosmicdrift/kumiko-bundled-features to the app's ` +
|
|
234
235
|
`feature list.`);
|
|
235
236
|
}
|
|
236
237
|
}
|
|
@@ -1641,6 +1642,12 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1641
1642
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references unknown ` +
|
|
1642
1643
|
`field "${filterField}". Known fields: ${[...fieldNames].sort().join(", ")}`);
|
|
1643
1644
|
}
|
|
1645
|
+
if (!isListFilterAllowed(entityDef, filterField, screen.filter)) {
|
|
1646
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references field ` +
|
|
1647
|
+
`"${filterField}" which is encrypted, personal or secret, so the list handler rejects ` +
|
|
1648
|
+
`this filter at runtime. Only op "eq" works on a personal field with find: "exact" or ` +
|
|
1649
|
+
`"fuzzy"; otherwise drop the filter or filter on another field.`);
|
|
1650
|
+
}
|
|
1644
1651
|
const fieldDef = entityDef.fields[filterField];
|
|
1645
1652
|
if (fieldDef !== undefined && !isFieldFilterable(fieldDef)) {
|
|
1646
1653
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references field ` +
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HandlerContext, SessionUser } from "./types/index.js";
|
|
2
|
+
export declare function bridgeStub(opts?: {
|
|
3
|
+
readonly user?: SessionUser;
|
|
4
|
+
}): Pick<HandlerContext, "query" | "queryAs" | "write" | "writeAs" | "appendEvent" | "unsafeAppendEvent" | "tryAppendEvent" | "fetchForWriting" | "loadAggregate" | "archiveStream" | "restoreStream" | "isStreamArchived" | "snapshotAggregate" | "loadAggregateWithSnapshot" | "queryProjection" | "resolveAuthClaims" | "resolveActiveMembership" | "queryAsMember" | "hasFeature" | "metrics" | "metricsFor" | "tracer" | "tz" | "locale" | "user">;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DEFAULT_LOCALE } from "../i18n/request-locale.js";
|
|
2
|
+
import { createNoopMetricsHandle, getFallbackTracer } from "../observability/index.js";
|
|
3
|
+
import { createTzContext } from "../time/index.js";
|
|
4
|
+
import { createAnonymousUser } from "./system-user.js";
|
|
5
|
+
// Test/service helper: cross-feature bridge methods that throw on use.
|
|
6
|
+
//
|
|
7
|
+
// Production code always receives a full HandlerContext from the Dispatcher's
|
|
8
|
+
// buildHandlerContext (with real query/write closures). Some internal services
|
|
9
|
+
// and tests construct a mini-context manually (typically just `{ db, registry }`)
|
|
10
|
+
// to invoke a single handler. Those call sites don't use ctx.query/write —
|
|
11
|
+
// the stubs make the TypeScript shape match while still failing loudly if
|
|
12
|
+
// anything downstream accidentally reaches for them.
|
|
13
|
+
//
|
|
14
|
+
// Use: `{ db, registry, ...bridgeStub() }`
|
|
15
|
+
const notAvailable = (what) => async () => {
|
|
16
|
+
throw new Error(`ctx.${what} not available in this context — use the dispatcher, not a stubbed handler context`);
|
|
17
|
+
};
|
|
18
|
+
// Noop observability — hand back the shared fallback tracer so ctx.tracer has
|
|
19
|
+
// a valid Tracer shape. No allocations per call.
|
|
20
|
+
const noopTracer = getFallbackTracer();
|
|
21
|
+
export function bridgeStub(opts) {
|
|
22
|
+
// ctx.user is a convenience alias for event.user (see HandlerContext
|
|
23
|
+
// docs). Caller code expects the field; bridgeStub hands back a stub with
|
|
24
|
+
// anonymous default values when no user is passed explicitly. Test code
|
|
25
|
+
// that cares about identity passes its own SessionUser here and gets it
|
|
26
|
+
// back on ctx.
|
|
27
|
+
const stubUser = opts?.user ??
|
|
28
|
+
createAnonymousUser("00000000-0000-0000-0000-000000000000"); // @cast-boundary engine-bridge
|
|
29
|
+
return {
|
|
30
|
+
user: stubUser,
|
|
31
|
+
query: notAvailable("query"), // @cast-boundary engine-bridge
|
|
32
|
+
queryAs: notAvailable("queryAs"),
|
|
33
|
+
write: notAvailable("write"),
|
|
34
|
+
writeAs: notAvailable("writeAs"),
|
|
35
|
+
appendEvent: notAvailable("appendEvent"),
|
|
36
|
+
unsafeAppendEvent: notAvailable("unsafeAppendEvent"),
|
|
37
|
+
tryAppendEvent: notAvailable("tryAppendEvent"),
|
|
38
|
+
fetchForWriting: notAvailable("fetchForWriting"),
|
|
39
|
+
loadAggregate: notAvailable("loadAggregate"),
|
|
40
|
+
archiveStream: notAvailable("archiveStream"),
|
|
41
|
+
restoreStream: notAvailable("restoreStream"),
|
|
42
|
+
isStreamArchived: notAvailable("isStreamArchived"),
|
|
43
|
+
snapshotAggregate: notAvailable("snapshotAggregate"),
|
|
44
|
+
loadAggregateWithSnapshot: notAvailable("loadAggregateWithSnapshot"),
|
|
45
|
+
queryProjection: notAvailable("queryProjection"),
|
|
46
|
+
resolveAuthClaims: notAvailable("resolveAuthClaims"),
|
|
47
|
+
resolveActiveMembership: notAvailable("resolveActiveMembership"),
|
|
48
|
+
queryAsMember: notAvailable("queryAsMember"),
|
|
49
|
+
// Stub defaults to always-enabled — matches the dispatcher's behaviour
|
|
50
|
+
// when no effectiveFeatures resolver is wired (tests without toggles).
|
|
51
|
+
hasFeature: async () => true,
|
|
52
|
+
metrics: createNoopMetricsHandle(),
|
|
53
|
+
metricsFor: () => createNoopMetricsHandle(),
|
|
54
|
+
tracer: noopTracer,
|
|
55
|
+
// Real TzContext, not notAvailable — test code uses ctx.tz routinely,
|
|
56
|
+
// it isn't a "bridge" concept. Defaults to UTC.
|
|
57
|
+
tz: createTzContext(),
|
|
58
|
+
// Same reasoning as tz above — ctx.locale is always-present, not a
|
|
59
|
+
// bridge method. Defaults to DEFAULT_LOCALE.
|
|
60
|
+
locale: DEFAULT_LOCALE,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { EventDef, HandlerContext } from "./types/handlers.js";
|
|
1
|
+
import type { AppendEventArgs, EventDef, HandlerContext } from "./types/handlers.js";
|
|
2
2
|
export type EmitCtx = Pick<HandlerContext, "unsafeAppendEvent">;
|
|
3
3
|
export declare function emitEvent<TPayload>(ctx: EmitCtx, eventDef: EventDef<TPayload>, args: {
|
|
4
4
|
readonly aggregateId: string;
|
|
5
5
|
readonly aggregateType: string;
|
|
6
6
|
readonly payload: TPayload;
|
|
7
|
+
readonly headers?: AppendEventArgs["headers"];
|
|
7
8
|
}): Promise<void>;
|
|
8
9
|
export declare function typedPayload<TPayload>(event: {
|
|
9
10
|
readonly type: string;
|
|
@@ -21,6 +21,7 @@ export async function emitEvent(ctx, eventDef, args) {
|
|
|
21
21
|
aggregateType: args.aggregateType,
|
|
22
22
|
type: eventDef.name,
|
|
23
23
|
payload: args.payload,
|
|
24
|
+
...(args.headers !== undefined && { headers: args.headers }),
|
|
24
25
|
};
|
|
25
26
|
await ctx.unsafeAppendEvent(appendArgs);
|
|
26
27
|
}
|
|
@@ -40,12 +40,12 @@ function assertOwnerFieldYieldsPresentString(args) {
|
|
|
40
40
|
const { featureName, eventName, field, ownerField, owner } = args;
|
|
41
41
|
if (neverYieldsString(owner)) {
|
|
42
42
|
throw new Error(`[Feature ${featureName}] defineEvent("${eventName}"): piiFields."${field}" is owned by "${ownerField}", whose payload schema is not a string — the subject id must be a non-empty string, so every append would fail. ` +
|
|
43
|
-
`Use a string owner field, or declare whenAbsent ("tenant" | "plaintext").`);
|
|
43
|
+
`Use a string owner field, or declare whenAbsent ("tenant" | "self" | "plaintext").`);
|
|
44
44
|
}
|
|
45
45
|
if (isAbsentable(owner)) {
|
|
46
46
|
throw new Error(`[Feature ${featureName}] defineEvent("${eventName}"): piiFields."${field}" is owned by "${ownerField}", which the payload schema allows to be null/undefined. ` +
|
|
47
47
|
`Declare what happens then: { personal: { of: "${ownerField}", whenAbsent: "tenant" } } encrypts under the envelope tenant key, ` +
|
|
48
|
-
`whenAbsent: "plaintext" acknowledges that the value ships unencrypted and cannot be crypto-shredded (fw#2776).`);
|
|
48
|
+
`whenAbsent: "self" under the event's own record key, whenAbsent: "plaintext" acknowledges that the value ships unencrypted and cannot be crypto-shredded (fw#2776).`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
// Builds config/secrets/claims/events/jobs/notifications registrar methods.
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, type PrincipalP
|
|
|
4
4
|
export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
|
|
5
5
|
export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
|
|
6
6
|
export { type CardMetaOverflow, collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
7
|
+
export { bridgeStub } from "./bridge-stub.js";
|
|
7
8
|
export { type BuildAppSchemaOptions, buildAppSchema } from "./build-app-schema.js";
|
|
8
9
|
export type { ConfigFeatureSchema } from "./build-config-feature-schema.js";
|
|
9
10
|
export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
|
package/dist/engine/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, TENANT_TEARDOWN
|
|
|
5
5
|
export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
|
|
6
6
|
export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
|
|
7
7
|
export { collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
8
|
+
export { bridgeStub } from "./bridge-stub.js";
|
|
8
9
|
export { buildAppSchema } from "./build-app-schema.js";
|
|
9
10
|
export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
|
|
10
11
|
export { buildTarget } from "./build-target.js";
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { index, integer, table as pgTable, sql, text, timestamp } from "../db/dialect.js";
|
|
13
13
|
// sql now comes from native dialect
|
|
14
14
|
import { tableExists } from "../db/index.js";
|
|
15
|
-
import { unsafePushTables } from "../
|
|
15
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
16
16
|
export const esOperationsTable = pgTable("kumiko_es_operations", {
|
|
17
17
|
// Filename without extension serves as ID. Chronologically sortable
|
|
18
18
|
// (date-prefix convention), human-meaningful, no separate hash needed.
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
// Guard-Script `scripts/guard-admin-api.ts` blockt Aufrufe aus App-Code —
|
|
8
8
|
// Allowlist: samples/*/migration/, scripts/migrations/, die Definition
|
|
9
9
|
// selbst, das Guard-Script selbst.
|
|
10
|
-
import { instantToTimestamptzParam } from "../bun-db/query.js";
|
|
11
10
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
12
11
|
import { claimXactId, runAtomicEventWrite } from "../db/queries/event-store.js";
|
|
13
12
|
import { eventPredecessorExists, findExistingEventVersion, insertRawEventBatch, insertRawFirstEvent, insertRawSubsequentEvent, } from "../db/queries/event-store-admin.js";
|
|
13
|
+
import { instantToTimestamptzParam } from "../db/query.js";
|
|
14
14
|
import { IdempotentAppendConflictError, VersionConflictError } from "./errors.js";
|
|
15
15
|
function mapEventUniqueViolation(e, event) {
|
|
16
16
|
if (constraintOf(e) === "events_idempotency_uq" && event.metadata.idempotencyKey) {
|
|
@@ -3,7 +3,7 @@ import { instant, table as pgTable, sql, text, uniqueIndex, uuid } from "../db/d
|
|
|
3
3
|
import { upsertArchivedStream } from "../db/queries/event-store.js";
|
|
4
4
|
import { deleteMany, fetchOne } from "../db/query.js";
|
|
5
5
|
import { tableExists } from "../db/schema-inspection.js";
|
|
6
|
-
import { unsafePushTables } from "../
|
|
6
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
7
7
|
// Marten-aligned stream archival. Archived streams become read-only: fresh
|
|
8
8
|
// appendEvent on an archived aggregate throws, and loadAggregate returns
|
|
9
9
|
// an empty slice unless the caller passes { includeArchived: true }.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
export declare class VersionConflictError extends Error {
|
|
3
3
|
readonly aggregateId: string;
|
|
4
4
|
readonly expectedVersion: number;
|
|
5
|
-
|
|
5
|
+
readonly currentVersion: number | undefined;
|
|
6
|
+
constructor(aggregateId: string, expectedVersion: number, currentVersion?: number);
|
|
6
7
|
}
|
|
7
8
|
export declare class IdempotentAppendConflictError extends Error {
|
|
8
9
|
readonly tenantId: string;
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
export class VersionConflictError extends Error {
|
|
10
10
|
aggregateId;
|
|
11
11
|
expectedVersion;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Undefined when the conflict surfaced as a bare unique violation with no lookup possible.
|
|
13
|
+
currentVersion;
|
|
14
|
+
constructor(aggregateId, expectedVersion, currentVersion) {
|
|
15
|
+
super(`Version conflict on aggregate ${aggregateId}: expected predecessor version ${expectedVersion}` +
|
|
16
|
+
(currentVersion === undefined ? "" : `, stream is at version ${currentVersion}`));
|
|
17
|
+
this.currentVersion = currentVersion;
|
|
14
18
|
this.name = "VersionConflictError";
|
|
15
19
|
this.aggregateId = aggregateId;
|
|
16
20
|
this.expectedVersion = expectedVersion;
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
import { isPersonalDataGated, UNATTRIBUTED_ORIGIN, } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
3
3
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
import { requestContext } from "../api/request-context.js";
|
|
5
|
-
import { instantToTimestamptzParam } from "../bun-db/query.js";
|
|
6
5
|
import { encryptEventPayloadPii } from "../crypto/event-pii.js";
|
|
7
6
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
8
|
-
import { claimXactId, insertSubsequentEventRow, notifyPgChannel, runAtomicEventWrite, selectAggregateMaxVersion, selectEventsHighWaterMark, selectStreamMaxVersion, selectStreamMaxVersions, } from "../db/queries/event-store.js";
|
|
9
|
-
import {
|
|
7
|
+
import { claimXactId, insertFirstEventRow, insertSubsequentEventRow, notifyPgChannel, runAtomicEventWrite, selectAggregateMaxVersion, selectEventsHighWaterMark, selectStreamMaxVersion, selectStreamMaxVersions, } from "../db/queries/event-store.js";
|
|
8
|
+
import { instantToTimestamptzParam, selectMany } from "../db/query.js";
|
|
10
9
|
import { isStreamArchived } from "./archive.js";
|
|
11
10
|
import { IdempotentAppendConflictError, SeedTimeOrderError, VersionConflictError, } from "./errors.js";
|
|
12
11
|
import { eventsTable } from "./events-schema.js";
|
|
@@ -112,21 +111,21 @@ async function assertSeedTimeNotBeforePredecessor(db, event, seedCreatedAt) {
|
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
113
|
async function insertFirstEvent(db, event, newVersion, eventVersion, seedCreatedAt) {
|
|
115
|
-
const row = await
|
|
114
|
+
const row = await insertFirstEventRow(db, {
|
|
116
115
|
aggregateId: event.aggregateId,
|
|
117
116
|
aggregateType: event.aggregateType,
|
|
118
117
|
tenantId: event.tenantId,
|
|
119
|
-
|
|
118
|
+
newVersion,
|
|
120
119
|
type: event.type,
|
|
121
120
|
eventVersion,
|
|
122
121
|
payload: event.payload,
|
|
123
122
|
metadata: event.metadata,
|
|
124
123
|
createdBy: event.metadata.userId,
|
|
125
|
-
|
|
124
|
+
createdAt: seedCreatedAt && instantToTimestamptzParam(seedCreatedAt),
|
|
126
125
|
});
|
|
127
126
|
if (!row)
|
|
128
|
-
throw
|
|
129
|
-
return
|
|
127
|
+
throw await versionConflictWithCurrentVersion(db, event);
|
|
128
|
+
return insertReturnOf(row);
|
|
130
129
|
}
|
|
131
130
|
// Subsequent event — predecessor must exist AND belong to the same tenant.
|
|
132
131
|
// INSERT … SELECT … WHERE EXISTS is awkward in the typed builder, so this
|
|
@@ -147,7 +146,10 @@ async function insertSubsequentEvent(db, event, newVersion, eventVersion, seedCr
|
|
|
147
146
|
createdAt: seedCreatedAt && instantToTimestamptzParam(seedCreatedAt),
|
|
148
147
|
});
|
|
149
148
|
if (!row)
|
|
150
|
-
throw
|
|
149
|
+
throw await versionConflictWithCurrentVersion(db, event);
|
|
150
|
+
return insertReturnOf(row);
|
|
151
|
+
}
|
|
152
|
+
function insertReturnOf(row) {
|
|
151
153
|
const createdAt = row.created_at instanceof Date
|
|
152
154
|
? Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())
|
|
153
155
|
: Temporal.Instant.from(row.created_at);
|
|
@@ -156,6 +158,12 @@ async function insertSubsequentEvent(db, event, newVersion, eventVersion, seedCr
|
|
|
156
158
|
createdAt,
|
|
157
159
|
};
|
|
158
160
|
}
|
|
161
|
+
// Runs in the still-healthy write transaction: ON CONFLICT DO NOTHING left it
|
|
162
|
+
// unaborted, so the stream version is readable here.
|
|
163
|
+
async function versionConflictWithCurrentVersion(db, event) {
|
|
164
|
+
const currentVersion = await selectStreamMaxVersion(db, event.aggregateId, event.tenantId);
|
|
165
|
+
return new VersionConflictError(event.aggregateId, event.expectedVersion, currentVersion);
|
|
166
|
+
}
|
|
159
167
|
function buildStoredEvent(event, newVersion, eventVersion, row) {
|
|
160
168
|
return {
|
|
161
169
|
id: String(row.id),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { bigserial, index, instant, integer, jsonb, table as pgTable, sql, text, uniqueIndex, uuid, } from "../db/dialect.js";
|
|
3
3
|
import { tableExists } from "../db/index.js";
|
|
4
4
|
import { ensureIdempotencyKeyIndex, ensureMembershipPayloadTenantIndex, } from "../db/queries/event-store.js";
|
|
5
|
-
import { unsafePushTables } from "../
|
|
5
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
6
6
|
import { createArchivedStreamsTable } from "./archive.js";
|
|
7
7
|
import { createSnapshotsTable } from "./snapshot.js";
|
|
8
8
|
// Event-store schema as a Drizzle table. The typed select/insert path handles
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// is a separate ops step — same stance as the upcaster dead-letter.
|
|
15
15
|
import { bigint, index, jsonb, table as pgTable, text, timestamp, uuid } from "../db/dialect.js";
|
|
16
16
|
import { tableExists } from "../db/schema-inspection.js";
|
|
17
|
-
import { unsafePushTables } from "../
|
|
17
|
+
import { unsafePushTables } from "../db/table-push.js";
|
|
18
18
|
export const rebuildDeadLetterTable = pgTable("kumiko_rebuild_dead_letters", {
|
|
19
19
|
// Surrogate PK — the same event can land here across multiple rebuild
|
|
20
20
|
// attempts before the fix ships, without unique-violation noise.
|
|
@@ -47,7 +47,7 @@ export async function createRebuildDeadLetterTable(db) {
|
|
|
47
47
|
// tx). Unqualified insert — during the rebuild the search_path points at the
|
|
48
48
|
// shadow schema, and this table exists only in public, so it falls through.
|
|
49
49
|
export async function recordRebuildDeadLetters(db, projectionName, skipped) {
|
|
50
|
-
const { insertMany } = await import("../
|
|
50
|
+
const { insertMany } = await import("../db/query.js");
|
|
51
51
|
await insertMany(db, rebuildDeadLetterTable, skipped.map(({ event, error }) => ({
|
|
52
52
|
projectionName,
|
|
53
53
|
eventId: event.id,
|
|
@@ -61,7 +61,7 @@ export async function recordRebuildDeadLetters(db, projectionName, skipped) {
|
|
|
61
61
|
}
|
|
62
62
|
// Ops-side triage query, optionally scoped to one projection.
|
|
63
63
|
export async function listRebuildDeadLetters(db, options = {}) {
|
|
64
|
-
const { selectMany } = await import("../
|
|
64
|
+
const { selectMany } = await import("../db/query.js");
|
|
65
65
|
const limit = options.limit ?? 100;
|
|
66
66
|
const where = options.projectionName !== undefined ? { projectionName: options.projectionName } : undefined;
|
|
67
67
|
return selectMany(db, rebuildDeadLetterTable, where, {
|