@cosmicdrift/kumiko-framework 0.386.0 → 0.387.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/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/widgets.md +39 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/routes.js +2 -2
- package/dist/api/server.js +34 -33
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +15 -15
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.js +4 -4
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +2 -2
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +4 -4
- package/dist/db/event-store-executor-context.d.ts +1 -1
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read.js +27 -28
- package/dist/db/event-store-executor-write.js +65 -70
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +26 -19
- package/dist/db/migrate-runner.js +25 -25
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +6 -6
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +82 -82
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +9 -2
- package/dist/derivatives/delete-stored-file.js +5 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +3 -3
- package/dist/engine/boot-validator/entity-handler.js +60 -61
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +1 -1
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +20 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +50 -57
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.js +15 -16
- package/dist/engine/boot-validator/screens-forms.js +10 -11
- package/dist/engine/boot-validator/screens-lists.js +14 -15
- package/dist/engine/boot-validator/screens.js +34 -40
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +75 -75
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +12 -11
- package/dist/engine/define-handler.js +12 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.js +13 -14
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +1 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +1 -1
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
- package/dist/engine/feature-ast/patterns.d.ts +2 -2
- package/dist/engine/feature-ast/render.js +2 -2
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +9 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.js +1 -1
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +12 -12
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +40 -38
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +7 -27
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.js +3 -3
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +28 -28
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +6 -7
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +2 -2
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +5 -6
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +11 -5
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +8 -8
- package/dist/jobs/job-runner-helpers.js +8 -2
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner.js +13 -15
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +10 -10
- package/dist/pipeline/dispatch-shared.d.ts +1 -0
- package/dist/pipeline/dispatch-shared.js +15 -17
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +3 -3
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-delivery.js +1 -1
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- package/dist/pipeline/projection-rebuild.js +13 -12
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- package/dist/schema-cli.js +116 -22
- package/dist/search/index.js +4 -4
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +10 -9
- package/dist/utils/compare.js +1 -1
- package/package.json +4 -4
- package/src/changes.json +48 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/pii-personal-migration.ts +17 -8
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -28,18 +28,19 @@ export class SchemaDriftError extends Error {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
export async function detectKumikoDrift(db, migrationsDir) {
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// generate)
|
|
34
|
-
// status
|
|
35
|
-
//
|
|
36
|
-
//
|
|
31
|
+
// A newer framework version may not have generated `./kumiko/migrations` yet
|
|
32
|
+
// (for example, a first upgrade or custom schema setup without `kumiko schema
|
|
33
|
+
// generate`). Treat this as no local migrations and therefore no drift, matching
|
|
34
|
+
// the status subcommand's `existsSync` guard. Without it, `loadMigrationsFromDir`
|
|
35
|
+
// calls `readdirSync`, which throws synchronous ENOENT as a plain Error instead
|
|
36
|
+
// of SchemaDriftError and causes an unhandled boot failure.
|
|
37
37
|
if (!existsSync(migrationsDir)) {
|
|
38
38
|
return { ok: true, pending: [], checksumMismatches: [], missingTables: [], missingColumns: [] };
|
|
39
39
|
}
|
|
40
40
|
const local = loadMigrationsFromDir(migrationsDir);
|
|
41
|
-
//
|
|
42
|
-
//
|
|
41
|
+
// A fresh database may not have the tracking table because `kumiko schema apply`
|
|
42
|
+
// has never run. Treat that as no applied migrations, so all local migrations are
|
|
43
|
+
// pending rather than reporting an error.
|
|
43
44
|
const trackingExists = await tableExists(db, "_kumiko_migrations");
|
|
44
45
|
const applied = trackingExists
|
|
45
46
|
? new Map((await fetchAppliedMigrations(db)).map((a) => [a.id, a.checksum]))
|
|
@@ -117,11 +118,11 @@ export function formatKumikoDriftReport(report) {
|
|
|
117
118
|
lines.push(` - ${m.table}: ${m.columns.join(", ")}`);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
// baseline
|
|
123
|
-
//
|
|
124
|
-
//
|
|
121
|
+
// Remediation depends on the cause: `kumiko schema apply` handles only pending
|
|
122
|
+
// migrations. A checksum mismatch causes MigrationChecksumMismatchError during
|
|
123
|
+
// apply, and baseline's ON CONFLICT DO NOTHING leaves it in alreadyTracked.
|
|
124
|
+
// A missing table with no pending migration was deleted after apply and cannot be
|
|
125
|
+
// restored by apply.
|
|
125
126
|
lines.push("");
|
|
126
127
|
if (report.pending.length > 0) {
|
|
127
128
|
lines.push("Run 'kumiko schema apply' to apply the pending migration(s).");
|
|
@@ -5,10 +5,10 @@ import type { Logger } from "../logging/types.ts";
|
|
|
5
5
|
import { type RebuildResult } from "../pipeline/index.ts";
|
|
6
6
|
export declare const pendingRebuildsTable: import("@cosmicdrift/kumiko-types/executor-brand").WritableSchemaTable;
|
|
7
7
|
export declare function createPendingRebuildsTable(db: DbConnection): Promise<void>;
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
/** Reads rebuild markers from freshly applied migrations and queues the
|
|
9
|
+
* affected tables. Upsert: an already-pending table keeps its queue slot
|
|
10
|
+
* (`queued_at` remains unchanged, preserving order), while `migration_id`
|
|
11
|
+
* points to the latest migration that flagged it for debugging. */
|
|
12
12
|
export declare function queueRebuildsFromMarkers(db: DbConnection, options: {
|
|
13
13
|
readonly migrationsDir: string;
|
|
14
14
|
readonly appliedIds: readonly string[];
|
|
@@ -21,7 +21,7 @@ export declare function listPendingRebuildRows(db: DbConnection): Promise<readon
|
|
|
21
21
|
export declare function listPendingRebuilds(db: DbConnection): Promise<readonly string[]>;
|
|
22
22
|
export declare function clearPendingRebuilds(db: DbConnection, rows: readonly PendingRebuildRow[]): Promise<void>;
|
|
23
23
|
export type PendingRebuildRun = {
|
|
24
|
-
/**
|
|
24
|
+
/** Projections rebuilt successfully (queue entries removed). */
|
|
25
25
|
readonly rebuilt: readonly {
|
|
26
26
|
readonly projection: string;
|
|
27
27
|
readonly eventsProcessed: number;
|
|
@@ -36,18 +36,19 @@ export type PendingRebuildRun = {
|
|
|
36
36
|
* marker) OR were deliberately DROPped by a later migration in the same
|
|
37
37
|
* run (no data loss, no projection left to resolve) — drained, silent. */
|
|
38
38
|
readonly unmapped: readonly string[];
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
/** Managed tables emptied by a marker in THIS run without a resolvable
|
|
40
|
+
* projection, indicating that the owning feature is missing from the
|
|
41
|
+
* composition. They are drained to avoid a stuck loop, but logged loudly
|
|
42
|
+
* because the projection is now empty. */
|
|
42
43
|
readonly unresolvedManaged: readonly string[];
|
|
43
44
|
};
|
|
44
45
|
export type RunPendingRebuildsOptions = {
|
|
45
|
-
/**
|
|
46
|
-
* (
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
46
|
+
/** Tables freshly queued via markers in THIS apply run
|
|
47
|
+
* (returned by `queueRebuildsFromMarkers`). Markers contain only managed
|
|
48
|
+
* tables, so one without a resolvable projection is a real defect (a
|
|
49
|
+
* missing feature) and is reported loudly instead of being silently
|
|
50
|
+
* drained. Without this option, every unmapped table is treated as
|
|
51
|
+
* pre-existing and benign. */
|
|
51
52
|
readonly thisRunTables?: readonly string[];
|
|
52
53
|
};
|
|
53
54
|
export declare function rebuildProjectionOrMultiStream(name: string, deps: {
|
|
@@ -63,12 +64,12 @@ export declare function rebuildProjectionOrMultiStream(name: string, deps: {
|
|
|
63
64
|
* failed tables stay queued. Shared by every `schema apply` entry point so the
|
|
64
65
|
* report and the error text cannot drift apart. */
|
|
65
66
|
export declare function reportPendingRebuildRun(run: PendingRebuildRun, log: (line: string) => void): void;
|
|
66
|
-
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
67
|
+
/** Processes the persistent queue: maps tables to projections, rebuilds each
|
|
68
|
+
* affected projection, and removes its tables from the queue only after
|
|
69
|
+
* SUCCESS. Failed tables remain pending and are retried by the next apply
|
|
70
|
+
* (or a direct re-call). Unmapped tables are drained to avoid a stuck loop;
|
|
71
|
+
* managed tables freshly emptied in this run without a projection are also
|
|
72
|
+
* reported through `unresolvedManaged`. */
|
|
72
73
|
export declare function runPendingRebuilds(db: DbConnection, registry: Registry, options?: RunPendingRebuildsOptions): Promise<PendingRebuildRun>;
|
|
73
74
|
export declare const PROJECTION_REBUILD_JOB = "jobs:job:projection-rebuild";
|
|
74
75
|
export type EnqueueProjectionRebuildResult = {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Persistent pending-rebuild queue for the `kumiko schema apply` path.
|
|
2
2
|
//
|
|
3
|
-
// Problem
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Problem: Apps read rebuild markers only from `result.applied` for the
|
|
4
|
+
// CURRENT apply round. If the projection rebuild failed (or the process
|
|
5
|
+
// crashed in between), the marker reference was gone on the next apply —
|
|
6
|
+
// `applied` was empty — leaving the projection silently incomplete without a
|
|
7
|
+
// self-service retry path.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// Solution: persist the affected tables in `kumiko_pending_rebuilds` BEFORE
|
|
10
|
+
// the rebuild and delete them only after the associated projection rebuild
|
|
11
|
+
// succeeds. A subsequent apply, even without new migrations, processes open
|
|
12
|
+
// rebuilds through `runPendingRebuilds`.
|
|
13
13
|
import { instant, table as pgTable, sql, text } from "../db/dialect.js";
|
|
14
14
|
import { deleteMany, selectMany, upsertOnConflict } from "../db/query.js";
|
|
15
15
|
import { readRebuildMarker } from "../db/rebuild-marker.js";
|
|
@@ -24,15 +24,15 @@ export const pendingRebuildsTable = pgTable("kumiko_pending_rebuilds", {
|
|
|
24
24
|
queuedAt: instant("queued_at", { precision: 3 }).notNull().default(sql `now()`),
|
|
25
25
|
});
|
|
26
26
|
export async function createPendingRebuildsTable(db) {
|
|
27
|
-
// skip: table already exists — bootstrap
|
|
27
|
+
// skip: table already exists — bootstrap runs from multiple paths
|
|
28
28
|
if (await tableExists(db, "public.kumiko_pending_rebuilds"))
|
|
29
29
|
return;
|
|
30
30
|
await unsafePushTables(db, { kumikoPendingRebuilds: pendingRebuildsTable });
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
/** Reads rebuild markers from freshly applied migrations and queues the
|
|
33
|
+
* affected tables. Upsert: an already-pending table keeps its queue slot
|
|
34
|
+
* (`queued_at` remains unchanged, preserving order), while `migration_id`
|
|
35
|
+
* points to the latest migration that flagged it for debugging. */
|
|
36
36
|
export async function queueRebuildsFromMarkers(db, options) {
|
|
37
37
|
await createPendingRebuildsTable(db);
|
|
38
38
|
const queued = new Set();
|
|
@@ -58,7 +58,7 @@ export async function listPendingRebuilds(db) {
|
|
|
58
58
|
// table_name alone: a concurrent queueRebuildsFromMarkers can re-queue the same
|
|
59
59
|
// table for a NEWER migration (upsert bumps migration_id, keeps the slot)
|
|
60
60
|
// between the read and the clear. Scoping the delete to the snapshot's
|
|
61
|
-
// migration_id leaves that fresh re-queue intact instead of dropping it.
|
|
61
|
+
// migration_id leaves that fresh re-queue intact instead of dropping it.
|
|
62
62
|
export async function clearPendingRebuilds(db, rows) {
|
|
63
63
|
for (const { tableName, migrationId } of rows) {
|
|
64
64
|
await deleteMany(db, pendingRebuildsTable, { tableName, migrationId });
|
|
@@ -106,12 +106,12 @@ export function reportPendingRebuildRun(run, log) {
|
|
|
106
106
|
.join("; ")}. Table(s) stay queued in kumiko_pending_rebuilds — retried on the next apply.`);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
109
|
+
/** Processes the persistent queue: maps tables to projections, rebuilds each
|
|
110
|
+
* affected projection, and removes its tables from the queue only after
|
|
111
|
+
* SUCCESS. Failed tables remain pending and are retried by the next apply
|
|
112
|
+
* (or a direct re-call). Unmapped tables are drained to avoid a stuck loop;
|
|
113
|
+
* managed tables freshly emptied in this run without a projection are also
|
|
114
|
+
* reported through `unresolvedManaged`. */
|
|
115
115
|
export async function runPendingRebuilds(db, registry, options = {}) {
|
|
116
116
|
const pendingRows = await listPendingRebuildRows(db);
|
|
117
117
|
if (pendingRows.length === 0) {
|
|
@@ -119,7 +119,7 @@ export async function runPendingRebuilds(db, registry, options = {}) {
|
|
|
119
119
|
}
|
|
120
120
|
// (table → migration) snapshot read together with the table list; the clear
|
|
121
121
|
// below scopes its DELETE to this migration_id so a concurrent re-queue for a
|
|
122
|
-
// newer migration survives instead of being dropped
|
|
122
|
+
// newer migration survives instead of being dropped.
|
|
123
123
|
const migrationByTable = new Map(pendingRows.map((r) => [r.tableName, r.migrationId]));
|
|
124
124
|
const snapshotRows = (tables) => tables.flatMap((tableName) => {
|
|
125
125
|
const migrationId = migrationByTable.get(tableName);
|
|
@@ -136,7 +136,7 @@ export async function runPendingRebuilds(db, registry, options = {}) {
|
|
|
136
136
|
if (projection === undefined) {
|
|
137
137
|
// Markers only carry managed tables (rebuild-marker.ts). A table emptied
|
|
138
138
|
// in THIS run with no resolving projection is a real defect (owning
|
|
139
|
-
// feature missing from the composition) → loud
|
|
139
|
+
// feature missing from the composition) → loud — unless the table
|
|
140
140
|
// no longer exists: then a later migration in the same run deliberately
|
|
141
141
|
// DROPped it after migrating its data elsewhere, no data loss. Pre-
|
|
142
142
|
// existing pending tables stay indistinguishable from old unmanaged
|
|
@@ -151,9 +151,9 @@ export async function runPendingRebuilds(db, registry, options = {}) {
|
|
|
151
151
|
}
|
|
152
152
|
byProjection.set(projection, [...(byProjection.get(projection) ?? []), tableName]);
|
|
153
153
|
}
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
154
|
+
// Both classes are drained: the queue must not grow indefinitely, and a
|
|
155
|
+
// re-apply must not remain stuck. The distinction is conveyed through the
|
|
156
|
+
// log and return field rather than by leaving entries queued.
|
|
157
157
|
const drained = [...unmapped, ...unresolvedManaged];
|
|
158
158
|
if (drained.length > 0) {
|
|
159
159
|
await clearPendingRebuilds(db, snapshotRows(drained));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Registry } from "../engine/types/feature.ts";
|
|
2
|
-
/** Index `tableName → projection-name`
|
|
3
|
-
* table
|
|
4
|
-
* Side-effect-only MSPs (table omitted)
|
|
2
|
+
/** Index `tableName → projection-name` from the registry. Only projections with
|
|
3
|
+
* a table definition (single-stream + multi-stream-with-table) are included.
|
|
4
|
+
* Side-effect-only MSPs (table omitted) cannot be rebuilt. */
|
|
5
5
|
export declare function buildProjectionTableIndex(registry: Registry): ReadonlyMap<string, string>;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// Index `tableName → projection-name`
|
|
2
|
-
// app-
|
|
3
|
-
//
|
|
1
|
+
// Index `tableName → projection-name` from the registry, used by the
|
|
2
|
+
// app-side projection rebuild (`kumiko schema apply` reads rebuild markers,
|
|
3
|
+
// maps tables to projections, then calls rebuildProjection).
|
|
4
4
|
//
|
|
5
|
-
// Drizzle-
|
|
6
|
-
// buildEntityTable/deriveEntityTableMeta
|
|
5
|
+
// Drizzle-free: the table name comes from the kumiko symbol attached to the
|
|
6
|
+
// table definition by buildEntityTable/deriveEntityTableMeta.
|
|
7
7
|
import { extractTableName } from "../db/index.js";
|
|
8
|
-
/** Index `tableName → projection-name`
|
|
9
|
-
* table
|
|
10
|
-
* Side-effect-only MSPs (table omitted)
|
|
8
|
+
/** Index `tableName → projection-name` from the registry. Only projections with
|
|
9
|
+
* a table definition (single-stream + multi-stream-with-table) are included.
|
|
10
|
+
* Side-effect-only MSPs (table omitted) cannot be rebuilt. */
|
|
11
11
|
export function buildProjectionTableIndex(registry) {
|
|
12
12
|
const index = new Map();
|
|
13
13
|
for (const [name, def] of registry.getAllProjections()) {
|
|
@@ -52,7 +52,7 @@ export function validateMetricName(name, type) {
|
|
|
52
52
|
// time) — normalize "-" to "_" here so a feature like "ai-foundation"
|
|
53
53
|
// resolves to the same "kumiko_ai_foundation_x" on both the registration
|
|
54
54
|
// path (registry-ingest.ts) and the read path (ctx.metrics / ctx.metricsFor),
|
|
55
|
-
// instead of the kebab form being rejected outright
|
|
55
|
+
// instead of the kebab form being rejected outright.
|
|
56
56
|
export function buildMetricName(featureName, shortName) {
|
|
57
57
|
const normalizedFeatureName = toKebab(featureName).replace(/-/g, "_");
|
|
58
58
|
if (!SNAKE_CASE.test(normalizedFeatureName)) {
|
|
@@ -25,12 +25,12 @@ export function createMetricsHandle(meter, featureName) {
|
|
|
25
25
|
}
|
|
26
26
|
// Same feature-bound resolution as createMetricsHandle, but for an
|
|
27
27
|
// explicit `featureName` chosen by the caller rather than the dispatching
|
|
28
|
-
// handler's own feature
|
|
28
|
+
// handler's own feature. Meant for shared/library code
|
|
29
29
|
// invoked from many features' HandlerContext (ctx.metricsFor) — the
|
|
30
30
|
// library owns one stable metric name instead of splintering into
|
|
31
31
|
// kumiko_<caller>_x per consumer.
|
|
32
32
|
//
|
|
33
|
-
// Decision
|
|
33
|
+
// Decision: unlike createMetricsHandle, an
|
|
34
34
|
// unregistered name here is a silent no-op, not a throw. This handle is
|
|
35
35
|
// meant for error/catch-path counters in shared code — a missing
|
|
36
36
|
// registration (consuming feature not mounted, metric not declared yet)
|
|
@@ -101,7 +101,7 @@ export function createNoopMetricsHandle() {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
// Shared factory behind ctx.metricsFor for handler and job contexts, so both
|
|
104
|
-
// keep the same no-op-on-unregistered contract
|
|
104
|
+
// keep the same no-op-on-unregistered contract.
|
|
105
105
|
export function createMetricsForFactory(meter) {
|
|
106
106
|
return meter
|
|
107
107
|
? (featureName) => createSafeMetricsHandle(meter, featureName)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
import { requestContext } from "../api/request-context.js";
|
|
2
3
|
import { SYSTEM_EVENT_PREFIX } from "../crypto/system-event-pii.js";
|
|
3
4
|
import { qnScope, toKebab } from "../engine/qualified-name.js";
|
|
@@ -6,6 +7,14 @@ import { isStreamArchived } from "../event-store/archive.js";
|
|
|
6
7
|
import { ArchivedStreamError } from "../event-store/errors.js";
|
|
7
8
|
import { append, getStreamVersion } from "../event-store/event-store.js";
|
|
8
9
|
import { runProjectionsForEvent } from "./projections-runner.js";
|
|
10
|
+
function requirePayloadObject(payload, eventType) {
|
|
11
|
+
if (!isPlainObject(payload)) {
|
|
12
|
+
throw new InternalError({
|
|
13
|
+
message: `appendEvent("${eventType}"): payload must be an object, got ${Array.isArray(payload) ? "array" : typeof payload}`,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return payload;
|
|
17
|
+
}
|
|
9
18
|
export async function appendDomainEventCore(deps, args) {
|
|
10
19
|
const isSystemEvent = args.type.startsWith(SYSTEM_EVENT_PREFIX);
|
|
11
20
|
const eventDef = deps.registry.getEvent(args.type);
|
|
@@ -42,10 +51,10 @@ export async function appendDomainEventCore(deps, args) {
|
|
|
42
51
|
const parsed = eventDef.schema.safeParse(args.payload ?? {});
|
|
43
52
|
if (!parsed.success)
|
|
44
53
|
throw validationErrorFromZod(parsed.error);
|
|
45
|
-
validatedPayload = parsed.data;
|
|
54
|
+
validatedPayload = requirePayloadObject(parsed.data, args.type);
|
|
46
55
|
}
|
|
47
56
|
else {
|
|
48
|
-
validatedPayload = (args.payload ?? {});
|
|
57
|
+
validatedPayload = requirePayloadObject(args.payload ?? {}, args.type);
|
|
49
58
|
}
|
|
50
59
|
// Archive guard: block writes on archived streams. Without this an append
|
|
51
60
|
// would produce an "invisible" row that loadAggregate filters out by default
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TableColumns } from "../db/dialect.ts";
|
|
2
2
|
import type { PreDeleteHookFn, Registry } from "../engine/types/index.ts";
|
|
3
3
|
import type { SystemHookDef } from "./lifecycle-pipeline.ts";
|
|
4
|
-
type TableMap = ReadonlyMap<string, TableColumns
|
|
4
|
+
type TableMap = ReadonlyMap<string, TableColumns>;
|
|
5
5
|
export declare function createCascadeDeleteHook(registry: Registry, tables: TableMap): SystemHookDef<PreDeleteHookFn>;
|
|
6
6
|
export {};
|
|
@@ -3,7 +3,7 @@ import { transaction } from "../db/query.js";
|
|
|
3
3
|
import { InternalError, toWriteErrorInfo, UnprocessableError, writeFailure, } from "../errors/index.js";
|
|
4
4
|
import { createFallbackLogger } from "../logging/utils.js";
|
|
5
5
|
import { parseJsonSafe } from "../utils/safe-json.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveAppPoolDb } from "./dispatch-shared.js";
|
|
7
7
|
import { executeNestedWrite, executeWrite } from "./dispatch-write.js";
|
|
8
8
|
import { BatchRollback, isLifecycleResult, wrapToKumiko, } from "./dispatcher-utils.js";
|
|
9
9
|
import { reserveBeforeTransaction, runAsCommand, } from "./pre-transaction-reservations.js";
|
|
@@ -209,7 +209,7 @@ async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
|
|
|
209
209
|
}
|
|
210
210
|
// batch() opens its own outer transaction — needs the top-level
|
|
211
211
|
// connection's `.begin()` (TransactionSql exposes only `.savepoint()`).
|
|
212
|
-
const db =
|
|
212
|
+
const db = resolveAppPoolDb(ctx);
|
|
213
213
|
if (!db || runsWithoutTransaction) {
|
|
214
214
|
// Without a DB connection (or for a perCommand handler, whose writes open their own transactions)
|
|
215
215
|
// there is no transaction to open. Fall back to
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
|
+
import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
2
|
import { hasAccess } from "../engine/access.js";
|
|
3
3
|
import { tenantOverrideDenied } from "../engine/cross-tenant.js";
|
|
4
4
|
import { filterReadFields } from "../engine/field-access.js";
|
|
@@ -60,16 +60,16 @@ async function runQueryHandler(ctx, type, handler, payload, includeDeleted, user
|
|
|
60
60
|
const { registry } = ctx;
|
|
61
61
|
const handlerContext = await buildHandlerContext(ctx, type, user, origin, tx, undefined, includeDeleted, undefined, outerNotifySink);
|
|
62
62
|
let result = await handler.handler({ type, payload, user }, handlerContext);
|
|
63
|
-
// postQuery
|
|
64
|
-
//
|
|
65
|
-
// responsible for
|
|
66
|
-
//
|
|
63
|
+
// postQuery hooks run BEFORE field-access filtering so they see raw data and can
|
|
64
|
+
// merge custom fields, computed counts, tags, and similar values. Each hook is
|
|
65
|
+
// responsible for applying field access to values it adds; the filter below
|
|
66
|
+
// only knows the entity's standard fields.
|
|
67
67
|
//
|
|
68
|
-
// Two
|
|
68
|
+
// Two hook scopes run in this order:
|
|
69
69
|
// 1. Handler-keyed hooks via r.hook({ type: "postQuery", target: "ns:query:list", handler })
|
|
70
|
-
// —
|
|
70
|
+
// — run only for that handler
|
|
71
71
|
// 2. Entity-keyed hooks via r.entityHook("postQuery", "property", fn)
|
|
72
|
-
// —
|
|
72
|
+
// — run for all query handlers of the entity
|
|
73
73
|
const entityName = registry.getHandlerEntity(type);
|
|
74
74
|
// Handler-keyed postQuery hooks fire for any query (incl. entity-less
|
|
75
75
|
// standalone queries like "ns:dashboard"). Entity-keyed hooks only apply
|
|
@@ -87,7 +87,7 @@ async function runQueryHandler(ctx, type, handler, payload, includeDeleted, user
|
|
|
87
87
|
}
|
|
88
88
|
result = rows;
|
|
89
89
|
}
|
|
90
|
-
else if (Array.isArray(result
|
|
90
|
+
else if (isPlainObject(result) && Array.isArray(result["rows"])) {
|
|
91
91
|
// @cast-boundary engine-payload
|
|
92
92
|
const r = result;
|
|
93
93
|
let rows = r.rows;
|
|
@@ -121,7 +121,7 @@ async function runQueryHandler(ctx, type, handler, payload, includeDeleted, user
|
|
|
121
121
|
}
|
|
122
122
|
else {
|
|
123
123
|
const resultAsDbRow = result; // @cast-boundary engine-payload
|
|
124
|
-
if (Array.isArray(resultAsDbRow
|
|
124
|
+
if (Array.isArray(resultAsDbRow["rows"])) {
|
|
125
125
|
// generic handler-result shape narrow
|
|
126
126
|
const r = result; // @cast-boundary engine-payload
|
|
127
127
|
result = {
|
|
@@ -52,6 +52,7 @@ export type DispatchContext = {
|
|
|
52
52
|
};
|
|
53
53
|
export declare const DEFAULT_MEMBER_READ_TIMEOUT_MS = 10000;
|
|
54
54
|
export declare function resolveDbSource(ctx: DispatchContext, tx: DbTx | undefined): DbConnection | DbTx | undefined;
|
|
55
|
+
export declare function resolveAppPoolDb(ctx: DispatchContext): DbConnection | undefined;
|
|
55
56
|
export declare function isMemberResolutionPrincipal(user: SessionUser): boolean;
|
|
56
57
|
export declare function applyMemberResolutionReadOnly(handlerContext: HandlerContext): HandlerContext;
|
|
57
58
|
export declare function perCommandNestedDenied(targetType: string): InternalError;
|
|
@@ -42,13 +42,13 @@ export { TENANT_TIMEZONE_CONFIG_KEY };
|
|
|
42
42
|
export const CONFIG_WRITE_SET_TYPE = "config:write:set";
|
|
43
43
|
export const CONFIG_WRITE_RESET_TYPE = "config:write:reset";
|
|
44
44
|
export const DEFAULT_MEMBER_READ_TIMEOUT_MS = 10_000;
|
|
45
|
-
// Narrowing
|
|
46
|
-
// dispatch
|
|
47
|
-
// appendEvent/projection
|
|
45
|
+
// Narrowing helper: AppContext.db is DbConnection|TenantDb|undefined. The
|
|
46
|
+
// dispatch paths need DbConnection (or DbTx from the caller scope) for
|
|
47
|
+
// appendEvent/projection writes; the TenantDb branch is excluded here.
|
|
48
48
|
export function resolveDbSource(ctx, tx) {
|
|
49
49
|
return tx ?? resolveAppPoolDb(ctx);
|
|
50
50
|
}
|
|
51
|
-
function resolveAppPoolDb(ctx) {
|
|
51
|
+
export function resolveAppPoolDb(ctx) {
|
|
52
52
|
return ctx.appContext.db; // @cast-boundary db-operator
|
|
53
53
|
}
|
|
54
54
|
// ctx.appendEvent — append a domain event onto a specific aggregate stream
|
|
@@ -492,8 +492,8 @@ readOnlyForTenant = false) {
|
|
|
492
492
|
const meter = context.meter;
|
|
493
493
|
const metrics = meter && featureName ? createMetricsHandle(meter, featureName) : createNoopMetricsHandle();
|
|
494
494
|
// ctx.metricsFor(featureName) — shared/library code binds to a feature
|
|
495
|
-
// name of its own choosing instead of the dispatching handler's
|
|
496
|
-
//
|
|
495
|
+
// name of its own choosing instead of the dispatching handler's.
|
|
496
|
+
// Unregistered names no-op rather than throw.
|
|
497
497
|
const metricsFor = createMetricsForFactory(meter);
|
|
498
498
|
// A failed inner write has already appended its event and projections when a later
|
|
499
499
|
// step (e.g. an inTransaction hook) fails, and the outer handler may swallow the
|
|
@@ -819,7 +819,7 @@ readOnlyForTenant = false) {
|
|
|
819
819
|
// (context.geoTzProvider) feeds ctx.tz.fromCoordinates / fromAddress.
|
|
820
820
|
//
|
|
821
821
|
// Resolving this via `config()` is a config-table SELECT on every single
|
|
822
|
-
// dispatch
|
|
822
|
+
// dispatch — ctx.tenantTimezoneCache memoizes it per tenant.
|
|
823
823
|
// dispatch-write.ts invalidates on config:write:set/reset for this key.
|
|
824
824
|
// Only populate from a tx-free read (tx === undefined): a write-path
|
|
825
825
|
// lookup runs inside the caller's own open transaction, and caching a
|
|
@@ -859,8 +859,8 @@ readOnlyForTenant = false) {
|
|
|
859
859
|
});
|
|
860
860
|
// ctx.locale — request-layer signal (X-Locale header → Accept-Language,
|
|
861
861
|
// resolved once at the HTTP boundary by request-id-middleware.ts) wins;
|
|
862
|
-
// then SessionUser.locale (set at login
|
|
863
|
-
//
|
|
862
|
+
// then SessionUser.locale (set at login — stale until re-login, same
|
|
863
|
+
// tradeoff as ctx.tz.user); falls back to the app's
|
|
864
864
|
// boot-configured defaultLocale, then DEFAULT_LOCALE. Mirrors ctx.tz's
|
|
865
865
|
// Request → Session → Boot-Default chain above.
|
|
866
866
|
const safeUserLocale = user.locale !== undefined && isValidLocaleTag(user.locale) ? user.locale : undefined;
|
|
@@ -889,8 +889,7 @@ readOnlyForTenant = false) {
|
|
|
889
889
|
...(derivatives && { derivatives }),
|
|
890
890
|
// preSave hooks need `changes`/`previous`/`isNew`, which only exist once
|
|
891
891
|
// a handler actually starts building its write — bound here so entity
|
|
892
|
-
// CRUD handlers (entity-handlers.ts) can forward it to the executor
|
|
893
|
-
// (kumiko-framework#1672).
|
|
892
|
+
// CRUD handlers (entity-handlers.ts) can forward it to the executor.
|
|
894
893
|
...(lifecycle && {
|
|
895
894
|
runPreSave: (handlerName, changes, previous, isNew) => lifecycle.runPreSave(handlerName, changes, previous, isNew, context),
|
|
896
895
|
}),
|
|
@@ -911,11 +910,10 @@ readOnlyForTenant = false) {
|
|
|
911
910
|
// Lets write handlers call ctx.jobRunner.dispatch(...) directly, same
|
|
912
911
|
// as a follow-up job would (test-stack.ts wires the matching runner).
|
|
913
912
|
...(jobRunner && { jobRunner }),
|
|
914
|
-
// ctx.user
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
// event.user-Wert; Identity-Switches nutzen weiterhin queryAs/writeAs.
|
|
913
|
+
// ctx.user as a convenience alias for event.user. The intuitive path—"the
|
|
914
|
+
// context knows its user"—lets handlers avoid writing `event.user.tenantId`
|
|
915
|
+
// and reduces errors in ctx.user access patterns. It is identical to
|
|
916
|
+
// event.user; identity switches still use queryAs/writeAs.
|
|
919
917
|
user,
|
|
920
918
|
_tenantId: user.tenantId,
|
|
921
919
|
...(hasLiveCommitSink && { scheduleAfterCommit }),
|
|
@@ -970,7 +968,7 @@ export async function runHandlerInstrumented(ctx, type, operation, user, inner)
|
|
|
970
968
|
attributes: dispatcherSpanAttributes(type, operation, user, registry.getHandlerFeature(type)),
|
|
971
969
|
}, async (span) => {
|
|
972
970
|
try {
|
|
973
|
-
//
|
|
971
|
+
// Everything the handler writes, including entity-executor
|
|
974
972
|
// writes below it, is attributed to this handler.
|
|
975
973
|
const result = await runWithOrigin({
|
|
976
974
|
handler: type,
|
|
@@ -53,7 +53,7 @@ async function* executeStreamInner(ctx, type, payload, user, origin, options) {
|
|
|
53
53
|
// When access is revoked mid-pull, `iterator.next()` is still in flight.
|
|
54
54
|
// Awaiting `iterator.return()` in that state deadlocks async generators in
|
|
55
55
|
// Bun (overlapping next+return). Track abandonment so finally skips the
|
|
56
|
-
// await; close is fire-and-forget instead
|
|
56
|
+
// await; close is fire-and-forget instead.
|
|
57
57
|
let abandonedForInvalidation = false;
|
|
58
58
|
try {
|
|
59
59
|
const handlerContext = await buildHandlerContext(ctx, type, user, origin);
|
|
@@ -54,7 +54,7 @@ function isConfigWriteResult(data) {
|
|
|
54
54
|
return false;
|
|
55
55
|
return typeof data.key === "string";
|
|
56
56
|
}
|
|
57
|
-
//
|
|
57
|
+
// dispatch-shared.ts caches the resolved tenant:config:timezone
|
|
58
58
|
// value per tenant. A successful write to that key must drop the stale
|
|
59
59
|
// entry — tenant/user scope only affects the writing tenant, system scope
|
|
60
60
|
// changes the fallback every tenant without an override reads, so the
|
|
@@ -252,8 +252,8 @@ export async function executeNestedWrite(ctx, type, payload, user, origin, tx, a
|
|
|
252
252
|
// enumerates columns generically WOULD see `tasks`; no such subscriber
|
|
253
253
|
// exists today. If you add one that iterates `Object.keys(save.data)`,
|
|
254
254
|
// filter by `entity.fields` membership to stay correct.
|
|
255
|
-
// handler-Result.data
|
|
256
|
-
// write
|
|
255
|
+
// handler-Result.data is generic across all entity handlers; nested-
|
|
256
|
+
// write inspects the shape structurally.
|
|
257
257
|
const parentWrapper = parentResult.data; // @cast-boundary engine-payload
|
|
258
258
|
const parentRow = (parentWrapper["data"] ?? parentWrapper); // @cast-boundary engine-payload
|
|
259
259
|
const parentId = parentRow["id"];
|
|
@@ -5,7 +5,7 @@ export type FailedWriteResult = Extract<WriteResult, {
|
|
|
5
5
|
}>;
|
|
6
6
|
export declare function isFailedWriteResult(result: unknown): result is FailedWriteResult;
|
|
7
7
|
export declare function isLifecycleResult(data: unknown): data is LifecycleResult;
|
|
8
|
-
export declare function isWriteResultShape(result: unknown):
|
|
8
|
+
export declare function isWriteResultShape(result: unknown): result is WriteResult;
|
|
9
9
|
export declare function describeShape(result: unknown): string;
|
|
10
10
|
export declare function dispatcherSpanAttributes(type: string, operation: "query" | "write" | "stream", user: SessionUser, feature: string | undefined): Record<string, string | number | boolean>;
|
|
11
11
|
export type AfterCommitHook = () => Promise<void>;
|
|
@@ -56,7 +56,7 @@ export function extractNestedSpecs(parentType, payload, registry) {
|
|
|
56
56
|
if (!entityName)
|
|
57
57
|
return null;
|
|
58
58
|
const relations = registry.getRelations(entityName);
|
|
59
|
-
const source = payload; // @cast-boundary engine-payload — generic dispatch
|
|
59
|
+
const source = payload; // @cast-boundary engine-payload — generic dispatch across all entity types
|
|
60
60
|
const clean = { ...source };
|
|
61
61
|
const specs = [];
|
|
62
62
|
const typeIssues = [];
|
|
@@ -4,7 +4,7 @@ import { tableExists } from "../db/schema-inspection.js";
|
|
|
4
4
|
import { unsafePushTables } from "../db/table-push.js";
|
|
5
5
|
// Reserved sentinel used in the instance_id column for consumers whose
|
|
6
6
|
// delivery is "shared" — i.e. one cursor across all dispatcher instances
|
|
7
|
-
// (the default
|
|
7
|
+
// (the default behaviour). Per-instance consumers store
|
|
8
8
|
// the concrete instanceId. Postgres PK columns can't be NULL and
|
|
9
9
|
// `NULL = NULL` is UNKNOWN in SQL — a nullable instance_id would break
|
|
10
10
|
// both uniqueness and PK constraints. The boot-validator refuses to
|
|
@@ -62,7 +62,7 @@ export async function createEventConsumerStateTable(db) {
|
|
|
62
62
|
// no need to pre-check columnNamesOf first. Two instances booting
|
|
63
63
|
// concurrently against the same DB (this table is explicitly
|
|
64
64
|
// multi-instance) both ALTER, neither crashes on "column already
|
|
65
|
-
// exists"
|
|
65
|
+
// exists".
|
|
66
66
|
await alterTableAddColumn(db, "kumiko_event_consumers", "rearm_count", "integer", " DEFAULT 0", " NOT NULL",
|
|
67
67
|
/* ifNotExists */ true);
|
|
68
68
|
await alterTableAddColumn(db, "kumiko_event_consumers", "pending_gaps", "jsonb", " DEFAULT '[]'::jsonb", " NOT NULL",
|
|
@@ -239,7 +239,7 @@ export async function deliverEvents(consumer, events, context, maxAttempts, stat
|
|
|
239
239
|
// turn (see applyBatch for why). requestId falls back to a fresh id
|
|
240
240
|
// because the dispatcher runs outside any HTTP request (background poll),
|
|
241
241
|
// and a stable log-correlation handle is still useful for debugging.
|
|
242
|
-
//
|
|
242
|
+
// An event this apply writes is attributed to the consumer, not
|
|
243
243
|
// to whatever wrote the triggering event; causationId already links back.
|
|
244
244
|
function buildConsumerRequestScope(consumer, causationSource) {
|
|
245
245
|
const correlationId = causationSource.metadata.correlationId ?? requestContext.generateId();
|