@cosmicdrift/kumiko-framework 0.379.0 → 0.380.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/dist/db/apply-entity-event.d.ts +3 -2
- package/dist/db/apply-entity-event.js +1 -1
- package/dist/db/dialect.d.ts +2 -1
- package/dist/db/dialect.js +2 -2
- package/dist/db/entity-table-from-registry.d.ts +1 -2
- package/dist/db/entity-table-from-registry.js +0 -2
- package/dist/db/entity-table-meta.d.ts +5 -1
- package/dist/db/entity-table-meta.js +5 -12
- package/dist/db/executor-managed-tables.d.ts +3 -1
- package/dist/db/executor-managed-tables.js +9 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/query.d.ts +22 -11
- package/dist/db/query.js +13 -36
- package/dist/db/table-builder.d.ts +2 -3
- package/dist/db/table-introspection.d.ts +2 -0
- package/dist/db/table-introspection.js +4 -0
- package/dist/db/tenant-db.d.ts +8 -4
- package/dist/db/tenant-db.js +35 -10
- package/dist/engine/extensions/tenant-data.d.ts +2 -2
- package/dist/engine/projection-helpers.d.ts +3 -3
- package/dist/engine/steps/unsafe-projection-delete.js +3 -2
- package/dist/es-ops/operations-schema.d.ts +1 -1
- package/dist/event-store/archive.d.ts +1 -1
- package/dist/event-store/events-schema.d.ts +1 -1
- package/dist/event-store/rebuild-dead-letter.d.ts +1 -1
- package/dist/event-store/snapshot.d.ts +1 -1
- package/dist/event-store/upcaster-dead-letter.d.ts +1 -1
- package/dist/files/storage-tracking.d.ts +1 -1
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/internal/testing.d.ts +6 -1
- package/dist/internal/testing.js +1 -2
- package/dist/migrations/pending-rebuilds.d.ts +1 -1
- package/dist/migrations/pii-event-backfill.d.ts +1 -1
- package/dist/pipeline/event-consumer-state.d.ts +1 -1
- package/dist/pipeline/projection-state.d.ts +1 -1
- package/package.json +3 -3
- package/src/changes.json +7 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { ExecutorManagedRunner } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
1
2
|
import type { EntityDefinition } from "../engine/types/index.ts";
|
|
2
3
|
import type { StoredEvent } from "../event-store/index.ts";
|
|
3
|
-
import type { DbRow
|
|
4
|
+
import type { DbRow } from "./connection.ts";
|
|
4
5
|
import type { TableColumns } from "./dialect.ts";
|
|
5
6
|
type Table = TableColumns<any>;
|
|
6
7
|
export type AutoVerb = "created" | "updated" | "deleted" | "restored" | "forgotten";
|
|
@@ -17,5 +18,5 @@ export declare function parseAutoVerb(event: StoredEvent): AutoVerb | null;
|
|
|
17
18
|
/** Idempotente Anwendung eines Auto-Events auf die Entity-Tabelle.
|
|
18
19
|
* Wird sowohl beim Live-Append (innerhalb der Write-TX) als auch beim
|
|
19
20
|
* Rebuild (innerhalb der Rebuild-TX) gerufen — identische Logik. */
|
|
20
|
-
export declare function applyEntityEvent(event: StoredEvent, table: Table, entity: EntityDefinition, tx:
|
|
21
|
+
export declare function applyEntityEvent(event: StoredEvent, table: Table, entity: EntityDefinition, tx: ExecutorManagedRunner): Promise<ApplyResult>;
|
|
21
22
|
export {};
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// (DEK erased → Wert unlesbar, bidx NULL). Load-bearing Test:
|
|
15
15
|
// db/__tests__/implicit-projection-equivalence.integration.test.ts.
|
|
16
16
|
//
|
|
17
|
-
// Tenant isolation: applyEntityEvent expects
|
|
17
|
+
// Tenant isolation: applyEntityEvent expects an ExecutorManagedRunner (tx or pool), NOT
|
|
18
18
|
// a TenantDb wrapper. Two sources keep it safe:
|
|
19
19
|
// 1. The live path reads the target row through the tenant-scoped TenantDb
|
|
20
20
|
// before writing — loadById() for update/delete/forget, db.fetchOne() for
|
package/dist/db/dialect.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WritableSchemaTable } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
1
2
|
import { type ColumnHandle, type SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
2
3
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
3
4
|
import type { PgType } from "./entity-table-meta.ts";
|
|
@@ -94,7 +95,7 @@ export type ColumnMap = Record<string, ColumnBuilder<unknown>>;
|
|
|
94
95
|
type IndexOrPk = IndexBuilderWithCols | PrimaryKeyDescriptor;
|
|
95
96
|
export declare function table<TCols extends ColumnMap>(tableName: string, cols: TCols, optsFn?: (t: {
|
|
96
97
|
[K in keyof TCols]: ColumnHandle;
|
|
97
|
-
}) => Record<string, IndexOrPk> | ReadonlyArray<IndexOrPk>):
|
|
98
|
+
}) => Record<string, IndexOrPk> | ReadonlyArray<IndexOrPk>): WritableSchemaTable;
|
|
98
99
|
/** Reads the `kumiko:schema:Name` symbol from a table object.
|
|
99
100
|
* Throws with `context` in the message so callers don't have to duplicate the guard. */
|
|
100
101
|
export declare function extractTableName(table: unknown, context?: string): string;
|
package/dist/db/dialect.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// crashed on timestamptz writes without it (#1480).
|
|
19
19
|
import { KUMIKO_COLUMNS_SYMBOL, KUMIKO_META_SYMBOL, KUMIKO_NAME_SYMBOL, } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
20
20
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
21
|
-
import { schemaTableName } from "./table-introspection.js";
|
|
21
|
+
import { brandWritable, schemaTableName } from "./table-introspection.js";
|
|
22
22
|
function isNumericPgType(t) {
|
|
23
23
|
return t.startsWith("numeric(");
|
|
24
24
|
}
|
|
@@ -376,7 +376,7 @@ export function table(tableName, cols, optsFn) {
|
|
|
376
376
|
[KUMIKO_COLUMNS_SYMBOL]: handles,
|
|
377
377
|
[KUMIKO_META_SYMBOL]: base,
|
|
378
378
|
});
|
|
379
|
-
return out;
|
|
379
|
+
return brandWritable(out);
|
|
380
380
|
}
|
|
381
381
|
/** Reads the `kumiko:schema:Name` symbol from a table object.
|
|
382
382
|
* Throws with `context` in the message so callers don't have to duplicate the guard. */
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { ExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
2
1
|
import type { Registry } from "../engine/index.ts";
|
|
3
2
|
import type { SchemaTable } from "./dialect.ts";
|
|
4
3
|
import { buildEntityTable } from "./table-builder.ts";
|
|
5
|
-
export declare function entityTableFromRegistry(registry: Registry, entityName: string, entity: Parameters<typeof buildEntityTable>[1]): SchemaTable
|
|
4
|
+
export declare function entityTableFromRegistry(registry: Registry, entityName: string, entity: Parameters<typeof buildEntityTable>[1]): SchemaTable;
|
|
@@ -17,8 +17,6 @@ export function entityTableFromRegistry(registry, entityName, entity) {
|
|
|
17
17
|
continue;
|
|
18
18
|
if (projection.source !== entityName)
|
|
19
19
|
continue;
|
|
20
|
-
// @cast-boundary projection-table — ProjectionDefinition.table is the
|
|
21
|
-
// untyped table object every projection registrar accepts.
|
|
22
20
|
return projection.table;
|
|
23
21
|
}
|
|
24
22
|
return buildEntityTable(entityName, entity);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Writable } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
1
2
|
import type { EntityTenancy, TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
|
|
2
3
|
import type { EntityDefinition } from "../engine/types/index.ts";
|
|
3
4
|
import type { BuildEntityTableMetaOptions, EntityTableMeta, UnmanagedTableInput } from "./entity-table-meta-types.ts";
|
|
@@ -12,6 +13,9 @@ export declare function resolveTableName(entityName: string, entity: EntityDefin
|
|
|
12
13
|
* Named `derive*` (not `build*Meta`) so it is not mistaken for the unmanaged
|
|
13
14
|
* escape hatch (#1208).
|
|
14
15
|
*/
|
|
16
|
+
export declare function deriveEntityTableMeta(entityName: string, entity: EntityDefinition, options: BuildEntityTableMetaOptions & {
|
|
17
|
+
readonly source: "unmanaged";
|
|
18
|
+
}): EntityTableMeta & Writable;
|
|
15
19
|
export declare function deriveEntityTableMeta(entityName: string, entity: EntityDefinition, options?: BuildEntityTableMetaOptions): EntityTableMeta;
|
|
16
20
|
export declare function assertBackingTableSuperset(entityName: string, fieldMeta: EntityTableMeta, tableMeta: EntityTableMeta): void;
|
|
17
21
|
/**
|
|
@@ -25,4 +29,4 @@ export declare function assertBackingTableSuperset(entityName: string, fieldMeta
|
|
|
25
29
|
*/
|
|
26
30
|
export declare function defineUnmanagedTable<const T extends EntityTenancy = "tenant">(input: UnmanagedTableInput & {
|
|
27
31
|
readonly tenancy?: T;
|
|
28
|
-
}): EntityTableMeta & TenancyBrand<T
|
|
32
|
+
}): EntityTableMeta & TenancyBrand<T> & Writable;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { collectPiiEncryptedFields, isRecordKeyedField } from "../crypto/index.js";
|
|
19
19
|
import { isSqlExpression } from "./dialect.js";
|
|
20
20
|
import { READ_MODEL_PREFIX, SOFT_DELETE_LIVE_ROW_PREDICATE, toSnakeCase, toTableName, } from "./table-builder.js";
|
|
21
|
+
import { brandWritable } from "./table-introspection.js";
|
|
21
22
|
// Standard base columns for event-sourced read-model tables. Mirrors
|
|
22
23
|
// `buildBaseColumns()` from table-builder.ts (drizzle variant).
|
|
23
24
|
function fullBaseColumns(idType, softDelete) {
|
|
@@ -206,15 +207,6 @@ export function resolveTableName(entityName, entity, featureName) {
|
|
|
206
207
|
}
|
|
207
208
|
return `${featureName}_${baseName}`;
|
|
208
209
|
}
|
|
209
|
-
/**
|
|
210
|
-
* Derive EntityTableMeta from an EntityDefinition (base columns + field DDL).
|
|
211
|
-
* Defaults to `source: "managed"` (rebuildable projection). For direct-write
|
|
212
|
-
* stores pass `{ source: "unmanaged" }` and use a non-`read_` table name
|
|
213
|
-
* (convention: `store_*`) — or build columns by hand with `defineUnmanagedTable`.
|
|
214
|
-
*
|
|
215
|
-
* Named `derive*` (not `build*Meta`) so it is not mistaken for the unmanaged
|
|
216
|
-
* escape hatch (#1208).
|
|
217
|
-
*/
|
|
218
210
|
export function deriveEntityTableMeta(entityName, entity, options) {
|
|
219
211
|
const tableName = resolveTableName(entityName, entity, options?.featureName);
|
|
220
212
|
const source = options?.source ?? "managed";
|
|
@@ -345,7 +337,7 @@ export function deriveEntityTableMeta(entityName, entity, options) {
|
|
|
345
337
|
}
|
|
346
338
|
}
|
|
347
339
|
const piiSubjectFields = collectPiiEncryptedFields(entity);
|
|
348
|
-
|
|
340
|
+
const meta = {
|
|
349
341
|
tableName,
|
|
350
342
|
columns,
|
|
351
343
|
indexes,
|
|
@@ -353,6 +345,7 @@ export function deriveEntityTableMeta(entityName, entity, options) {
|
|
|
353
345
|
...(piiSubjectFields.length > 0 && { piiSubjectFields }),
|
|
354
346
|
...(entity.tenancy === "global" && { tenancy: "global" }),
|
|
355
347
|
};
|
|
348
|
+
return source === "unmanaged" ? brandWritable(meta) : meta;
|
|
356
349
|
}
|
|
357
350
|
function sqlExpressionText(where) {
|
|
358
351
|
return isSqlExpression(where) ? where.text : undefined;
|
|
@@ -405,7 +398,7 @@ export function defineUnmanagedTable(input) {
|
|
|
405
398
|
"have a tenant_id column — a global table's rows carry the system tenant, not a per-row " +
|
|
406
399
|
"tenant identity.");
|
|
407
400
|
}
|
|
408
|
-
return {
|
|
401
|
+
return brandWritable({
|
|
409
402
|
tableName: input.tableName,
|
|
410
403
|
columns: input.columns,
|
|
411
404
|
indexes: input.indexes ?? [],
|
|
@@ -415,7 +408,7 @@ export function defineUnmanagedTable(input) {
|
|
|
415
408
|
source: "unmanaged",
|
|
416
409
|
...(input.tenancy === "global" && { tenancy: "global" }),
|
|
417
410
|
// @cast-boundary phantom-brand — TenancyBrand<T> is a phantom marker with no runtime representation.
|
|
418
|
-
};
|
|
411
|
+
});
|
|
419
412
|
}
|
|
420
413
|
function assertUnmanagedTableName(tableName, via) {
|
|
421
414
|
if (tableName.startsWith("read_")) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { ExecutorManagedWriteCapability, Writable } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
1
2
|
import { AccessDeniedError } from "../errors/index.ts";
|
|
2
3
|
export declare function markExecutorManagedTable(table: object): void;
|
|
3
4
|
export declare function isExecutorManagedTable(table: unknown): boolean;
|
|
4
5
|
export declare function registerExecutorManagedWriteRunner(runner: object): void;
|
|
5
|
-
export declare function hasExecutorManagedWriteCapability(runner: unknown):
|
|
6
|
+
export declare function hasExecutorManagedWriteCapability(runner: unknown): runner is ExecutorManagedWriteCapability;
|
|
6
7
|
export declare function executorManagedWriteDenied(runner: unknown, table: unknown, operation: string): AccessDeniedError | undefined;
|
|
8
|
+
export declare function assertWritableTable<T extends object>(table: T): asserts table is T & Writable;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccessDeniedError } from "../errors/index.js";
|
|
1
|
+
import { AccessDeniedError, InternalError } from "../errors/index.js";
|
|
2
2
|
import { asEntityTableMeta, tableNameOf } from "./table-introspection.js";
|
|
3
3
|
const executorManagedTables = new WeakSet();
|
|
4
4
|
const executorManagedWriteRunners = new WeakSet();
|
|
@@ -38,3 +38,11 @@ export function executorManagedWriteDenied(runner, table, operation) {
|
|
|
38
38
|
"projection apply; a direct write drifts from the event stream and a rebuild drops or resurrects the row.",
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
// Unmanaged tables are the Writable ones; callers invoke this only after executorManagedWriteDenied passed without a capable runner.
|
|
42
|
+
export function assertWritableTable(table) {
|
|
43
|
+
if (isExecutorManagedTable(table)) {
|
|
44
|
+
throw new InternalError({
|
|
45
|
+
message: `assertWritableTable: "${tableNameOf(table)}" is executor-managed and the runner holds no write capability.`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/db/index.d.ts
CHANGED
|
@@ -36,6 +36,6 @@ export { renderTableDdl, renderTablesDdl } from "./render-ddl.ts";
|
|
|
36
36
|
export { diffReplayAgainstSnapshot, type ReplayedSchema, type ReplayedTable, type ReplayMismatch, replayMigrationsDir, } from "./replay-migration-sql.ts";
|
|
37
37
|
export { findCommentedDropTables, isRetiredFrameworkTable, type RetiredFrameworkTable, retiredFrameworkTables, } from "./retired-framework-tables.ts";
|
|
38
38
|
export { tableExists } from "./schema-inspection.ts";
|
|
39
|
-
export { buildBaseColumns, buildEntityTable, declareGlobalTenancy, type EntityRow, type EntityTable, physicalColumnName, toSnakeCase, toTableName, } from "./table-builder.ts";
|
|
40
|
-
export type { CreateTenantDbOptions, OutsideTransactionContext, TenantDb, TenantDbGrants, TenantDbMode, UncheckedSystemDb, } from "./tenant-db.ts";
|
|
39
|
+
export { buildBaseColumns, buildEntityTable, declareGlobalTenancy, type EntityRow, type EntityTable, type ExecutorManagedRunner, type ExecutorManagedWriteCapability, physicalColumnName, toSnakeCase, toTableName, type Writable, type WritableSchemaTable, type WritableTable, } from "./table-builder.ts";
|
|
40
|
+
export type { CreateTenantDbOptions, ExecutorManagedTenantDb, OutsideTransactionContext, TenantDb, TenantDbGrants, TenantDbMode, UncheckedSystemDb, } from "./tenant-db.ts";
|
|
41
41
|
export { castTenantRows, createSystemDbView, createTenantDataDestroyDb, createTenantDb, hasOutsideTransactionDb, requireOutsideTransactionDb, runInOwnTransaction, SYSTEM_SCOPE_CHECK_BRAND, scopeTenantDbToTenant, } from "./tenant-db.ts";
|
package/dist/db/query.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AggregateRow, AggregateSpec } from "@cosmicdrift/kumiko-types/aggregate-types";
|
|
2
2
|
import type { DbRunner, DbTx } from "@cosmicdrift/kumiko-types/db-connection";
|
|
3
|
+
import type { ExecutorManagedRunner, ExecutorManagedWriteCapability, WritableTable } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
3
4
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
5
|
import type { SelectOptions, WhereObject } from "@cosmicdrift/kumiko-types/where-clause-types";
|
|
5
6
|
import type { EntityTableMeta } from "./entity-table-meta.ts";
|
|
6
|
-
import { type NotExecutorOnly } from "./table-builder.ts";
|
|
7
7
|
import { asEntityTableMeta, requireEntityTableMeta } from "./table-introspection.ts";
|
|
8
8
|
export { asEntityTableMeta, requireEntityTableMeta };
|
|
9
9
|
export type ReservedClient = {
|
|
@@ -23,12 +23,14 @@ export type RawClientWithBegin = RawClient & {
|
|
|
23
23
|
export declare function hasBegin(client: RawClient): client is RawClientWithBegin;
|
|
24
24
|
export declare function asRawClient(db: unknown): RawClient;
|
|
25
25
|
export declare function runnerCanBeginTransaction(db: unknown): boolean;
|
|
26
|
+
export declare function runInSavepoint<T>(tx: ExecutorManagedRunner, fn: (sp: ExecutorManagedRunner) => Promise<T>): Promise<T>;
|
|
26
27
|
export declare function runInSavepoint<T>(tx: unknown, fn: (sp: unknown) => Promise<T>): Promise<T>;
|
|
28
|
+
export declare function runInSavepointIfSupported<T>(db: ExecutorManagedRunner, fn: (sp: ExecutorManagedRunner) => Promise<T>): Promise<T>;
|
|
27
29
|
export declare function runInSavepointIfSupported<T>(db: unknown, fn: (sp: DbRunner) => Promise<T>): Promise<T>;
|
|
28
30
|
export type AnyDb = DbRunner | unknown;
|
|
31
|
+
type AnyRow = any;
|
|
29
32
|
export type { JsonTextMatch, OrderByClause, SelectOptions, WhereObject, WhereOperator, WhereValue, } from "@cosmicdrift/kumiko-types/where-clause-types";
|
|
30
33
|
type TableLike = EntityTableMeta | any;
|
|
31
|
-
type WritableTable = EntityTableMeta & NotExecutorOnly;
|
|
32
34
|
export type TableInfo = {
|
|
33
35
|
readonly name: string;
|
|
34
36
|
readonly columnOf: (field: string) => string;
|
|
@@ -43,8 +45,8 @@ export declare function instantToTimestamptzParam(instant: Temporal.Instant): st
|
|
|
43
45
|
export declare function isClosedConnectionError(err: unknown): boolean;
|
|
44
46
|
export declare function describeClosedConnectionFailure(raw: unknown, err: unknown, attempts: number): Record<string, unknown> | undefined;
|
|
45
47
|
export declare function unsafeReadRetrying<TRow>(db: AnyDb, sqlText: string, params: readonly unknown[]): Promise<readonly TRow[]>;
|
|
46
|
-
export declare function selectMany<TRow =
|
|
47
|
-
export declare function fetchOne<TRow =
|
|
48
|
+
export declare function selectMany<TRow = AnyRow>(db: AnyDb, table: TableLike, where?: WhereObject, options?: SelectOptions): Promise<readonly TRow[]>;
|
|
49
|
+
export declare function fetchOne<TRow = AnyRow>(db: AnyDb, table: TableLike, where: WhereObject): Promise<TRow | undefined>;
|
|
48
50
|
export type JoinColumnPair = {
|
|
49
51
|
readonly left: string;
|
|
50
52
|
readonly right: string;
|
|
@@ -65,10 +67,14 @@ export type InnerJoinRow<TLeft, TRight> = {
|
|
|
65
67
|
readonly right: TRight;
|
|
66
68
|
};
|
|
67
69
|
export declare function selectInnerJoin<TLeft = Record<string, unknown>, TRight = Record<string, unknown>>(db: AnyDb, spec: InnerJoinSpec): Promise<readonly InnerJoinRow<TLeft, TRight>[]>;
|
|
68
|
-
export declare function insertMany<TRow =
|
|
69
|
-
export declare function
|
|
70
|
-
export declare function
|
|
70
|
+
export declare function insertMany<TRow = AnyRow>(db: AnyDb, table: WritableTable, rows: ReadonlyArray<Record<string, unknown>>): Promise<readonly TRow[]>;
|
|
71
|
+
export declare function insertMany<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, rows: ReadonlyArray<Record<string, unknown>>): Promise<readonly TRow[]>;
|
|
72
|
+
export declare function insertOne<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>): Promise<TRow | undefined>;
|
|
73
|
+
export declare function insertOne<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>): Promise<TRow | undefined>;
|
|
74
|
+
export declare function updateMany<TRow = AnyRow>(db: AnyDb, table: WritableTable, set: Record<string, unknown>, where: WhereObject): Promise<readonly TRow[]>;
|
|
75
|
+
export declare function updateMany<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, set: Record<string, unknown>, where: WhereObject): Promise<readonly TRow[]>;
|
|
71
76
|
export declare function deleteMany(db: AnyDb, table: WritableTable, where: WhereObject): Promise<number>;
|
|
77
|
+
export declare function deleteMany(db: ExecutorManagedWriteCapability, table: EntityTableMeta, where: WhereObject): Promise<number>;
|
|
72
78
|
export declare function resolveConflictColumns(table: TableLike, info: TableInfo, conflictKeys: readonly string[] | undefined): readonly string[];
|
|
73
79
|
export type UpsertOnConflictOptions = {
|
|
74
80
|
readonly conflictKeys: readonly string[];
|
|
@@ -88,18 +94,23 @@ export type DeleteManyBatchedResult = {
|
|
|
88
94
|
export declare function countWhere(db: AnyDb, table: TableLike, where?: WhereObject): Promise<number>;
|
|
89
95
|
export declare function isTimestamptzType(pgType: string | undefined): boolean;
|
|
90
96
|
export declare function aggregateWhere(db: AnyDb, table: TableLike, spec: AggregateSpec, where?: WhereObject): Promise<readonly AggregateRow[]>;
|
|
91
|
-
export declare function upsertOnConflict<TRow =
|
|
92
|
-
export declare function
|
|
97
|
+
export declare function upsertOnConflict<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, options: UpsertOnConflictOptions): Promise<TRow | undefined>;
|
|
98
|
+
export declare function upsertOnConflict<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>, options: UpsertOnConflictOptions): Promise<TRow | undefined>;
|
|
99
|
+
export declare function upsertByPk<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, updateOnConflict?: Record<string, unknown>): Promise<TRow | undefined>;
|
|
100
|
+
export declare function upsertByPk<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>, updateOnConflict?: Record<string, unknown>): Promise<TRow | undefined>;
|
|
93
101
|
export type InsertOnConflictDoNothingOptions = {
|
|
94
102
|
readonly conflictKeys?: readonly string[];
|
|
95
103
|
};
|
|
96
|
-
export declare function insertOnConflictDoNothing<TRow =
|
|
104
|
+
export declare function insertOnConflictDoNothing<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, options?: InsertOnConflictDoNothingOptions): Promise<TRow | undefined>;
|
|
105
|
+
export declare function insertOnConflictDoNothing<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>, options?: InsertOnConflictDoNothingOptions): Promise<TRow | undefined>;
|
|
97
106
|
export type IncrementCounterOptions = {
|
|
98
107
|
readonly conflictKeys?: readonly string[];
|
|
99
108
|
/** Extra SET clauses on conflict (e.g. lastUpdatedAt: sql\`NOW()\`). */
|
|
100
109
|
readonly set?: Record<string, unknown>;
|
|
101
110
|
};
|
|
102
|
-
export declare function incrementCounter<TRow =
|
|
111
|
+
export declare function incrementCounter<TRow = AnyRow>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, increments: Record<string, number>, options?: IncrementCounterOptions): Promise<TRow | undefined>;
|
|
112
|
+
export declare function incrementCounter<TRow = AnyRow>(db: ExecutorManagedWriteCapability, table: EntityTableMeta, values: Record<string, unknown>, increments: Record<string, number>, options?: IncrementCounterOptions): Promise<TRow | undefined>;
|
|
103
113
|
export declare function deleteManyBatched(db: AnyDb, table: WritableTable, where: WhereObject, options: DeleteManyBatchedOptions): Promise<DeleteManyBatchedResult>;
|
|
114
|
+
export declare function deleteManyBatched(db: ExecutorManagedWriteCapability, table: EntityTableMeta, where: WhereObject, options: DeleteManyBatchedOptions): Promise<DeleteManyBatchedResult>;
|
|
104
115
|
export declare function transaction<T>(db: AnyDb, fn: (tx: DbTx) => Promise<T>): Promise<T>;
|
|
105
116
|
export declare function runInNewTransaction<T>(db: unknown, fn: (tx: DbRunner) => Promise<T>): Promise<T>;
|
package/dist/db/query.js
CHANGED
|
@@ -78,42 +78,27 @@ export function asRawClient(db) {
|
|
|
78
78
|
export function runnerCanBeginTransaction(db) {
|
|
79
79
|
return hasBegin(asRawClient(db));
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// the savepoint-scoped tx; run every confined statement on THAT handle.
|
|
81
|
+
function savepointRunnerOf(sp) {
|
|
82
|
+
// @cast-boundary db-runner savepoint handle → TRunner at the single seam (boundRunner re-wraps savepoint handles with the caller's capability)
|
|
83
|
+
return sp;
|
|
84
|
+
}
|
|
86
85
|
export async function runInSavepoint(tx, fn) {
|
|
87
86
|
const raw = asRawClient(tx);
|
|
88
87
|
if (typeof raw.savepoint !== "function") {
|
|
89
88
|
throw new Error("runInSavepoint: transaction handle has no savepoint() — pass the driver tx (postgres.js TransactionSql).");
|
|
90
89
|
}
|
|
91
|
-
return raw.savepoint(fn);
|
|
90
|
+
return raw.savepoint((sp) => fn(savepointRunnerOf(sp)));
|
|
92
91
|
}
|
|
93
|
-
// Same error-confinement as runInSavepoint, but for call sites that don't
|
|
94
|
-
// know whether `db` is a bare pool connection or an active transaction
|
|
95
|
-
// (e.g. the CRUD executor, invoked both from a dispatcher tx and directly
|
|
96
|
-
// against the pool by seeds/tests). A pool connection has no savepoint() —
|
|
97
|
-
// and doesn't need one, since each statement there is its own auto-committed
|
|
98
|
-
// unit and a failed statement can't poison anything downstream.
|
|
99
|
-
//
|
|
100
|
-
// `.savepoint` being present isn't proof the transaction is still open: a
|
|
101
|
-
// committed TransactionSql-shaped db still exposes `.savepoint`, and calling
|
|
102
|
-
// it fails with PG 25P01 ("no active sql transaction") since there's no
|
|
103
|
-
// BEGIN left to nest into. That is now a programming error, not something
|
|
104
|
-
// to paper over by re-running `fn` without the savepoint's isolation.
|
|
105
92
|
export async function runInSavepointIfSupported(db, fn) {
|
|
106
93
|
const raw = asRawClient(db);
|
|
107
|
-
// @cast-boundary db-runner savepoint handle → DbRunner at the single seam
|
|
108
|
-
const asRunner = (sp) => sp;
|
|
109
94
|
if (typeof raw.savepoint !== "function")
|
|
110
|
-
return fn(
|
|
95
|
+
return fn(savepointRunnerOf(db));
|
|
111
96
|
// An error from inside fn must never be remapped, even if it carries a closed-connection code.
|
|
112
97
|
let fnStarted = false;
|
|
113
98
|
try {
|
|
114
99
|
return await raw.savepoint((sp) => {
|
|
115
100
|
fnStarted = true;
|
|
116
|
-
return fn(
|
|
101
|
+
return fn(savepointRunnerOf(sp));
|
|
117
102
|
});
|
|
118
103
|
}
|
|
119
104
|
catch (e) {
|
|
@@ -870,7 +855,6 @@ function selectColumnList(info, fields) {
|
|
|
870
855
|
})
|
|
871
856
|
.join(", ");
|
|
872
857
|
}
|
|
873
|
-
// biome-ignore lint/suspicious/noExplicitAny: opt-in default loosens row type for unannotated test fixtures
|
|
874
858
|
export async function selectMany(db, table, where, options) {
|
|
875
859
|
const scoped = tenantDbDelegate(db);
|
|
876
860
|
if (scoped) {
|
|
@@ -905,7 +889,6 @@ export async function selectMany(db, table, where, options) {
|
|
|
905
889
|
const raw = await unsafeReadRetrying(db, sqlText, values);
|
|
906
890
|
return coerceRows(raw, info);
|
|
907
891
|
}
|
|
908
|
-
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
909
892
|
export async function fetchOne(db, table, where) {
|
|
910
893
|
const rows = await selectMany(db, table, where, { limit: 1 });
|
|
911
894
|
return rows[0];
|
|
@@ -980,10 +963,6 @@ export async function selectInnerJoin(db, spec) {
|
|
|
980
963
|
};
|
|
981
964
|
});
|
|
982
965
|
}
|
|
983
|
-
// Bulk INSERT — same shape as insertOne but takes an array of rows and
|
|
984
|
-
// produces one multi-VALUES statement. Mirrors drizzle's
|
|
985
|
-
// `db.insert(t).values(rows[])`. Empty input is a no-op.
|
|
986
|
-
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
987
966
|
export async function insertMany(db, table, rows) {
|
|
988
967
|
if (rows.length === 0)
|
|
989
968
|
return [];
|
|
@@ -1026,7 +1005,6 @@ export async function insertMany(db, table, rows) {
|
|
|
1026
1005
|
});
|
|
1027
1006
|
return coerceRows(raw, info);
|
|
1028
1007
|
}
|
|
1029
|
-
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
1030
1008
|
export async function insertOne(db, table, values) {
|
|
1031
1009
|
const scoped = tenantDbDelegate(db);
|
|
1032
1010
|
if (scoped) {
|
|
@@ -1065,7 +1043,6 @@ export async function insertOne(db, table, values) {
|
|
|
1065
1043
|
return undefined;
|
|
1066
1044
|
return coerceRow(first, info);
|
|
1067
1045
|
}
|
|
1068
|
-
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
1069
1046
|
export async function updateMany(db, table, set, where) {
|
|
1070
1047
|
const scoped = tenantDbDelegate(db);
|
|
1071
1048
|
if (scoped) {
|
|
@@ -1290,8 +1267,11 @@ export async function aggregateWhere(db, table, spec, where = {}) {
|
|
|
1290
1267
|
};
|
|
1291
1268
|
});
|
|
1292
1269
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1270
|
+
export function upsertOnConflict(db, table, values, options) {
|
|
1271
|
+
return upsertOnConflictChecked(db, table, values, options);
|
|
1272
|
+
}
|
|
1273
|
+
// upsertByPk shares this body; the checks inside gate the write, so it needs no overload pair.
|
|
1274
|
+
async function upsertOnConflictChecked(db, table, values, options) {
|
|
1295
1275
|
assertNotTenantScoped(db, "upsertOnConflict");
|
|
1296
1276
|
const managedDenied = executorManagedWriteDenied(db, table, "upsertOnConflict");
|
|
1297
1277
|
if (managedDenied)
|
|
@@ -1337,7 +1317,6 @@ export async function upsertOnConflict(db, table, values, options) {
|
|
|
1337
1317
|
return undefined;
|
|
1338
1318
|
return coerceRow(first, info);
|
|
1339
1319
|
}
|
|
1340
|
-
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
1341
1320
|
export async function upsertByPk(db, table, values, updateOnConflict) {
|
|
1342
1321
|
const managedDenied = executorManagedWriteDenied(db, table, "upsertByPk");
|
|
1343
1322
|
if (managedDenied)
|
|
@@ -1345,12 +1324,11 @@ export async function upsertByPk(db, table, values, updateOnConflict) {
|
|
|
1345
1324
|
const info = extractTableInfo(table);
|
|
1346
1325
|
const conflictKeys = resolveConflictColumns(table, info, undefined);
|
|
1347
1326
|
const fieldKeys = conflictKeys.map((col) => info.fieldOf(col));
|
|
1348
|
-
return
|
|
1327
|
+
return upsertOnConflictChecked(db, table, values, {
|
|
1349
1328
|
conflictKeys: fieldKeys,
|
|
1350
1329
|
...(updateOnConflict !== undefined ? { update: updateOnConflict } : {}),
|
|
1351
1330
|
});
|
|
1352
1331
|
}
|
|
1353
|
-
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
1354
1332
|
export async function insertOnConflictDoNothing(db, table, values, options) {
|
|
1355
1333
|
assertNotTenantScoped(db, "insertOnConflictDoNothing");
|
|
1356
1334
|
const managedDenied = executorManagedWriteDenied(db, table, "insertOnConflictDoNothing");
|
|
@@ -1371,7 +1349,6 @@ export async function insertOnConflictDoNothing(db, table, values, options) {
|
|
|
1371
1349
|
return undefined;
|
|
1372
1350
|
return coerceRow(first, info);
|
|
1373
1351
|
}
|
|
1374
|
-
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
1375
1352
|
export async function incrementCounter(db, table, values, increments, options = {}) {
|
|
1376
1353
|
assertNotTenantScoped(db, "incrementCounter");
|
|
1377
1354
|
const managedDenied = executorManagedWriteDenied(db, table, "incrementCounter");
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
|
-
import type { ExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
3
2
|
import type { TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
|
|
4
3
|
import type { EntityDefinition, EntityRelations, FieldDefinition, FieldsMap } from "../engine/types/index.ts";
|
|
5
4
|
import type { Table } from "./event-store-executor-context.ts";
|
|
@@ -194,11 +193,11 @@ type Flatten<T> = {
|
|
|
194
193
|
/** The row `executor.list()` / `executor.detail()` return for entity `E`. A wide
|
|
195
194
|
* `EntityDefinition` (fields without literal keys) yields `Record<string, unknown>`. */
|
|
196
195
|
export type EntityRow<E extends EntityDefinition = EntityDefinition> = string extends keyof E["fields"] ? Record<string, unknown> : Flatten<RowOfColumns<BaseColumnsType<E>> & SoftDeleteRow<E> & RowForFields<E["fields"]>>;
|
|
197
|
-
export type {
|
|
196
|
+
export type { ExecutorManagedRunner, ExecutorManagedWriteCapability, Writable, WritableSchemaTable, WritableTable, } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
198
197
|
export type { EntityTenancy, TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
|
|
199
198
|
type EntityTableTenancy<E extends EntityDefinition> = [NonNullable<E["tenancy"]>] extends [never] ? "tenant" : [NonNullable<E["tenancy"]>] extends ["global"] ? "global" : "tenant";
|
|
200
199
|
export type EntityTable<E extends EntityDefinition = EntityDefinition> = TableColumns<// biome-ignore lint/suspicious/noExplicitAny: drizzle's internal table-config stays generic; we layer typed columns on top via the intersection below.
|
|
201
|
-
any> & BaseColumnsType<E> & SoftDeleteColumnsType & ColumnsForEntity<E["fields"]> &
|
|
200
|
+
any> & BaseColumnsType<E> & SoftDeleteColumnsType & ColumnsForEntity<E["fields"]> & TenancyBrand<EntityTableTenancy<E>>;
|
|
202
201
|
export declare function buildBaseColumns(softDelete: boolean, idType?: "serial" | "uuid"): {
|
|
203
202
|
id: ColumnBuilder<string> | ColumnBuilder<number>;
|
|
204
203
|
tenantId: ColumnBuilder<string>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EntityTableMeta } from "@cosmicdrift/kumiko-types/entity-table-meta-types";
|
|
2
|
+
import type { Writable } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
2
3
|
export declare function isEntityTableMeta(value: unknown): value is EntityTableMeta;
|
|
3
4
|
export declare function asEntityTableMeta(table: unknown): EntityTableMeta | undefined;
|
|
4
5
|
export declare function requireEntityTableMeta(table: unknown, tableName: string): EntityTableMeta;
|
|
@@ -14,3 +15,4 @@ export declare function columnHandleName(table: unknown, field: string): string
|
|
|
14
15
|
export declare function hasFieldProperty(table: unknown, field: string): boolean;
|
|
15
16
|
/** SQL column name of a JS field via the column-handle map; undefined when not mapped. */
|
|
16
17
|
export declare function columnSqlNameOf(table: unknown, field: string): string | undefined;
|
|
18
|
+
export declare function brandWritable<T extends EntityTableMeta>(table: T): T & Writable;
|
|
@@ -74,3 +74,7 @@ export function columnSqlNameOf(table, field) {
|
|
|
74
74
|
const name = handle["name"];
|
|
75
75
|
return typeof name === "string" ? name : undefined;
|
|
76
76
|
}
|
|
77
|
+
export function brandWritable(table) {
|
|
78
|
+
// @cast-boundary phantom-brand — Writable is a phantom marker with no runtime representation.
|
|
79
|
+
return table;
|
|
80
|
+
}
|
package/dist/db/tenant-db.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { EntityTableMeta } from "@cosmicdrift/kumiko-types/entity-table-meta-types";
|
|
2
|
+
import type { ExecutorManagedRunner } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
2
3
|
import type { EscapeHatchDeclaration, EscapeHatchReporter, EscapeHatchTarget } from "@cosmicdrift/kumiko-types/handlers";
|
|
3
4
|
import type { SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
4
|
-
import { type TenantDb, type TenantDbMode, type UncheckedSystemDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
5
|
+
import { type ExecutorManagedTenantDb, type TenantDb, type TenantDbMode, type UncheckedSystemDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
5
6
|
import { asRawClient } from "../db/query.ts";
|
|
6
7
|
import type { EntityDefinition } from "../engine/types/fields.ts";
|
|
7
8
|
import { type TenantId } from "../engine/types/identifiers.ts";
|
|
@@ -10,8 +11,8 @@ import { type Meter, type Tracer } from "../observability/index.ts";
|
|
|
10
11
|
import type { DbRunner } from "./connection.ts";
|
|
11
12
|
import { tableNameOf } from "./table-introspection.ts";
|
|
12
13
|
type Table = SchemaTable;
|
|
13
|
-
export { SYSTEM_SCOPE_CHECK_BRAND, type TenantDb, type TenantDbMode, type UncheckedSystemDb, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
14
|
-
export declare function withExecutorManagedWrites(runner: DbRunner):
|
|
14
|
+
export { type ExecutorManagedTenantDb, SYSTEM_SCOPE_CHECK_BRAND, type TenantDb, type TenantDbMode, type UncheckedSystemDb, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
15
|
+
export declare function withExecutorManagedWrites(runner: DbRunner): ExecutorManagedRunner;
|
|
15
16
|
export declare function withoutExecutorManagedWrites(runner: DbRunner): DbRunner;
|
|
16
17
|
export declare function assertPersonalDataWrite(db: TenantDb, tableName: string, keys: readonly string[], entity: EntityDefinition): void;
|
|
17
18
|
export declare function unsafeRawForDeclaredStep(holder: TenantDb | UncheckedSystemDb, reason: string): DbRunner;
|
|
@@ -55,7 +56,10 @@ export type CreateTenantDbOptions = {
|
|
|
55
56
|
readonly signal?: AbortSignal;
|
|
56
57
|
readonly grants?: TenantDbGrants;
|
|
57
58
|
};
|
|
59
|
+
export declare function createTenantDb(options: CreateTenantDbOptions & {
|
|
60
|
+
readonly db: ExecutorManagedRunner;
|
|
61
|
+
}): ExecutorManagedTenantDb;
|
|
58
62
|
export declare function createTenantDb(options: CreateTenantDbOptions): TenantDb;
|
|
59
|
-
export declare function createTenantDataDestroyDb(options: Omit<CreateTenantDbOptions, "mode">):
|
|
63
|
+
export declare function createTenantDataDestroyDb(options: Omit<CreateTenantDbOptions, "mode">): ExecutorManagedTenantDb;
|
|
60
64
|
export declare function createProjectingTenantDb(db: DbRunner, tenantId: TenantId, projectionRegistry: Registry | undefined): TenantDb;
|
|
61
65
|
export { asRawClient };
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -4,7 +4,7 @@ import { asEntityTableMeta, asRawClient, aggregateWhere as bunAggregateWhere, co
|
|
|
4
4
|
import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
|
|
5
5
|
import { AccessDeniedError, InternalError, memberResolutionReadOnlyDenied, } from "../errors/index.js";
|
|
6
6
|
import { emitDbQuery, fallbackEscapeHatchReporter, registerStandardMetrics, } from "../observability/index.js";
|
|
7
|
-
import { executorManagedWriteDenied, registerExecutorManagedWriteRunner, } from "./executor-managed-tables.js";
|
|
7
|
+
import { assertWritableTable, executorManagedWriteDenied, hasExecutorManagedWriteCapability, registerExecutorManagedWriteRunner, } from "./executor-managed-tables.js";
|
|
8
8
|
import { bindProjectionRegistry, projectionRegistryOf } from "./projection-registry-binding.js";
|
|
9
9
|
import { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock.js";
|
|
10
10
|
import { queryDatabaseNow } from "./queries/database-now.js";
|
|
@@ -73,12 +73,17 @@ const capableRunnerByInner = new WeakMap();
|
|
|
73
73
|
// Framework-private (not re-exported from db/index.ts): the only way to obtain a runner that may
|
|
74
74
|
// write executor-managed tables. Only the framework's own event-apply paths call it.
|
|
75
75
|
export function withExecutorManagedWrites(runner) {
|
|
76
|
-
if (
|
|
76
|
+
if (hasExecutorManagedWriteCapability(runner))
|
|
77
77
|
return runner;
|
|
78
78
|
const cached = capableRunnerByInner.get(runner);
|
|
79
79
|
if (cached)
|
|
80
80
|
return cached;
|
|
81
81
|
const capable = boundRunner(runner, runnerPersonalDataGates.get(runner), projectionRegistryOf(runner), true);
|
|
82
|
+
if (!hasExecutorManagedWriteCapability(capable)) {
|
|
83
|
+
throw new InternalError({
|
|
84
|
+
message: "boundRunner did not register the executor write capability",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
82
87
|
capableRunnerByInner.set(runner, capable);
|
|
83
88
|
return capable;
|
|
84
89
|
}
|
|
@@ -353,7 +358,7 @@ export function createTenantDb(options) {
|
|
|
353
358
|
}
|
|
354
359
|
// Tenant-destroy hooks purge their own tenant's event-fed rows; the pinned tenant is the only reach and unsafeRaw stays without the write capability.
|
|
355
360
|
export function createTenantDataDestroyDb(options) {
|
|
356
|
-
return
|
|
361
|
+
return createTenantDb({ ...options, db: withExecutorManagedWrites(options.db), mode: "tenant" });
|
|
357
362
|
}
|
|
358
363
|
// Untyped JS callers or an `as` cast can still hand in undefined; a scoped handle
|
|
359
364
|
// without a tenant would bind `tenant_id = NULL` instead of failing loudly.
|
|
@@ -488,6 +493,26 @@ function buildTenantDb(options) {
|
|
|
488
493
|
"use db.global(table) with escapeHatch instead.",
|
|
489
494
|
});
|
|
490
495
|
}
|
|
496
|
+
// Callers ran executorManagedWriteDenied first: either db holds the capability or the table is
|
|
497
|
+
// unmanaged. The split only proves to the compiler which overload of the free writers applies.
|
|
498
|
+
function insertRow(table, values) {
|
|
499
|
+
if (hasExecutorManagedWriteCapability(db))
|
|
500
|
+
return bunInsertOne(db, table, values);
|
|
501
|
+
assertWritableTable(table);
|
|
502
|
+
return bunInsertOne(db, table, values);
|
|
503
|
+
}
|
|
504
|
+
function updateRows(table, set, where) {
|
|
505
|
+
if (hasExecutorManagedWriteCapability(db))
|
|
506
|
+
return bunUpdateMany(db, table, set, where);
|
|
507
|
+
assertWritableTable(table);
|
|
508
|
+
return bunUpdateMany(db, table, set, where);
|
|
509
|
+
}
|
|
510
|
+
function deleteRows(table, where) {
|
|
511
|
+
if (hasExecutorManagedWriteCapability(db))
|
|
512
|
+
return bunDeleteMany(db, table, where);
|
|
513
|
+
assertWritableTable(table);
|
|
514
|
+
return bunDeleteMany(db, table, where);
|
|
515
|
+
}
|
|
491
516
|
function globalTable(table) {
|
|
492
517
|
const meta = asEntityTableMeta(table);
|
|
493
518
|
if (meta?.tenancy !== "global") {
|
|
@@ -511,7 +536,7 @@ function buildTenantDb(options) {
|
|
|
511
536
|
if (denied)
|
|
512
537
|
return Promise.reject(denied);
|
|
513
538
|
report("global-write", globalWriteReason());
|
|
514
|
-
return withDbSpan("insert", table, async () =>
|
|
539
|
+
return withDbSpan("insert", table, async () => insertRow(table, values));
|
|
515
540
|
},
|
|
516
541
|
updateMany(set, where) {
|
|
517
542
|
const denied = writesDenied("ctx.db.global().updateMany") ??
|
|
@@ -525,7 +550,7 @@ function buildTenantDb(options) {
|
|
|
525
550
|
return Promise.reject(new Error("db.global().updateMany without where would mass-update every tenant's rows. Pass at least one where condition."));
|
|
526
551
|
}
|
|
527
552
|
report("global-write", globalWriteReason());
|
|
528
|
-
return withDbSpan("update", table, async () =>
|
|
553
|
+
return withDbSpan("update", table, async () => updateRows(table, set, where));
|
|
529
554
|
},
|
|
530
555
|
deleteMany(where) {
|
|
531
556
|
const denied = writesDenied("ctx.db.global().deleteMany") ??
|
|
@@ -538,10 +563,10 @@ function buildTenantDb(options) {
|
|
|
538
563
|
}
|
|
539
564
|
report("global-write", globalWriteReason());
|
|
540
565
|
return withDbSpan("delete", table, async () => {
|
|
541
|
-
await
|
|
566
|
+
await deleteRows(table, where);
|
|
542
567
|
});
|
|
543
568
|
},
|
|
544
|
-
// @cast-boundary phantom-brand — GlobalWrites is only present in the type when TTable
|
|
569
|
+
// @cast-boundary phantom-brand — GlobalWrites is only present in the type when TTable carries Writable; the object above always carries the methods.
|
|
545
570
|
};
|
|
546
571
|
}
|
|
547
572
|
function grantedUnsafeRawRunner(declaredStepReason) {
|
|
@@ -605,7 +630,7 @@ function buildTenantDb(options) {
|
|
|
605
630
|
if (managedDenied)
|
|
606
631
|
return Promise.reject(managedDenied);
|
|
607
632
|
const data = insertValues(table, values);
|
|
608
|
-
return withDbSpan("insert", table, async () =>
|
|
633
|
+
return withDbSpan("insert", table, async () => insertRow(table, data));
|
|
609
634
|
},
|
|
610
635
|
updateMany(table, set, where) {
|
|
611
636
|
const writeDenied = writesDenied("ctx.db.updateMany");
|
|
@@ -624,7 +649,7 @@ function buildTenantDb(options) {
|
|
|
624
649
|
if (managedDenied)
|
|
625
650
|
return Promise.reject(managedDenied);
|
|
626
651
|
const filter = writeWhere(table, where);
|
|
627
|
-
return withDbSpan("update", table, async () =>
|
|
652
|
+
return withDbSpan("update", table, async () => updateRows(table, set, filter));
|
|
628
653
|
},
|
|
629
654
|
deleteMany(table, where) {
|
|
630
655
|
const writeDenied = writesDenied("ctx.db.deleteMany");
|
|
@@ -640,7 +665,7 @@ function buildTenantDb(options) {
|
|
|
640
665
|
if (managedDenied)
|
|
641
666
|
return Promise.reject(managedDenied);
|
|
642
667
|
const filter = writeWhere(table, where);
|
|
643
|
-
return withDbSpan("delete", table, async () =>
|
|
668
|
+
return withDbSpan("delete", table, async () => deleteRows(table, filter));
|
|
644
669
|
},
|
|
645
670
|
};
|
|
646
671
|
declaredUnsafeRawRunners.set(tenantDb, grantedUnsafeRawRunner);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ExecutorManagedTenantDb } from "../../db/tenant-db.ts";
|
|
3
3
|
import type { Registry, TenantId } from "../types/index.ts";
|
|
4
4
|
import type { TenantDestroyHookResult } from "./tenant-resource.ts";
|
|
5
5
|
export interface TenantDataHookCtx {
|
|
6
|
-
readonly db:
|
|
6
|
+
readonly db: ExecutorManagedTenantDb;
|
|
7
7
|
readonly registry: Registry;
|
|
8
8
|
readonly tenantId: TenantId;
|
|
9
9
|
readonly deadlineAt?: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ExecutorManagedRunner } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
2
2
|
import type { StoredEvent } from "../event-store/event-store.ts";
|
|
3
3
|
import type { MultiStreamApplyContext } from "../pipeline/multi-stream-apply-context.ts";
|
|
4
4
|
import type { MultiStreamApplyFn, ProjectionTable, SingleStreamApplyFn } from "./types/projection.ts";
|
|
5
|
-
export declare function defineApply<TPayload = Record<string, unknown>>(fn: (event: StoredEvent<TPayload>, tx:
|
|
6
|
-
export declare function defineMspApply<TPayload = Record<string, unknown>>(fn: (event: StoredEvent<TPayload>, tx:
|
|
5
|
+
export declare function defineApply<TPayload = Record<string, unknown>>(fn: (event: StoredEvent<TPayload>, tx: ExecutorManagedRunner, table: ProjectionTable) => Promise<void>): SingleStreamApplyFn;
|
|
6
|
+
export declare function defineMspApply<TPayload = Record<string, unknown>>(fn: (event: StoredEvent<TPayload>, tx: ExecutorManagedRunner, ctx: MultiStreamApplyContext) => Promise<void>): MultiStreamApplyFn;
|
|
7
7
|
export declare function setFields(table: ProjectionTable, fields: Record<string, unknown> | ((event: Parameters<SingleStreamApplyFn>[0]) => Record<string, unknown>)): SingleStreamApplyFn;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// must commit in the same TX as the aggregate-mutation that triggered
|
|
17
17
|
// it (stronger consistency than an async projection). Reviewer judges.
|
|
18
18
|
import { deleteMany } from "../../db/query.js";
|
|
19
|
+
import { requireEntityTableMeta } from "../../db/table-introspection.js";
|
|
19
20
|
import { withExecutorManagedWrites } from "../../db/tenant-db.js";
|
|
20
21
|
import { tenantDbWriteRunner } from "../../db/tenant-db-runner.js";
|
|
21
22
|
import { defineStep } from "../define-step.js";
|
|
@@ -27,8 +28,8 @@ defineStep({
|
|
|
27
28
|
const where = resolveRequired(args.where, ctx);
|
|
28
29
|
// The `unsafe*`-step IS the sanctioned direct projection write — it exists
|
|
29
30
|
// to bypass the executor deliberately (boot-validated allowlist + reviewer
|
|
30
|
-
// gate).
|
|
31
|
-
await deleteMany(withExecutorManagedWrites(tenantDbWriteRunner(ctx.db, "unsafe projection delete")), args.table, where);
|
|
31
|
+
// gate).
|
|
32
|
+
await deleteMany(withExecutorManagedWrites(tenantDbWriteRunner(ctx.db, "unsafe projection delete")), requireEntityTableMeta(args.table, "unsafeProjectionDelete"), where);
|
|
32
33
|
},
|
|
33
34
|
});
|
|
34
35
|
export function buildUnsafeProjectionDeleteStep(args) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DbConnection } from "../db/index.ts";
|
|
2
2
|
export type EsOperationType = "seed-migration";
|
|
3
3
|
export type EsOperationAppliedBy = "boot" | "cli" | "ci-pipeline";
|
|
4
|
-
export declare const esOperationsTable: import("@cosmicdrift/kumiko-types/
|
|
4
|
+
export declare const esOperationsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
5
5
|
export declare function createEsOperationsTable(db: DbConnection): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DbConnection, DbRunner } from "../db/connection.ts";
|
|
2
2
|
import type { TenantId } from "../engine/types/index.ts";
|
|
3
|
-
export declare const archivedStreamsTable: import("@cosmicdrift/kumiko-types/
|
|
3
|
+
export declare const archivedStreamsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
4
4
|
export declare function createArchivedStreamsTable(db: DbConnection): Promise<void>;
|
|
5
5
|
export type ArchiveStreamArgs = {
|
|
6
6
|
readonly tenantId: TenantId;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type DbConnection } from "../db/index.ts";
|
|
2
|
-
export declare const eventsTable: import("@cosmicdrift/kumiko-types/
|
|
2
|
+
export declare const eventsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
3
3
|
export declare function createEventsTable(db: DbConnection): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DbConnection, DbRunner } from "../db/connection.ts";
|
|
2
2
|
import type { StoredEvent } from "./event-store.ts";
|
|
3
|
-
export declare const rebuildDeadLetterTable: import("@cosmicdrift/kumiko-types/
|
|
3
|
+
export declare const rebuildDeadLetterTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
4
4
|
export declare function createRebuildDeadLetterTable(db: DbConnection): Promise<void>;
|
|
5
5
|
export type SkippedApply = {
|
|
6
6
|
readonly event: StoredEvent;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { LoadAggregateWithSnapshotOptions, LoadAggregateWithSnapshotResult, SnapshotReducer } from "@cosmicdrift/kumiko-types/snapshot-types";
|
|
3
3
|
import type { DbConnection, DbRunner } from "../db/connection.ts";
|
|
4
4
|
import type { TenantId } from "../engine/types/index.ts";
|
|
5
|
-
export declare const snapshotsTable: import("@cosmicdrift/kumiko-types/
|
|
5
|
+
export declare const snapshotsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
6
6
|
export declare function createSnapshotsTable(db: DbConnection): Promise<void>;
|
|
7
7
|
export declare function effectiveSnapshotGeneration(callerVersion: number, entityEventVersion: number | undefined): number;
|
|
8
8
|
export type Snapshot<TState extends Record<string, unknown> = Record<string, unknown>> = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DbConnection, DbRunner } from "../db/connection.ts";
|
|
2
2
|
import type { StoredEvent } from "./event-store.ts";
|
|
3
|
-
export declare const upcasterDeadLetterTable: import("@cosmicdrift/kumiko-types/
|
|
3
|
+
export declare const upcasterDeadLetterTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
4
4
|
export declare function createUpcasterDeadLetterTable(db: DbConnection): Promise<void>;
|
|
5
5
|
export declare function recordUpcasterDeadLetter(db: DbRunner, args: {
|
|
6
6
|
event: StoredEvent;
|
|
@@ -7,7 +7,7 @@ export declare function fileRefStorageDelta(event: {
|
|
|
7
7
|
readonly bytes: number;
|
|
8
8
|
readonly files: number;
|
|
9
9
|
} | null;
|
|
10
|
-
export declare const tenantStorageUsageTable: import("@cosmicdrift/kumiko-types/
|
|
10
|
+
export declare const tenantStorageUsageTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
11
11
|
export declare const filesStorageTrackingFeature: import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
|
|
12
12
|
readonly exports: void;
|
|
13
13
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import type { WritableTable } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
1
2
|
import type { FileProviderResolver, FileUploadLedger } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
|
|
2
3
|
import type { FileStorageProvider } from "@cosmicdrift/kumiko-types/file-storage-provider-types";
|
|
3
4
|
import type { TenantId } from "@cosmicdrift/kumiko-types/identifiers";
|
|
4
5
|
import type { DbConnection } from "../db/connection.ts";
|
|
5
|
-
import { type EntityTableMeta } from "../db/entity-table-meta.ts";
|
|
6
6
|
import { type TenantDb } from "../db/tenant-db.ts";
|
|
7
|
-
export declare const fileUploadLedgerTable:
|
|
7
|
+
export declare const fileUploadLedgerTable: WritableTable;
|
|
8
8
|
export declare function createFileUploadLedger(db: DbConnection | TenantDb, tenantId: TenantId): FileUploadLedger;
|
|
9
9
|
export declare function withUploadLedger(resolver: (tenantId: TenantId) => Promise<FileStorageProvider>, db: DbConnection | TenantDb): FileProviderResolver;
|
|
10
10
|
export declare function takeUploadLedgerEntry(txDb: TenantDb, storageKey: string): Promise<boolean>;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-types/db-connection";
|
|
2
|
+
import type { ExecutorManagedRunner } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
1
3
|
import type { AnyDb } from "../db/query.ts";
|
|
4
|
+
import { type ExecutorManagedTenantDb, type TenantDb } from "../db/tenant-db.ts";
|
|
2
5
|
export { resetBlindIndexKeyForTests } from "../crypto/blind-index.ts";
|
|
3
6
|
export { resetEventPiiCatalogForTests } from "../crypto/event-pii.ts";
|
|
4
7
|
export { resetPiiSubjectKmsForTests } from "../crypto/pii-field-encryption.ts";
|
|
5
8
|
export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encryption.ts";
|
|
6
|
-
export declare function withExecutorManagedWritesForTests(db:
|
|
9
|
+
export declare function withExecutorManagedWritesForTests(db: TenantDb): ExecutorManagedTenantDb;
|
|
10
|
+
export declare function withExecutorManagedWritesForTests(db: DbRunner): ExecutorManagedRunner;
|
|
11
|
+
export declare function withExecutorManagedWritesForTests(db: AnyDb): ExecutorManagedTenantDb | ExecutorManagedRunner;
|
package/dist/internal/testing.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { createTenantDb, withExecutorManagedWrites } from "../db/tenant-db.js";
|
|
1
|
+
import { createTenantDb, withExecutorManagedWrites, } from "../db/tenant-db.js";
|
|
2
2
|
import { isBoundTenantDb, isDbRunner, tenantDbRunner } from "../db/tenant-db-runner.js";
|
|
3
3
|
export { resetBlindIndexKeyForTests } from "../crypto/blind-index.js";
|
|
4
4
|
export { resetEventPiiCatalogForTests } from "../crypto/event-pii.js";
|
|
5
5
|
export { resetPiiSubjectKmsForTests } from "../crypto/pii-field-encryption.js";
|
|
6
6
|
export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encryption.js";
|
|
7
|
-
// Seeds and teardown write read-model rows without events; the capability is the sanctioned test seam.
|
|
8
7
|
export function withExecutorManagedWritesForTests(db) {
|
|
9
8
|
if (isBoundTenantDb(db)) {
|
|
10
9
|
return createTenantDb({
|
|
@@ -3,7 +3,7 @@ import type { Registry } from "../engine/types/index.ts";
|
|
|
3
3
|
import type { JobRunner } from "../jobs/index.ts";
|
|
4
4
|
import type { Logger } from "../logging/types.ts";
|
|
5
5
|
import { type RebuildResult } from "../pipeline/index.ts";
|
|
6
|
-
export declare const pendingRebuildsTable: import("@cosmicdrift/kumiko-types/
|
|
6
|
+
export declare const pendingRebuildsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
7
7
|
export declare function createPendingRebuildsTable(db: DbConnection): Promise<void>;
|
|
8
8
|
/** Liest die rebuild-Marker der frisch applizierten Migrations und queued
|
|
9
9
|
* die betroffenen Tabellen. Upsert: ein bereits pending-er Tisch behält
|
|
@@ -2,7 +2,7 @@ import type { DbConnection } from "../db/connection.ts";
|
|
|
2
2
|
import { type PiiBackfillFailure } from "../db/queries/backfill-pii.ts";
|
|
3
3
|
import type { Registry } from "../engine/types/index.ts";
|
|
4
4
|
import type { Logger } from "../logging/types.ts";
|
|
5
|
-
export declare const piiBackfillStateTable: import("@cosmicdrift/kumiko-types/
|
|
5
|
+
export declare const piiBackfillStateTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
6
6
|
export declare function createPiiBackfillStateTable(db: DbConnection): Promise<void>;
|
|
7
7
|
export declare function piiAnnotationEntries(registry: Registry): readonly string[];
|
|
8
8
|
export declare function piiAnnotationFingerprint(registry: Registry): string;
|
|
@@ -5,7 +5,7 @@ export type PendingGapEntry = {
|
|
|
5
5
|
readonly to: string;
|
|
6
6
|
readonly xmax: string;
|
|
7
7
|
};
|
|
8
|
-
export declare const eventConsumerStateTable: import("@cosmicdrift/kumiko-types/
|
|
8
|
+
export declare const eventConsumerStateTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
9
9
|
export declare const ConsumerStatuses: {
|
|
10
10
|
readonly idle: "idle";
|
|
11
11
|
readonly processing: "processing";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DbConnection } from "../db/connection.ts";
|
|
2
|
-
export declare const projectionStateTable: import("@cosmicdrift/kumiko-types/
|
|
2
|
+
export declare const projectionStateTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
3
3
|
export declare const ProjectionStatuses: {
|
|
4
4
|
readonly idle: "idle";
|
|
5
5
|
readonly rebuilding: "rebuilding";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.380.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"./package.json": "./package.json"
|
|
200
200
|
},
|
|
201
201
|
"dependencies": {
|
|
202
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
202
|
+
"@cosmicdrift/kumiko-types": "0.380.0",
|
|
203
203
|
"bullmq": "^5.76.7",
|
|
204
204
|
"bun-types": "^1.3.13",
|
|
205
205
|
"hono": "^4.13.1",
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
"zod": "^4.4.3"
|
|
216
216
|
},
|
|
217
217
|
"devDependencies": {
|
|
218
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
218
|
+
"@cosmicdrift/kumiko-testing": "0.380.0",
|
|
219
219
|
"bun-types": "^1.3.13",
|
|
220
220
|
"pino-pretty": "^13.1.3"
|
|
221
221
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.380.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "Writes compile only for Writable tables or capable runners",
|
|
6
|
+
"detail": "The negative ExecutorOnly brand is replaced by positive brands. table(), defineUnmanagedTable and deriveEntityTableMeta with source \"unmanaged\" return Writable tables; buildEntityTable and r.projection tables do not. insertOne, updateMany, deleteMany, the upsert and counter helpers and the TenantDb and db.global() write methods accept a Writable table, or any table when the runner is an ExecutorManagedRunner or ExecutorManagedTenantDb. Widening a table to SchemaTable or EntityTableMeta drops the brand, so erased tables no longer compile. Projection apply functions receive tx as ExecutorManagedRunner. The runtime lock is unchanged.",
|
|
7
|
+
"migration": "Apply functions that annotate `tx` as `DbRunner`, inline or standalone, type it as `ExecutorManagedRunner` (or drop the annotation / annotate the function with SingleStreamApplyFn/MultiStreamApplyFn). Own write helpers take `WritableTable` for direct-write tables, or `ExecutorManagedRunner` / `ExecutorManagedTenantDb` when they write event-fed tables from an apply, rebuild or tenant-destroy context, instead of `SchemaTable`/`EntityTableMeta`. Tables annotated as `SchemaTable` lose the `Writable` brand; drop the annotation or add `& Writable`. Tenant-destroy hooks keep using the db they receive. Affects kumiko-enterprise, solon, money-horse, kumiko-studio, publicstatus."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.379.0",
|
|
4
11
|
"type": "improvement",
|