@cosmicdrift/kumiko-framework 0.385.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/request-context.d.ts +2 -0
- package/dist/api/request-context.js +16 -7
- 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/cross-tenant-reads.d.ts +8 -0
- package/dist/db/cross-tenant-reads.js +23 -0
- 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/queries/test-stack.d.ts +4 -1
- package/dist/db/queries/test-stack.js +15 -2
- 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.d.ts +8 -0
- 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/internal/stack-support.d.ts +1 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-helpers.d.ts +1 -2
- package/dist/jobs/job-runner-helpers.js +8 -18
- 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 +17 -18
- 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 +42 -19
- 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 +54 -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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { asRawClient, unsafeReadRetrying } from "../query.js";
|
|
2
2
|
/** Escape hatch for caller-built SQL that may write (or lock). No closed-connection retry —
|
|
3
|
-
* retrying an ambiguous write risks double-apply
|
|
3
|
+
* retrying an ambiguous write risks double-apply. Prefer {@link executeRawQueryRead}
|
|
4
4
|
* for SELECT-only paths. */
|
|
5
5
|
export async function executeRawQuery(db, sqlText, params = []) {
|
|
6
6
|
return await asRawClient(db).unsafe(sqlText, params);
|
|
7
7
|
}
|
|
8
|
-
/** SELECT-only escape hatch with the
|
|
8
|
+
/** SELECT-only escape hatch with the closed-connection retry. Do not pass
|
|
9
9
|
* INSERT/UPDATE/DELETE — retry re-executes the statement. */
|
|
10
10
|
// @wrapper-known semantic-alias
|
|
11
11
|
export async function executeRawQueryRead(db, sqlText, params = []) {
|
|
@@ -64,10 +64,10 @@ export function rebuildMetaOrThrow(table, projectionName) {
|
|
|
64
64
|
return meta;
|
|
65
65
|
}
|
|
66
66
|
// Fence against a rebuild running with a registry that does not match the
|
|
67
|
-
// migrated live schema
|
|
67
|
+
// migrated live schema: during a rolling deploy, a pod still running
|
|
68
68
|
// the previous build can pick up an async rebuild job; its shadow — built from
|
|
69
69
|
// the stale EntityTableMeta — would swap away a freshly-migrated column
|
|
70
|
-
// (
|
|
70
|
+
// (a recurring failure). Compares COLUMN NAMES only; a type-/nullability-
|
|
71
71
|
// only drift passes (schema regression, not data loss — the boot gate of the
|
|
72
72
|
// next deploy catches it). A missing live table is fine: nothing to wipe.
|
|
73
73
|
// Must run BEFORE buildShadowTable; columnNamesOf pins table_schema='public',
|
|
@@ -93,7 +93,7 @@ export async function assertLiveColumnsMatchMeta(db, meta, projectionName) {
|
|
|
93
93
|
"Rebuilding would swap away the difference. Likely cause: this pod runs a build whose registry is behind (or ahead of) the applied migrations — rolling deploy in progress? — or DDL was applied by hand. " +
|
|
94
94
|
"Rebuild aborted; retry from a pod whose code matches the migrated schema.");
|
|
95
95
|
}
|
|
96
|
-
// EntityTableMeta carries no RLS/policies, so the swap would silently drop them
|
|
96
|
+
// EntityTableMeta carries no RLS/policies, so the swap would silently drop them.
|
|
97
97
|
export async function assertLiveTableHasNoRowLevelSecurity(tx, tableName, projectionName) {
|
|
98
98
|
const rows = await asRawClient(tx).unsafe(`SELECT c.relrowsecurity AS rls_enabled, c.relforcerowsecurity AS rls_forced,
|
|
99
99
|
(SELECT count(*)::int FROM pg_policy p WHERE p.polrelid = c.oid) AS policy_count
|
|
@@ -158,7 +158,7 @@ const UNREACHABLE_SAMPLE_LIMIT = 20;
|
|
|
158
158
|
// Runs INSIDE the rebuild tx, under the fence, before swapShadowIntoLive. A live
|
|
159
159
|
// row whose aggregate id has NO event in the projection's source streams is
|
|
160
160
|
// UNREACHABLE: no replay can ever reconstruct it, so the swap would silently
|
|
161
|
-
// drop it. That is
|
|
161
|
+
// drop it. That is a ghost row direct-inserted without ever emitting
|
|
162
162
|
// a .created event. The static CI guard cannot see it in data that already
|
|
163
163
|
// exists in production, or on table identifiers it couldn't resolve; this
|
|
164
164
|
// catches it at cutover and aborts (tx rolls back, live untouched).
|
|
@@ -169,14 +169,14 @@ const UNREACHABLE_SAMPLE_LIMIT = 20;
|
|
|
169
169
|
// - a blind-index column recomputed to NULL after the subject's key is
|
|
170
170
|
// shredded (GDPR erase) — the NULL is the intended end state;
|
|
171
171
|
// - a `sensitive` column stripped from the event log by design;
|
|
172
|
-
// - an archived stream that stops replaying
|
|
172
|
+
// - an archived stream that stops replaying — the row's wipe is the
|
|
173
173
|
// intended tombstone behavior, reported via backfill's `failed` list;
|
|
174
174
|
// - a legacy column direct-written before its handler emitted events, healed
|
|
175
|
-
// by the
|
|
175
|
+
// by the backfill-then-rebuild flow.
|
|
176
176
|
// Checking event existence INCLUDING archived streams leaves every one of them
|
|
177
177
|
// alone: those rows all have a real event, so they are not ghosts. Column-level
|
|
178
|
-
// drift is a SEPARATE, non-blocking check — see countColumnDrift below
|
|
179
|
-
// resolves the
|
|
178
|
+
// drift is a SEPARATE, non-blocking check — see countColumnDrift below,
|
|
179
|
+
// which resolves the open question: observe, don't block.
|
|
180
180
|
//
|
|
181
181
|
// Implicit projections only (caller-gated). aggregate_id and the entity id are
|
|
182
182
|
// both uuid, so the anti-join probes the events index without a cast.
|
|
@@ -298,8 +298,8 @@ async function assertBlindIndexKeyMatchesLive(raw, tableName, quotedTable, bidxC
|
|
|
298
298
|
`untouched.`);
|
|
299
299
|
}
|
|
300
300
|
// The bidx column is schema-driven, not key-driven: it exists NULL in a
|
|
301
|
-
// plaintext install and in the
|
|
302
|
-
// both of which are correct as-is. Only a POPULATED column with no key
|
|
301
|
+
// plaintext install and in the case where KMS is configured but no index key
|
|
302
|
+
// is present, both of which are correct as-is. Only a POPULATED column with no key
|
|
303
303
|
// configured in THIS process means the rebuild is about to overwrite proof
|
|
304
304
|
// of a real index with NULL — that's the one provable data-loss case.
|
|
305
305
|
export async function assertNoBlindIndexLoss(tx, tableName, meta, projectionName) {
|
|
@@ -331,29 +331,29 @@ export async function assertNoBlindIndexLoss(tx, tableName, meta, projectionName
|
|
|
331
331
|
// live-vs-shadow divergence. A blind-index column (`<field>_bidx`) is
|
|
332
332
|
// recomputed to NULL on GDPR key-shredding; the NULL is the intended end
|
|
333
333
|
// state, not drift. Everything else that legitimately diverges (archived
|
|
334
|
-
// streams,
|
|
334
|
+
// streams, backfill) either never reaches this comparison (archived rows
|
|
335
335
|
// are absent from the shadow entirely, see swapShadowIntoLive) or IS real
|
|
336
|
-
// column drift that the
|
|
336
|
+
// column drift that the backfill-then-rebuild flow relies on replay to
|
|
337
337
|
// heal — reporting it (without blocking) is correct, not a false positive.
|
|
338
338
|
const COLUMN_DRIFT_SAMPLE_LIMIT = 20;
|
|
339
339
|
// Runs INSIDE the rebuild tx, in the same slot as assertNoUnreachableLiveRows
|
|
340
340
|
// (after replay settles, before swapShadowIntoLive). Non-blocking counterpart
|
|
341
341
|
// to the ghost-row guard: reports live rows whose column values differ from
|
|
342
|
-
// the freshly-replayed shadow, WITHOUT aborting the swap (
|
|
343
|
-
//
|
|
342
|
+
// the freshly-replayed shadow, WITHOUT aborting the swap (it resolves the
|
|
343
|
+
// open question in favor of observe-not-block).
|
|
344
344
|
//
|
|
345
345
|
// Why non-blocking: a legacy column direct-written before its handler emitted
|
|
346
|
-
// events
|
|
346
|
+
// events diverges from replay by design — that divergence is exactly
|
|
347
347
|
// what the backfill-then-rebuild flow relies on replay to heal. Failing hard
|
|
348
348
|
// here would make rebuild mutually exclusive with that shipped healing path.
|
|
349
|
-
// There is no reliable metadata to distinguish "
|
|
349
|
+
// There is no reliable metadata to distinguish "healing in progress" from
|
|
350
350
|
// "someone else corrupted this row" short of an open-ended per-column policy
|
|
351
351
|
// blocklist — wrong-by-default whenever a class is missed. So: surface it,
|
|
352
352
|
// don't police it. The caller logs the result; ops decides.
|
|
353
353
|
//
|
|
354
|
-
// Caveat: sensitive CUSTOM fields still diverge until
|
|
355
|
-
// design
|
|
356
|
-
// post
|
|
354
|
+
// Caveat: sensitive CUSTOM fields still diverge until the Subject-DEK
|
|
355
|
+
// design — regular sensitive fields carry event-payload ciphertext parity
|
|
356
|
+
// post-design and don't drift. Both are reported the same as any other column
|
|
357
357
|
// drift; this is deliberate (see module comment above), not an oversight.
|
|
358
358
|
//
|
|
359
359
|
// Relies on assertLiveColumnsMatchMeta having already run: live/shadow/meta
|
|
@@ -2,5 +2,8 @@ import type { AnyDb } from "../query.ts";
|
|
|
2
2
|
export { alterTableAddColumn, createIndexIfNotExists, executeDdlStatement } from "./ddl.ts";
|
|
3
3
|
export declare function truncateTablesRestartIdentity(db: AnyDb, tableNames: readonly string[]): Promise<void>;
|
|
4
4
|
export declare function databaseExists(db: AnyDb, dbName: string): Promise<boolean>;
|
|
5
|
-
export declare function createDatabase(db: AnyDb, dbName: string): Promise<void>;
|
|
5
|
+
export declare function createDatabase(db: AnyDb, dbName: string, templateDbName?: string): Promise<void>;
|
|
6
|
+
export declare function renameDatabase(db: AnyDb, fromName: string, toName: string): Promise<void>;
|
|
7
|
+
export declare function listDatabaseNamesWithPrefix(db: AnyDb, prefix: string): Promise<readonly string[]>;
|
|
8
|
+
export declare function acquireSessionAdvisoryLock(db: AnyDb, key: string): Promise<void>;
|
|
6
9
|
export declare function dropDatabaseIfExists(db: AnyDb, dbName: string): Promise<void>;
|
|
@@ -12,8 +12,21 @@ export async function databaseExists(db, dbName) {
|
|
|
12
12
|
const rows = await asRawClient(db).unsafe(`SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = $1) AS exists`, [dbName]);
|
|
13
13
|
return rows[0]?.exists === true;
|
|
14
14
|
}
|
|
15
|
-
export async function createDatabase(db, dbName) {
|
|
16
|
-
|
|
15
|
+
export async function createDatabase(db, dbName, templateDbName) {
|
|
16
|
+
const template = templateDbName === undefined ? "" : ` TEMPLATE ${quoteTableIdent(templateDbName)}`;
|
|
17
|
+
await asRawClient(db).unsafe(`CREATE DATABASE ${quoteTableIdent(dbName)}${template}`);
|
|
18
|
+
}
|
|
19
|
+
export async function renameDatabase(db, fromName, toName) {
|
|
20
|
+
await asRawClient(db).unsafe(`ALTER DATABASE ${quoteTableIdent(fromName)} RENAME TO ${quoteTableIdent(toName)}`);
|
|
21
|
+
}
|
|
22
|
+
export async function listDatabaseNamesWithPrefix(db, prefix) {
|
|
23
|
+
const rows = await asRawClient(db).unsafe(`SELECT datname FROM pg_database WHERE starts_with(datname, $1) ORDER BY datname`, [prefix]);
|
|
24
|
+
return rows.map((row) => row.datname);
|
|
25
|
+
}
|
|
26
|
+
// Session-scoped: CREATE DATABASE cannot run inside a transaction, so the
|
|
27
|
+
// xact-scoped variant cannot guard it. Released when the session closes.
|
|
28
|
+
export async function acquireSessionAdvisoryLock(db, key) {
|
|
29
|
+
await asRawClient(db).unsafe(`SELECT pg_advisory_lock(hashtext($1))`, [key]);
|
|
17
30
|
}
|
|
18
31
|
export async function dropDatabaseIfExists(db, dbName) {
|
|
19
32
|
await asRawClient(db).unsafe(`DROP DATABASE IF EXISTS ${quoteTableIdent(dbName)}`);
|
package/dist/db/query.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare function hasBegin(client: RawClient): client is RawClientWithBegi
|
|
|
24
24
|
export declare function asRawClient(db: unknown): RawClient;
|
|
25
25
|
export declare function runnerCanBeginTransaction(db: unknown): boolean;
|
|
26
26
|
export declare function runInSavepoint<T>(tx: ExecutorManagedRunner, fn: (sp: ExecutorManagedRunner) => Promise<T>): Promise<T>;
|
|
27
|
-
export declare function runInSavepoint<T>(tx: unknown, fn: (sp:
|
|
27
|
+
export declare function runInSavepoint<T>(tx: unknown, fn: (sp: DbRunner) => Promise<T>): Promise<T>;
|
|
28
28
|
export declare function runInSavepointIfSupported<T>(db: ExecutorManagedRunner, fn: (sp: ExecutorManagedRunner) => Promise<T>): Promise<T>;
|
|
29
29
|
export declare function runInSavepointIfSupported<T>(db: unknown, fn: (sp: DbRunner) => Promise<T>): Promise<T>;
|
|
30
30
|
export type AnyDb = DbRunner | unknown;
|
package/dist/db/query.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// `table` is an EntityTableMeta; the legacy table objects carrying it under a Symbol are accepted too.
|
|
15
15
|
// Static import (not getTemporal()): coercion works without an installed
|
|
16
|
-
// global and returns the same Temporal classes as the rest of the repo
|
|
16
|
+
// global and returns the same Temporal classes as the rest of the repo.
|
|
17
17
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
18
18
|
import { isOneOf, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
19
19
|
import { requestContext } from "../api/request-context.js";
|
|
@@ -279,7 +279,7 @@ function isTemporalPlainDateTimeLike(v) {
|
|
|
279
279
|
"toPlainDate" in v &&
|
|
280
280
|
typeof v.toPlainDate === "function");
|
|
281
281
|
}
|
|
282
|
-
// Verified empirically
|
|
282
|
+
// Verified empirically: a `date`
|
|
283
283
|
// column comes back as a native JS Date, but ALWAYS anchored at UTC
|
|
284
284
|
// midnight regardless of process TZ — epochMilliseconds is stable across
|
|
285
285
|
// zones, only the local wall-clock read of that Date would drift the day
|
|
@@ -339,7 +339,7 @@ function coerceDriverRow(row, info) {
|
|
|
339
339
|
const out = {};
|
|
340
340
|
let changed = false;
|
|
341
341
|
for (const key of Object.keys(row)) {
|
|
342
|
-
// Blind-index columns
|
|
342
|
+
// Blind-index columns are pure lookup artifacts — the
|
|
343
343
|
// deterministic HMAC must never leak into caller/API responses.
|
|
344
344
|
if ((key.endsWith("_bidx") || key.endsWith("Bidx")) && info.hasColumn(key)) {
|
|
345
345
|
changed = true;
|
|
@@ -420,7 +420,7 @@ function isSqlExpression(v) {
|
|
|
420
420
|
// as-is — no Instant detour. Postgres parses a DATE literal without ever
|
|
421
421
|
// consulting the session timezone, so this is TZ-independent by construction
|
|
422
422
|
// (verified empirically — see plainDateFromDriver above for the read side).
|
|
423
|
-
// Compat cushion:
|
|
423
|
+
// Compat cushion: code (this field used to be an Instant) may still
|
|
424
424
|
// hand a Temporal.Instant into a where-filter or write on a `date` column —
|
|
425
425
|
// same UTC anchor as plainDateFromDriver's read side, so a filter built with
|
|
426
426
|
// `Temporal.Now.instant()` keeps working instead of binding a raw Instant the
|
|
@@ -515,7 +515,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
515
515
|
// multiSelect (and any other jsonb-array-of-scalars column) stores an
|
|
516
516
|
// array — a filter value is one option, not the whole array, so eq/ne/in
|
|
517
517
|
// must check array containment (`@>`) instead of scalar `=`/`<>`/`IN`
|
|
518
|
-
// against the jsonb column, which Postgres rejects outright
|
|
518
|
+
// against the jsonb column, which Postgres rejects outright.
|
|
519
519
|
// Mirrors event-store-executor-read.ts's applyFilter — keep in lock-step.
|
|
520
520
|
function jsonbContainsAny(col, candidates) {
|
|
521
521
|
if (candidates.length === 0)
|
|
@@ -703,7 +703,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
703
703
|
conditions.push(`${ref(col)} = ${p.literal}`);
|
|
704
704
|
}
|
|
705
705
|
else {
|
|
706
|
-
// Blind-index OR rewrite
|
|
706
|
+
// Blind-index OR rewrite: if the column has a bidx
|
|
707
707
|
// counterpart (suffix convention `<col>_bidx`, framework-reserved)
|
|
708
708
|
// and a key is configured, equality matches both arms —
|
|
709
709
|
// plaintext legacy rows via the plaintext arm, encrypted rows
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
// Rebuild
|
|
1
|
+
// Rebuild marker for the drizzle-free migration path.
|
|
2
2
|
//
|
|
3
|
-
// `kumiko schema generate`
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// Projektionen.
|
|
3
|
+
// `kumiko schema generate` writes a sibling marker
|
|
4
|
+
// `NNNN_<name>.rebuild.json` next to each `NNNN_<name>.sql`, listing the tables
|
|
5
|
+
// changed or created by that migration. `kumiko schema apply` reads the marker
|
|
6
|
+
// for each newly applied migration and rebuilds the affected projections.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
// registry-
|
|
11
|
-
//
|
|
12
|
-
// `buildProjectionTableIndex(registry)`.
|
|
13
|
-
//
|
|
8
|
+
// Deliberately stores only **table names** (not projection names): the generator
|
|
9
|
+
// is registry-free and does not know the app's projections. The table-to-
|
|
10
|
+
// projection resolution happens in the app during apply via
|
|
11
|
+
// `buildProjectionTableIndex(registry)`. Tables without a corresponding
|
|
12
|
+
// projection are simply skipped there.
|
|
14
13
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// re-derived.
|
|
14
|
+
// Markers are written at generate time from the structured `SchemaDiff`, rather
|
|
15
|
+
// than being re-derived during apply from the possibly hand-edited SQL.
|
|
18
16
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
17
|
import { join } from "node:path";
|
|
20
18
|
import { managedChangeRequiresRecreate } from "./migrate-generator.js";
|
|
@@ -29,12 +27,12 @@ function markerPathFor(migrationsDir, migrationId) {
|
|
|
29
27
|
// (managedChangeRequiresRecreate: drop/NOT-NULL-w/o-default/unique-index/type-
|
|
30
28
|
// or nullability-change) — then the table is emptied and must be re-derived from
|
|
31
29
|
// events. A pure additive nullable column is an in-place `ADD COLUMN` ALTER that
|
|
32
|
-
// already brings the table to the target state (same logic
|
|
30
|
+
// already brings the table to the target state (same logic applied to
|
|
33
31
|
// index-/default-only changes). Rebuilding it anyway is wasted replay AND — the
|
|
34
32
|
// bug this closes — a co-triggered rebuild can drop the fresh column (rebuild
|
|
35
33
|
// runs from the rebuilding process's registry meta; on a rolling deploy an older
|
|
36
34
|
// pod's meta lacks the column → shadow-swap wipes it → phantom migration + boot-
|
|
37
|
-
// drift crash
|
|
35
|
+
// drift crash).
|
|
38
36
|
//
|
|
39
37
|
// If a NEW additive column genuinely needs value-backfill from historical events
|
|
40
38
|
// (a column DERIVED from existing event fields), opt in explicitly by hand-adding
|
|
@@ -60,18 +58,19 @@ export function rebuildTablesFromDiff(diff) {
|
|
|
60
58
|
}
|
|
61
59
|
return [...names].sort();
|
|
62
60
|
}
|
|
63
|
-
//
|
|
64
|
-
//
|
|
61
|
+
// Writes `<sqlFilename without .sql>.rebuild.json`. An empty table list writes no
|
|
62
|
+
// marker (for example, a drop-only migration).
|
|
65
63
|
export function writeRebuildMarker(migrationsDir, sqlFilename, tables) {
|
|
66
|
-
// skip:
|
|
64
|
+
// skip: empty table list means no marker is written (for example, for a
|
|
65
|
+
// migration that only drops tables).
|
|
67
66
|
if (tables.length === 0)
|
|
68
67
|
return;
|
|
69
68
|
const migrationId = sqlFilename.replace(/\.sql$/, "");
|
|
70
69
|
const marker = { version: MARKER_VERSION, tables };
|
|
71
70
|
writeFileSync(markerPathFor(migrationsDir, migrationId), `${JSON.stringify(marker, null, 2)}\n`);
|
|
72
71
|
}
|
|
73
|
-
//
|
|
74
|
-
//
|
|
72
|
+
// Reads the table list for an applied migration. A missing or malformed file
|
|
73
|
+
// yields an empty list (the migration has no projection impact).
|
|
75
74
|
export function readRebuildMarker(migrationsDir, migrationId) {
|
|
76
75
|
const path = markerPathFor(migrationsDir, migrationId);
|
|
77
76
|
if (!existsSync(path))
|
|
@@ -70,7 +70,7 @@ function parseColumnNames(body) {
|
|
|
70
70
|
// through to the fail-loud check as "unparsed". RENAME TO / RENAME COLUMN
|
|
71
71
|
// are not listed here: they change identity and are applied explicitly.
|
|
72
72
|
const SHAPE_NEUTRAL_ALTER_CLAUSE_RE = new RegExp([
|
|
73
|
-
`ALTER COLUMN\\s+${IDENT}\\s+TYPE\\b`, //
|
|
73
|
+
`ALTER COLUMN\\s+${IDENT}\\s+TYPE\\b`, // int/bigint-catchup fixes
|
|
74
74
|
`ALTER COLUMN\\s+${IDENT}\\s+(SET|DROP)\\s+NOT NULL\\b`,
|
|
75
75
|
`ALTER COLUMN\\s+${IDENT}\\s+(SET DEFAULT\\b|DROP DEFAULT\\b)`,
|
|
76
76
|
`(ADD|DROP)\\s+CONSTRAINT\\s+${IDENT}`,
|
|
@@ -269,7 +269,7 @@ function applyStatement(schema, statement, context) {
|
|
|
269
269
|
// Reuses the real runner's file-discovery + statement-splitting
|
|
270
270
|
// (loadMigrationsFromDir) instead of a second copy, so any future change to
|
|
271
271
|
// sub-directory handling, numeric sort order, or the .sql filter can't
|
|
272
|
-
// silently drift between the runner and this replay
|
|
272
|
+
// silently drift between the runner and this replay.
|
|
273
273
|
export function replayMigrationsDir(migrationsDir) {
|
|
274
274
|
const schema = new Map();
|
|
275
275
|
const migrations = loadMigrationsFromDir(migrationsDir, expandDestructiveMarkers);
|
|
@@ -26,11 +26,10 @@ function resolveUnsafeClient(db) {
|
|
|
26
26
|
// await unsafePushTables(db, { events: eventsTable });
|
|
27
27
|
export async function tableExists(db, fullyQualifiedName) {
|
|
28
28
|
const unsafe = resolveUnsafeClient(db);
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// Schema-qualifizierte Namen (`public.events`) werden per Split einzeln quotet.
|
|
29
|
+
// SQL-side quote_ident round-trip: unquoted PostgreSQL identifiers are folded case-insensitively
|
|
30
|
+
// (`myWidget` → `mywidget`), while generated DDL preserves the case via
|
|
31
|
+
// `quoteIdent("myWidget")` → `"myWidget"`. quote_ident keeps both sides quoted identically.
|
|
32
|
+
// Schema-qualified names (`public.events`) are quoted one component at a time after splitting.
|
|
34
33
|
const dotIdx = fullyQualifiedName.indexOf(".");
|
|
35
34
|
const [sql, params] = dotIdx >= 0
|
|
36
35
|
? [
|
|
@@ -196,8 +196,7 @@ export type EntityRow<E extends EntityDefinition = EntityDefinition> = string ex
|
|
|
196
196
|
export type { ExecutorManagedRunner, ExecutorManagedWriteCapability, Writable, WritableSchemaTable, WritableTable, } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
197
197
|
export type { EntityTenancy, TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
|
|
198
198
|
type EntityTableTenancy<E extends EntityDefinition> = [NonNullable<E["tenancy"]>] extends [never] ? "tenant" : [NonNullable<E["tenancy"]>] extends ["global"] ? "global" : "tenant";
|
|
199
|
-
export type EntityTable<E extends EntityDefinition = EntityDefinition> = TableColumns
|
|
200
|
-
any> & BaseColumnsType<E> & SoftDeleteColumnsType & ColumnsForEntity<E["fields"]> & TenancyBrand<EntityTableTenancy<E>>;
|
|
199
|
+
export type EntityTable<E extends EntityDefinition = EntityDefinition> = TableColumns & BaseColumnsType<E> & SoftDeleteColumnsType & ColumnsForEntity<E["fields"]> & TenancyBrand<EntityTableTenancy<E>>;
|
|
201
200
|
export declare function buildBaseColumns(softDelete: boolean, idType?: "serial" | "uuid"): {
|
|
202
201
|
id: ColumnBuilder<string> | ColumnBuilder<number>;
|
|
203
202
|
tenantId: ColumnBuilder<string>;
|