@cosmicdrift/kumiko-framework 0.378.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/AGENTS.md +4 -1
- package/agents/examples/compose-stacks.md +1 -2
- package/agents/examples/currencies-global.md +2 -3
- package/agents/examples/embedded.md +23 -37
- package/agents/examples/event-sourcing.md +6 -5
- package/agents/examples/legal-pages.md +5 -5
- package/agents/examples/reference-data.md +26 -21
- package/agents/examples/relations.md +2 -1
- package/agents/features.md +4 -4
- package/dist/api/entity-read-access.d.ts +2 -0
- package/dist/api/entity-read-access.js +27 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/server.js +10 -3
- package/dist/api/sse-route.d.ts +5 -0
- package/dist/api/sse-route.js +59 -9
- 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.js +0 -2
- package/dist/db/entity-table-meta.d.ts +5 -3
- package/dist/db/entity-table-meta.js +6 -15
- package/dist/db/event-store-executor-write.js +21 -16
- package/dist/db/executor-managed-tables.d.ts +8 -0
- package/dist/db/executor-managed-tables.js +48 -0
- package/dist/db/index.d.ts +6 -4
- package/dist/db/index.js +4 -2
- package/dist/db/money.d.ts +1 -1
- package/dist/db/queries/backfill-pii.js +22 -12
- package/dist/db/queries/database-now.d.ts +5 -0
- package/dist/db/queries/database-now.js +12 -0
- package/dist/db/queries/event-store-orphan-rows.d.ts +8 -0
- package/dist/db/queries/event-store-orphan-rows.js +32 -0
- package/dist/db/query.d.ts +22 -11
- package/dist/db/query.js +41 -36
- package/dist/db/reference-data.d.ts +5 -4
- package/dist/db/reference-data.js +23 -21
- package/dist/db/table-builder.d.ts +2 -3
- package/dist/db/table-builder.js +2 -0
- package/dist/db/table-introspection.d.ts +2 -0
- package/dist/db/table-introspection.js +4 -0
- package/dist/db/tenant-db-runner.d.ts +2 -0
- package/dist/db/tenant-db-runner.js +7 -0
- package/dist/db/tenant-db.d.ts +9 -2
- package/dist/db/tenant-db.js +82 -25
- package/dist/engine/boot-validator/entity-handler.js +9 -4
- package/dist/engine/boot-validator/query-refs.js +8 -0
- package/dist/engine/boot-validator/screens.js +34 -9
- package/dist/engine/extensions/tenant-data.d.ts +2 -2
- package/dist/engine/feature-ui-extensions.js +4 -0
- package/dist/engine/projection-helpers.d.ts +3 -3
- package/dist/engine/registry-state.js +2 -0
- package/dist/engine/registry-validate.js +4 -1
- package/dist/engine/schema-builder.js +18 -4
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +3 -0
- package/dist/engine/soft-delete-cleanup.js +53 -24
- package/dist/engine/steps/unsafe-projection-delete.js +4 -2
- package/dist/entrypoint/index.d.ts +1 -3
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +4 -1
- package/dist/errors/serialize.d.ts +1 -0
- package/dist/errors/serialize.js +9 -5
- package/dist/errors/write-error-info.js +3 -2
- package/dist/errors/zod-bridge.js +7 -0
- 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/index.d.ts +1 -0
- package/dist/event-store/index.js +1 -0
- 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/action-form-upload.d.ts +16 -0
- package/dist/files/action-form-upload.js +45 -0
- package/dist/files/bind-pending-file-refs.d.ts +14 -0
- package/dist/files/bind-pending-file-refs.js +44 -4
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +41 -13
- package/dist/files/file-routes.js +29 -2
- package/dist/files/index.d.ts +2 -1
- package/dist/files/index.js +1 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +3 -2
- package/dist/files/purge-expired-file-ref-bytes.js +11 -5
- package/dist/files/storage-tracking.d.ts +1 -1
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/internal/testing.d.ts +7 -0
- package/dist/internal/testing.js +14 -1
- package/dist/migrations/pending-rebuilds.d.ts +1 -1
- package/dist/migrations/pii-event-backfill.d.ts +1 -1
- package/dist/migrations/pii-event-backfill.js +14 -7
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +14 -3
- package/dist/pipeline/cascade-handler.js +79 -10
- package/dist/pipeline/dispatch-shared.js +1 -9
- package/dist/pipeline/event-consumer-state.d.ts +1 -1
- package/dist/pipeline/msp-rebuild.js +5 -3
- package/dist/pipeline/projection-rebuild.js +4 -2
- package/dist/pipeline/projection-state.d.ts +1 -1
- package/dist/pipeline/projections-runner.js +2 -1
- package/dist/rate-limit/index.d.ts +1 -1
- package/dist/rate-limit/index.js +1 -1
- package/dist/rate-limit/middleware.d.ts +4 -0
- package/dist/rate-limit/middleware.js +5 -2
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +2 -2
- package/dist/ui-types/list-row-meta.d.ts +3 -0
- package/dist/ui-types/list-row-meta.js +12 -1
- package/kumiko-es-tables.json +6 -0
- package/package.json +4 -3
- package/src/changes.json +117 -0
- package/src/scripts/codemod/README.md +20 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +416 -0
- package/src/scripts/codemod/package-merge.ts +0 -11
|
@@ -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,4 +1,11 @@
|
|
|
1
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-types/db-connection";
|
|
2
|
+
import type { ExecutorManagedRunner } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
3
|
+
import type { AnyDb } from "../db/query.ts";
|
|
4
|
+
import { type ExecutorManagedTenantDb, type TenantDb } from "../db/tenant-db.ts";
|
|
1
5
|
export { resetBlindIndexKeyForTests } from "../crypto/blind-index.ts";
|
|
2
6
|
export { resetEventPiiCatalogForTests } from "../crypto/event-pii.ts";
|
|
3
7
|
export { resetPiiSubjectKmsForTests } from "../crypto/pii-field-encryption.ts";
|
|
4
8
|
export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encryption.ts";
|
|
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,5 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import { createTenantDb, withExecutorManagedWrites, } from "../db/tenant-db.js";
|
|
2
|
+
import { isBoundTenantDb, isDbRunner, tenantDbRunner } from "../db/tenant-db-runner.js";
|
|
2
3
|
export { resetBlindIndexKeyForTests } from "../crypto/blind-index.js";
|
|
3
4
|
export { resetEventPiiCatalogForTests } from "../crypto/event-pii.js";
|
|
4
5
|
export { resetPiiSubjectKmsForTests } from "../crypto/pii-field-encryption.js";
|
|
5
6
|
export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encryption.js";
|
|
7
|
+
export function withExecutorManagedWritesForTests(db) {
|
|
8
|
+
if (isBoundTenantDb(db)) {
|
|
9
|
+
return createTenantDb({
|
|
10
|
+
db: withExecutorManagedWrites(tenantDbRunner(db)),
|
|
11
|
+
tenantId: db.tenantId,
|
|
12
|
+
mode: db.mode,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
if (isDbRunner(db))
|
|
16
|
+
return withExecutorManagedWrites(db);
|
|
17
|
+
throw new Error("withExecutorManagedWritesForTests: expected a TenantDb or a postgres-js runner");
|
|
18
|
+
}
|
|
@@ -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;
|
|
@@ -37,13 +37,20 @@ export const piiBackfillStateTable = pgTable("kumiko_pii_backfill_state", {
|
|
|
37
37
|
completedAt: instant("completed_at", { precision: 3 }),
|
|
38
38
|
updatedAt: instant("updated_at", { precision: 3 }).notNull().default(sql `now()`),
|
|
39
39
|
});
|
|
40
|
+
const PII_EVENT_BACKFILL_DDL_LOCK_KEY = "pii-event-backfill:ddl";
|
|
41
|
+
// CREATE and the follow-up ALTER are not atomic: without the lock a peer that sees the
|
|
42
|
+
// freshly created table skips its own bootstrap and reads before the column exists.
|
|
43
|
+
// The lock holder's transaction only carries the lock; the DDL runs on `db` itself.
|
|
40
44
|
export async function createPiiBackfillStateTable(db) {
|
|
41
|
-
|
|
42
|
-
await
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
await db.begin(async (lockTx) => {
|
|
46
|
+
await acquireNamespacedAdvisoryLock(lockTx, PII_EVENT_BACKFILL_LOCK_NAMESPACE, PII_EVENT_BACKFILL_DDL_LOCK_KEY);
|
|
47
|
+
if (!(await tableExists(db, "public.kumiko_pii_backfill_state"))) {
|
|
48
|
+
await unsafePushTables(db, { kumikoPiiBackfillState: piiBackfillStateTable });
|
|
49
|
+
}
|
|
50
|
+
// Added after the table's first deploy; IF NOT EXISTS keeps older deployments upgradable.
|
|
51
|
+
await alterTableAddColumn(db, "kumiko_pii_backfill_state", "failed_event_ids", "bigint[]", " DEFAULT '{}'::bigint[]", " NOT NULL",
|
|
52
|
+
/* ifNotExists */ true);
|
|
53
|
+
});
|
|
47
54
|
}
|
|
48
55
|
// Replicas boot concurrently: two of them can both pass the tableExists gate and
|
|
49
56
|
// one CREATE then fails. The table existing afterwards means the other won.
|
|
@@ -71,7 +78,7 @@ export function piiAnnotationEntries(registry) {
|
|
|
71
78
|
// Bump when the backfill gains a transform stage: completed states keyed by the
|
|
72
79
|
// old fingerprint must run one more full pass. Kept out of piiAnnotationEntries,
|
|
73
80
|
// whose emptiness gates no_pii.
|
|
74
|
-
const PII_BACKFILL_TRANSFORM_VERSION = "
|
|
81
|
+
const PII_BACKFILL_TRANSFORM_VERSION = "jsonb-objects-v3";
|
|
75
82
|
export function piiAnnotationFingerprint(registry) {
|
|
76
83
|
return createHash("sha256")
|
|
77
84
|
.update([...piiAnnotationEntries(registry), PII_BACKFILL_TRANSFORM_VERSION].join("\n"))
|
|
@@ -3,7 +3,7 @@ import type { Logger } from "../logging/types.ts";
|
|
|
3
3
|
import type { Meter } from "./types/index.ts";
|
|
4
4
|
export declare const ESCAPE_HATCH_USED_SIGNAL = "security:escape-hatch-used";
|
|
5
5
|
export type EscapeHatchReportWindow = {
|
|
6
|
-
shouldReport(key: string, nowMs: number): boolean;
|
|
6
|
+
shouldReport(key: string, nowMs: number, windowMs?: number): boolean;
|
|
7
7
|
};
|
|
8
8
|
export declare function createEscapeHatchReportWindow(opts?: {
|
|
9
9
|
readonly windowMs?: number;
|
|
@@ -11,12 +11,12 @@ export function createEscapeHatchReportWindow(opts) {
|
|
|
11
11
|
// Map insertion order doubles as recency order: a re-set moves the key to the end.
|
|
12
12
|
const entries = new Map();
|
|
13
13
|
return {
|
|
14
|
-
shouldReport(key, nowMs) {
|
|
14
|
+
shouldReport(key, nowMs, keyWindowMs = windowMs) {
|
|
15
15
|
const expiresAt = entries.get(key);
|
|
16
16
|
if (expiresAt !== undefined && expiresAt > nowMs)
|
|
17
17
|
return false;
|
|
18
18
|
entries.delete(key);
|
|
19
|
-
entries.set(key, nowMs +
|
|
19
|
+
entries.set(key, nowMs + keyWindowMs);
|
|
20
20
|
while (entries.size > maxEntries) {
|
|
21
21
|
const oldestKey = entries.keys().next().value;
|
|
22
22
|
if (oldestKey === undefined)
|
|
@@ -27,6 +27,16 @@ export function createEscapeHatchReportWindow(opts) {
|
|
|
27
27
|
},
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
// Cross-tenant reads without a target (every reference lookup of a SystemAdmin list is one)
|
|
31
|
+
// repeat on each page load. One event per actor and hour keeps the trace that a SystemAdmin read
|
|
32
|
+
// other tenants without flooding the audit view; the in-memory window means one event per
|
|
33
|
+
// instance with several instances, which is acceptable. Writes, identity switches and anything
|
|
34
|
+
// aimed at a target keep the short window.
|
|
35
|
+
const READ_WINDOW_MS = 60 * 60 * 1000;
|
|
36
|
+
const READ_WINDOW_KINDS = new Set([
|
|
37
|
+
"acknowledge-cross-tenant",
|
|
38
|
+
"cross-tenant-read",
|
|
39
|
+
]);
|
|
30
40
|
// A long-lived worker must leave an audit event inside every retention window
|
|
31
41
|
// (90 days), or its crons vanish from the log once the first event is pruned.
|
|
32
42
|
const PROCESS_REAUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -165,8 +175,9 @@ export function createEscapeHatchReporter(opts) {
|
|
|
165
175
|
target?.id ?? null,
|
|
166
176
|
target?.tenantId ?? null,
|
|
167
177
|
]);
|
|
178
|
+
const isTargetlessRead = READ_WINDOW_KINDS.has(kind) && target === undefined;
|
|
168
179
|
// skip: same (handler, kind, reason, target) already reported within the window — dedup
|
|
169
|
-
if (!window.shouldReport(key, now()))
|
|
180
|
+
if (!window.shouldReport(key, now(), isTargetlessRead ? READ_WINDOW_MS : undefined))
|
|
170
181
|
return;
|
|
171
182
|
reportEscapeHatchUse(eventFor(kind, reason, target), deps);
|
|
172
183
|
};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createEventStoreExecutor } from "../db/event-store-executor.js";
|
|
2
|
+
import { fetchOne, selectMany } from "../db/query.js";
|
|
3
|
+
import { isBoundTenantDb } from "../db/tenant-db-runner.js";
|
|
2
4
|
import { OnDeleteStrategies, SystemHookNames, SystemHookPriorities } from "../engine/constants.js";
|
|
3
5
|
import { isRecordLocked } from "../engine/screen-helpers.js";
|
|
4
|
-
import {
|
|
6
|
+
import { createSystemUser } from "../engine/system-user.js";
|
|
7
|
+
import { ConflictError, FrameworkReasons, InternalError, RecordLockedError, reraiseAsKumikoError, } from "../errors/index.js";
|
|
8
|
+
// The cascade is a consequence of the parent delete the caller was already authorized for, so the
|
|
9
|
+
// children are written as the framework system user and the lock decision is taken once, up front.
|
|
10
|
+
const CASCADE_CHILD_WRITE_OPTIONS = { unsafeSkipLockGuard: true };
|
|
5
11
|
export function createCascadeDeleteHook(registry, tables) {
|
|
6
12
|
return {
|
|
7
13
|
name: SystemHookNames.cascadeDelete,
|
|
@@ -15,6 +21,41 @@ export function createCascadeDeleteHook(registry, tables) {
|
|
|
15
21
|
ctx.log?.debug(`cascadeDelete: skipping — ${!entityName ? "no entityName" : "no db"} on payload ${payload.id}`);
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
24
|
+
const writeDb = isBoundTenantDb(db) ? db : undefined;
|
|
25
|
+
const cascadeUser = createSystemUser(writeDb?.tenantId ?? payload.event.tenantId);
|
|
26
|
+
const executors = new Map();
|
|
27
|
+
const executorFor = (name, entity) => {
|
|
28
|
+
const cached = executors.get(name);
|
|
29
|
+
if (cached)
|
|
30
|
+
return cached;
|
|
31
|
+
const table = tables.get(name);
|
|
32
|
+
if (!table) {
|
|
33
|
+
throw new InternalError({ message: `cascadeDelete: no table registered for "${name}"` });
|
|
34
|
+
}
|
|
35
|
+
const created = createEventStoreExecutor(table, entity, { entityName: name });
|
|
36
|
+
executors.set(name, created);
|
|
37
|
+
return created;
|
|
38
|
+
};
|
|
39
|
+
const requireWriteDb = () => {
|
|
40
|
+
if (!writeDb) {
|
|
41
|
+
throw new InternalError({
|
|
42
|
+
message: "cascadeDelete: a TenantDb is required to write children",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return writeDb;
|
|
46
|
+
};
|
|
47
|
+
const unwrap = (result) => {
|
|
48
|
+
if (!result.isSuccess)
|
|
49
|
+
throw reraiseAsKumikoError(result.error);
|
|
50
|
+
};
|
|
51
|
+
const childIds = async (targetTable, where, entity) => {
|
|
52
|
+
// A soft-deleted child is already invisible; deleting it again would fail.
|
|
53
|
+
const rows = await selectMany(db, targetTable, {
|
|
54
|
+
...where,
|
|
55
|
+
...(entity?.softDelete === true && { isDeleted: false }),
|
|
56
|
+
});
|
|
57
|
+
return rows.map((row) => row.id);
|
|
58
|
+
};
|
|
18
59
|
// Cascade applies to outgoing hasMany / manyToMany relations only —
|
|
19
60
|
// the parent side of the link is where `onDelete` lives (see
|
|
20
61
|
// relations.ts). Incoming belongsTo edges are covered because their
|
|
@@ -33,6 +74,13 @@ export function createCascadeDeleteHook(registry, tables) {
|
|
|
33
74
|
const targetTable = tables.get(relation.target);
|
|
34
75
|
if (!targetTable)
|
|
35
76
|
continue;
|
|
77
|
+
const childEntity = registry.getEntity(relation.target);
|
|
78
|
+
if (!childEntity &&
|
|
79
|
+
(strategy === OnDeleteStrategies.cascade || strategy === OnDeleteStrategies.setNull)) {
|
|
80
|
+
throw new InternalError({
|
|
81
|
+
message: `cascadeDelete: child "${relation.target}" is not an entity; changing its rows needs an executor`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
36
84
|
if (strategy === OnDeleteStrategies.restrict) {
|
|
37
85
|
const row = await fetchOne(db, targetTable, { [relation.foreignKey]: payload.id });
|
|
38
86
|
if (row) {
|
|
@@ -48,11 +96,10 @@ export function createCascadeDeleteHook(registry, tables) {
|
|
|
48
96
|
});
|
|
49
97
|
}
|
|
50
98
|
}
|
|
51
|
-
// The
|
|
52
|
-
// block the parent delete
|
|
99
|
+
// The child writes below skip the executor's lock guard (decided here once), so a locked
|
|
100
|
+
// child must block the parent delete instead of changing with it. The error names
|
|
53
101
|
// the parent only: the caller may delete the parent without reading the child.
|
|
54
102
|
if (strategy === OnDeleteStrategies.cascade || strategy === OnDeleteStrategies.setNull) {
|
|
55
|
-
const childEntity = registry.getEntity(relation.target);
|
|
56
103
|
const childLock = childEntity?.lockedWhen;
|
|
57
104
|
if (childLock !== undefined && payload.lockGuardBypassed !== true) {
|
|
58
105
|
// A soft-deleted child is invisible to the caller and must not block.
|
|
@@ -65,11 +112,24 @@ export function createCascadeDeleteHook(registry, tables) {
|
|
|
65
112
|
}
|
|
66
113
|
}
|
|
67
114
|
}
|
|
68
|
-
if (strategy === OnDeleteStrategies.cascade) {
|
|
69
|
-
|
|
115
|
+
if (strategy === OnDeleteStrategies.cascade && childEntity) {
|
|
116
|
+
const executor = executorFor(relation.target, childEntity);
|
|
117
|
+
for (const id of await childIds(targetTable, { [relation.foreignKey]: payload.id }, childEntity)) {
|
|
118
|
+
unwrap(await executor.delete({ id }, cascadeUser, requireWriteDb(), CASCADE_CHILD_WRITE_OPTIONS));
|
|
119
|
+
}
|
|
70
120
|
}
|
|
71
|
-
if (strategy === OnDeleteStrategies.setNull) {
|
|
72
|
-
|
|
121
|
+
if (strategy === OnDeleteStrategies.setNull && childEntity) {
|
|
122
|
+
const executor = executorFor(relation.target, childEntity);
|
|
123
|
+
const rows = await selectMany(db, targetTable, {
|
|
124
|
+
[relation.foreignKey]: payload.id,
|
|
125
|
+
});
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
unwrap(await executor.update({ id: row.id, changes: { [relation.foreignKey]: null } }, cascadeUser, requireWriteDb(), {
|
|
128
|
+
...CASCADE_CHILD_WRITE_OPTIONS,
|
|
129
|
+
skipOptimisticLock: true,
|
|
130
|
+
includeDeleted: true,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
73
133
|
}
|
|
74
134
|
}
|
|
75
135
|
if (relation.type === "manyToMany" && relation.through) {
|
|
@@ -94,7 +154,16 @@ export function createCascadeDeleteHook(registry, tables) {
|
|
|
94
154
|
}
|
|
95
155
|
}
|
|
96
156
|
if (strategy === OnDeleteStrategies.cascade) {
|
|
97
|
-
|
|
157
|
+
const throughEntity = registry.getEntity(relation.through.table);
|
|
158
|
+
if (!throughEntity) {
|
|
159
|
+
throw new InternalError({
|
|
160
|
+
message: `cascadeDelete: through table "${relation.through.table}" is not an entity; deleting its rows needs an executor`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const executor = executorFor(relation.through.table, throughEntity);
|
|
164
|
+
for (const id of await childIds(throughTable, { [sourceKey]: payload.id }, throughEntity)) {
|
|
165
|
+
unwrap(await executor.delete({ id }, cascadeUser, requireWriteDb(), CASCADE_CHILD_WRITE_OPTIONS));
|
|
166
|
+
}
|
|
98
167
|
}
|
|
99
168
|
}
|
|
100
169
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
2
|
import { requestContext, runWithOrigin } from "../api/request-context.js";
|
|
3
3
|
import { runInSavepoint, selectMany } from "../db/query.js";
|
|
4
|
-
import { createTenantDb, createUncheckedSystemDb, hasTenantColumn,
|
|
4
|
+
import { createTenantDb, createUncheckedSystemDb, hasTenantColumn, } from "../db/tenant-db.js";
|
|
5
5
|
import { createDerivativesContext } from "../derivatives/derivatives-context.js";
|
|
6
6
|
import { mayOverrideTenant } from "../engine/cross-tenant.js";
|
|
7
7
|
import { createSystemUser } from "../engine/system-user.js";
|
|
@@ -166,7 +166,6 @@ export function applyMemberResolutionReadOnly(handlerContext) {
|
|
|
166
166
|
// `db` stays open — executeQuery runs the whole handler in a Postgres READ ONLY
|
|
167
167
|
// transaction, and its `memberReadOnly` grant denies every raw-runner handout.
|
|
168
168
|
dbOutsideTransaction: undefined,
|
|
169
|
-
outsideTransaction: undefined,
|
|
170
169
|
write: denyMemberResolutionWrite,
|
|
171
170
|
writeAs: denyMemberResolutionWrite,
|
|
172
171
|
// A resolved member cannot switch identity either — the target would get a normal, writable context.
|
|
@@ -253,7 +252,6 @@ export function applyBeforeTransactionReadOnly(handlerContext) {
|
|
|
253
252
|
return {
|
|
254
253
|
...denyTransactionBoundSurfaces(handlerContext),
|
|
255
254
|
dbOutsideTransaction: undefined,
|
|
256
|
-
outsideTransaction: undefined,
|
|
257
255
|
};
|
|
258
256
|
}
|
|
259
257
|
export async function buildHandlerContext(ctx, type, user, origin, tx, afterCommitHooks, includeDeleted, transactionScope = tx ? "inTransaction" : "withoutTransaction",
|
|
@@ -341,11 +339,6 @@ readOnlyForTenant = false) {
|
|
|
341
339
|
? createSystemScopedDbGuard(type, "dbOutsideTransaction", "ctx.systemDb.outsideTransaction.assertTenantMatch(...) or " +
|
|
342
340
|
"ctx.systemDb.outsideTransaction.acknowledgeCrossTenant(...)")
|
|
343
341
|
: rawDbOutsideTransaction;
|
|
344
|
-
// Not offered to r.systemScope() handlers: the raw outside-tx db is unfiltered in
|
|
345
|
-
// system mode, so fn would hand them the same cross-tenant door the guards above close.
|
|
346
|
-
const outsideTransaction = !isSystem && rawDbOutsideTransaction
|
|
347
|
-
? (fn) => runInOwnTransaction(rawDbOutsideTransaction, fn)
|
|
348
|
-
: undefined;
|
|
349
342
|
const log = context.log?.child({
|
|
350
343
|
handler: type,
|
|
351
344
|
tenantId: user.tenantId,
|
|
@@ -861,7 +854,6 @@ readOnlyForTenant = false) {
|
|
|
861
854
|
registry,
|
|
862
855
|
db: exposedDb,
|
|
863
856
|
dbOutsideTransaction,
|
|
864
|
-
...(outsideTransaction && { outsideTransaction }),
|
|
865
857
|
...(systemDb && { systemDb }),
|
|
866
858
|
log,
|
|
867
859
|
notify,
|
|
@@ -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,6 +1,7 @@
|
|
|
1
1
|
import { markConsumerRebuildFailed, resetConsumerForMspRebuild, selectConsumerForUpdate, selectEventIdHorizonWithMissingRanges, selectSnapshotXmax, updateConsumerRebuildCursor, } from "../db/queries/event-consumer.js";
|
|
2
2
|
import { assertLiveColumnsMatchMeta, assertLiveTableHasNoRowLevelSecurity, buildShadowTable, ensureRebuildSchema, rebuildMetaOrThrow, swapShadowIntoLive, } from "../db/queries/shadow-swap.js";
|
|
3
3
|
import { runInSavepoint, selectMany } from "../db/query.js";
|
|
4
|
+
import { withExecutorManagedWrites } from "../db/tenant-db.js";
|
|
4
5
|
import { InternalError } from "../errors/index.js";
|
|
5
6
|
import { loadAggregate, loadAggregateAsOf } from "../event-store/event-store.js";
|
|
6
7
|
import { eventsTable, upcastStoredEvent } from "../event-store/index.js";
|
|
@@ -70,6 +71,7 @@ export async function rebuildMultiStreamProjection(mspName, deps) {
|
|
|
70
71
|
// paths from ever double-applying the same event.
|
|
71
72
|
const events = await selectMany(tx, eventsTable, { type: [...subscribedTypes], id: { lte: horizon } }, { orderBy: { col: "id", direction: "asc" } });
|
|
72
73
|
const upcasters = registry.getEventUpcasters();
|
|
74
|
+
const applyTx = withExecutorManagedWrites(tx);
|
|
73
75
|
for (const row of events) {
|
|
74
76
|
deps.signal?.throwIfAborted();
|
|
75
77
|
const raw = {
|
|
@@ -99,7 +101,7 @@ export async function rebuildMultiStreamProjection(mspName, deps) {
|
|
|
99
101
|
// reject the whole begin() even after a caught error. Apply +
|
|
100
102
|
// ctx run on the savepoint-scoped handle.
|
|
101
103
|
try {
|
|
102
|
-
await runInSavepoint(
|
|
104
|
+
await runInSavepoint(applyTx, async (sp) => {
|
|
103
105
|
const rebuildCtx = createRebuildCtx(registry, sp, row.tenantId);
|
|
104
106
|
await applyFn(storedEvent, sp, rebuildCtx);
|
|
105
107
|
});
|
|
@@ -109,8 +111,8 @@ export async function rebuildMultiStreamProjection(mspName, deps) {
|
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
else {
|
|
112
|
-
const rebuildCtx = createRebuildCtx(registry,
|
|
113
|
-
await applyFn(storedEvent,
|
|
114
|
+
const rebuildCtx = createRebuildCtx(registry, applyTx, row.tenantId);
|
|
115
|
+
await applyFn(storedEvent, applyTx, rebuildCtx);
|
|
114
116
|
}
|
|
115
117
|
eventsProcessed++;
|
|
116
118
|
lastProcessedEventId = row.id;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { countSubscribedEvents, finalizeProjectionRebuild, markProjectionRebuildFailed, markProjectionRebuilding, selectEventsForProjectionRebuildBatch, } from "../db/queries/projection-rebuild.js";
|
|
2
2
|
import { assertLiveColumnsMatchMeta, assertLiveTableHasNoRowLevelSecurity, assertNoBlindIndexLoss, assertNoUnreachableLiveRows, buildShadowTable, countColumnDrift, ensureRebuildSchema, fenceLiveTable, rebuildMetaOrThrow, swapShadowIntoLive, } from "../db/queries/shadow-swap.js";
|
|
3
3
|
import { coerceRow, extractTableInfo, runInSavepoint, selectMany } from "../db/query.js";
|
|
4
|
+
import { withExecutorManagedWrites } from "../db/tenant-db.js";
|
|
4
5
|
import { eventsTable, getEventsHighWaterMark, upcastStoredEvent, } from "../event-store/index.js";
|
|
5
6
|
import { createRebuildDeadLetterTable, recordRebuildDeadLetters, } from "../event-store/rebuild-dead-letter.js";
|
|
6
7
|
import { emitProjectionRebuild } from "../observability/standard-metrics.js";
|
|
@@ -74,6 +75,7 @@ export async function rebuildProjection(projectionName, deps) {
|
|
|
74
75
|
// (a short batch = no more currently-committed events).
|
|
75
76
|
const drainBatch = async (tx) => {
|
|
76
77
|
const rawEvents = await selectEventsForProjectionRebuildBatch(tx, sourcesList, subscribedList, lastProcessedEventId, REBUILD_BATCH_SIZE);
|
|
78
|
+
const applyTx = withExecutorManagedWrites(tx);
|
|
77
79
|
for (const r of rawEvents) {
|
|
78
80
|
deps.signal?.throwIfAborted();
|
|
79
81
|
const row = coerceRow(r, eventsInfo);
|
|
@@ -92,7 +94,7 @@ export async function rebuildProjection(projectionName, deps) {
|
|
|
92
94
|
// begin() even after a caught error. The apply runs on the
|
|
93
95
|
// savepoint-scoped handle so its statements are confined.
|
|
94
96
|
try {
|
|
95
|
-
await runInSavepoint(
|
|
97
|
+
await runInSavepoint(applyTx, async (sp) => {
|
|
96
98
|
await applyFn(storedEvent, sp, projection.table);
|
|
97
99
|
});
|
|
98
100
|
}
|
|
@@ -101,7 +103,7 @@ export async function rebuildProjection(projectionName, deps) {
|
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
else {
|
|
104
|
-
await applyFn(storedEvent,
|
|
106
|
+
await applyFn(storedEvent, applyTx, projection.table);
|
|
105
107
|
}
|
|
106
108
|
eventsProcessed++;
|
|
107
109
|
lastProcessedEventId = row.id;
|
|
@@ -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";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withExecutorManagedWrites } from "../db/tenant-db.js";
|
|
1
2
|
import { InternalError } from "../errors/index.js";
|
|
2
3
|
// Run custom projections for a save or delete result. Lives INSIDE the
|
|
3
4
|
// transaction that appended the event — a throw from apply() rolls the event
|
|
@@ -59,7 +60,7 @@ export async function runProjectionsForEvent(event, registry, tx) {
|
|
|
59
60
|
for (const proj of liveProjectionsForEvent(event, registry)) {
|
|
60
61
|
const applyFn = proj.apply[event.type];
|
|
61
62
|
if (applyFn)
|
|
62
|
-
await applyFn(event, tx, proj.table);
|
|
63
|
+
await applyFn(event, withExecutorManagedWrites(tx), proj.table);
|
|
63
64
|
}
|
|
64
65
|
projectedEvents.add(event);
|
|
65
66
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { type BucketContext, type BucketResult, buildBucketKey, buildPayloadBucketKey, createPayloadDigest, normalizePayloadBucketValue, } from "./bucket.ts";
|
|
2
|
-
export { type AuthEndpointRateLimitOptions, authEndpointRateLimit, type GlobalIpRateLimitOptions, globalIpRateLimit, type HttpRouteRateLimitOptions, httpRouteRateLimit, } from "./middleware.ts";
|
|
2
|
+
export { type AuthEndpointRateLimitOptions, authEndpointRateLimit, DEFAULT_GLOBAL_RATE_LIMIT, type GlobalIpRateLimitOptions, globalIpRateLimit, type HttpRouteRateLimitOptions, httpRouteRateLimit, } from "./middleware.ts";
|
|
3
3
|
export { createRateLimitResolver, type RateLimitBucketRequest, type RateLimitConfig, type RateLimitDecision, type RateLimitResolver, type RateLimitResolverOptions, } from "./resolver.ts";
|
|
4
4
|
export { createUnlimitedRateLimitResolver } from "./unlimited-resolver.ts";
|
package/dist/rate-limit/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { buildBucketKey, buildPayloadBucketKey, createPayloadDigest, normalizePayloadBucketValue, } from "./bucket.js";
|
|
2
|
-
export { authEndpointRateLimit, globalIpRateLimit, httpRouteRateLimit, } from "./middleware.js";
|
|
2
|
+
export { authEndpointRateLimit, DEFAULT_GLOBAL_RATE_LIMIT, globalIpRateLimit, httpRouteRateLimit, } from "./middleware.js";
|
|
3
3
|
export { createRateLimitResolver, } from "./resolver.js";
|
|
4
4
|
export { createUnlimitedRateLimitResolver } from "./unlimited-resolver.js";
|
|
@@ -11,6 +11,10 @@ export type GlobalIpRateLimitOptions = {
|
|
|
11
11
|
readonly extractIp?: (c: Context) => string | undefined;
|
|
12
12
|
readonly onFailClosed?: (err: unknown) => void;
|
|
13
13
|
};
|
|
14
|
+
export declare const DEFAULT_GLOBAL_RATE_LIMIT: {
|
|
15
|
+
readonly limit: 600;
|
|
16
|
+
readonly windowSeconds: 60;
|
|
17
|
+
};
|
|
14
18
|
export declare function globalIpRateLimit(opts: GlobalIpRateLimitOptions): MiddlewareHandler;
|
|
15
19
|
export type HttpRouteRateLimitOptions = {
|
|
16
20
|
readonly resolver: RateLimitResolver;
|
|
@@ -3,9 +3,12 @@ import { clientIpSourceFromHonoContext, createClientIpResolver, } from "../api/c
|
|
|
3
3
|
import { requestContext } from "../api/request-context.js";
|
|
4
4
|
import { RateLimitError, serializeError } from "../errors/index.js";
|
|
5
5
|
import { toPublicBucketName } from "./bucket.js";
|
|
6
|
+
// Shared with runProdApp (server-runtime re-exports it). The auth middleware default stays
|
|
7
|
+
// stricter than runProdApp's auth limit (30): a bare authEndpointRateLimit guards brute force.
|
|
8
|
+
export const DEFAULT_GLOBAL_RATE_LIMIT = { limit: 600, windowSeconds: 60 };
|
|
6
9
|
export function globalIpRateLimit(opts) {
|
|
7
|
-
const limit = opts.limit ??
|
|
8
|
-
const windowSeconds = opts.windowSeconds ??
|
|
10
|
+
const limit = opts.limit ?? DEFAULT_GLOBAL_RATE_LIMIT.limit;
|
|
11
|
+
const windowSeconds = opts.windowSeconds ?? DEFAULT_GLOBAL_RATE_LIMIT.windowSeconds;
|
|
9
12
|
const clientIpResolver = opts.clientIpResolver ??
|
|
10
13
|
createClientIpResolver(opts.trustedProxyHops ?? 0, "globalIpRateLimit");
|
|
11
14
|
const extractIp = opts.extractIp ?? ((c) => clientIpResolver.resolve(clientIpSourceFromHonoContext(c)));
|
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { applyFormDerivations, isFormDerivationInputMode, isUnusedFallbackOperan
|
|
|
6
6
|
export type { ParsedRefTarget } from "../engine/parse-ref-target.ts";
|
|
7
7
|
export { parseRefTarget } from "../engine/parse-ref-target.ts";
|
|
8
8
|
export type { FieldsOrGroupsSection, UnavailableSelectOption } from "../engine/screen-helpers.ts";
|
|
9
|
-
export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isUnchangedValue, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.ts";
|
|
9
|
+
export { ACTION_FORM_PSEUDO_ENTITY, availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isUnchangedValue, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.ts";
|
|
10
10
|
export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefinition, FieldDefinition, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, SelectFieldDef, SelectOptionTone, TextFieldDef, } from "../engine/types/fields.ts";
|
|
11
11
|
export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.ts";
|
|
12
12
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.ts";
|
|
@@ -22,5 +22,5 @@ export type { CardColumnRoles, CardRoleColumn } from "./card-column-roles.ts";
|
|
|
22
22
|
export { CARD_META_MAX, cardColumnRoles, cardFileDisplayKind } from "./card-column-roles.ts";
|
|
23
23
|
export { type Formality, formalLocaleTag } from "./formality.ts";
|
|
24
24
|
export type { ListRowMetaColumnType, ListRowMetaReference, ReferenceLookupSource, SystemReferenceLabel, } from "./list-row-meta.ts";
|
|
25
|
-
export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "./list-row-meta.ts";
|
|
25
|
+
export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, UNRESOLVED_REFERENCE_LABEL_KEYS, } from "./list-row-meta.ts";
|
|
26
26
|
export { type PluralForms, resolveTranslationValue, type TranslationValue, translationValueOtherText, } from "./plural.ts";
|
package/dist/ui-types/index.js
CHANGED
|
@@ -23,11 +23,11 @@ export { NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-types/fields";
|
|
|
23
23
|
export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedded-derived.js";
|
|
24
24
|
export { applyFormDerivations, isFormDerivationInputMode, isUnusedFallbackOperand, } from "../engine/form-derivations.js";
|
|
25
25
|
export { parseRefTarget } from "../engine/parse-ref-target.js";
|
|
26
|
-
export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isUnchangedValue, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
26
|
+
export { ACTION_FORM_PSEUDO_ENTITY, availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isUnchangedValue, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
27
27
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
28
28
|
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
|
|
29
29
|
export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
30
30
|
export { CARD_META_MAX, cardColumnRoles, cardFileDisplayKind } from "./card-column-roles.js";
|
|
31
31
|
export { formalLocaleTag } from "./formality.js";
|
|
32
|
-
export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "./list-row-meta.js";
|
|
32
|
+
export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, UNRESOLVED_REFERENCE_LABEL_KEYS, } from "./list-row-meta.js";
|
|
33
33
|
export { resolveTranslationValue, translationValueOtherText, } from "./plural.js";
|
|
@@ -13,6 +13,9 @@ export type ReferenceLookupSource = {
|
|
|
13
13
|
export declare const REFERENCE_LOOKUP_SOURCES: Readonly<Record<string, ReferenceLookupSource>>;
|
|
14
14
|
export type SystemReferenceLabel = {
|
|
15
15
|
readonly id: string;
|
|
16
|
+
/** Further literals that mean the same system identity (e.g. the "system" createdBy literal). */
|
|
17
|
+
readonly aliases?: readonly string[];
|
|
16
18
|
readonly labelKey: string;
|
|
17
19
|
};
|
|
18
20
|
export declare const SYSTEM_REFERENCE_LABELS: Readonly<Record<string, SystemReferenceLabel>>;
|
|
21
|
+
export declare const UNRESOLVED_REFERENCE_LABEL_KEYS: Readonly<Record<string, string>>;
|
|
@@ -61,5 +61,16 @@ export const SYSTEM_REFERENCE_LABELS = {
|
|
|
61
61
|
"tenant:tenant": { id: SYSTEM_TENANT_ID, labelKey: "kumiko.reference.system-tenant" },
|
|
62
62
|
// createdBy on a system write (fw#3103) — SYSTEM_USER_ID is an alias for
|
|
63
63
|
// "no human caller", so read_users never holds a matching row.
|
|
64
|
-
"user:user": {
|
|
64
|
+
"user:user": {
|
|
65
|
+
id: SYSTEM_USER_ID,
|
|
66
|
+
aliases: ["system"],
|
|
67
|
+
labelKey: "kumiko.reference.system-user",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
// Label for a reference id that is neither a system identity nor has a
|
|
71
|
+
// visible row (deleted user, seed actor, row outside the caller's scope).
|
|
72
|
+
// Without it a reference cell falls back to the raw UUID. Opt-in per entity:
|
|
73
|
+
// for entities whose id is meaningful to the reader the raw id stays.
|
|
74
|
+
export const UNRESOLVED_REFERENCE_LABEL_KEYS = {
|
|
75
|
+
"user:user": "kumiko.reference.unknown-user",
|
|
65
76
|
};
|
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
|
},
|
|
@@ -394,6 +394,7 @@
|
|
|
394
394
|
},
|
|
395
395
|
"files": [
|
|
396
396
|
"dist",
|
|
397
|
+
"kumiko-es-tables.json",
|
|
397
398
|
"presets",
|
|
398
399
|
"src/changes.json",
|
|
399
400
|
"src/scripts/codemod",
|