@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,13 +1,13 @@
|
|
|
1
1
|
import type { EntityDefinition } from "../engine/types/index.ts";
|
|
2
2
|
/**
|
|
3
|
-
* API → DB: locatedTimestamp
|
|
3
|
+
* API → DB: flatten locatedTimestamp fields into two flat columns.
|
|
4
4
|
*
|
|
5
|
-
* - `{ at, tz }` (UI
|
|
6
|
-
* - `{ utc, tz }` (
|
|
7
|
-
* - `{ at, tz, utc }` → utc
|
|
8
|
-
*
|
|
5
|
+
* - `{ at, tz }` (UI form) → `{ <name>Utc, <name>Tz }` (`utc` calculated via Temporal)
|
|
6
|
+
* - `{ utc, tz }` (server form) → `utc` takes precedence
|
|
7
|
+
* - `{ at, tz, utc }` → `utc` takes precedence; `at` is ignored (the caller is
|
|
8
|
+
* responsible for the consistency check)
|
|
9
9
|
*
|
|
10
|
-
* Pure —
|
|
10
|
+
* Pure — does not mutate.
|
|
11
11
|
*/
|
|
12
12
|
export declare function flattenLocatedTimestamp(payload: Record<string, unknown>, entity: EntityDefinition): Record<string, unknown>;
|
|
13
13
|
/** Shape of a single rehydrated locatedTimestamp field. `at` is the wall clock in `tz`
|
|
@@ -18,12 +18,12 @@ export type LocatedTimestampRead = {
|
|
|
18
18
|
readonly utc: string;
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
|
-
* DB → API:
|
|
21
|
+
* DB → API: rehydrate two flat columns into combined { at, tz, utc }.
|
|
22
22
|
*
|
|
23
|
-
* `at`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* `at` is always the wall-clock time in the stored `tz` (local to the pickup
|
|
24
|
+
* location). To obtain a user-specific view, derive it from `utc` yourself —
|
|
25
|
+
* the server does not know the user's time zone.
|
|
26
26
|
*
|
|
27
|
-
* Pure —
|
|
27
|
+
* Pure — does not mutate.
|
|
28
28
|
*/
|
|
29
29
|
export declare function rehydrateLocatedTimestamp(row: Record<string, unknown>, entity: EntityDefinition): Record<string, unknown>;
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Automatically converts locatedTimestamp fields in the DB layer.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// API
|
|
5
|
-
// DB
|
|
6
|
-
// Read
|
|
3
|
+
// Contract (see also db/money.ts — the same compound-type pattern):
|
|
4
|
+
// API form: { at, tz } | { utc, tz }
|
|
5
|
+
// DB form: <name>Utc TIMESTAMPTZ + <name>Tz TEXT
|
|
6
|
+
// Read form: { at, tz, utc }
|
|
7
7
|
//
|
|
8
|
-
// `at
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// Default `at` view on read: local to the pickup location (`utc` projected into
|
|
9
|
+
// the stored `tz`). The server does not know the user's time zone, so
|
|
10
|
+
// user-specific display is derived from `utc` on the client.
|
|
11
11
|
// Static import, not the ambient global: Bun doesn't expose Temporal on
|
|
12
12
|
// globalThis, so this crashed with "Temporal is not defined" outside boot
|
|
13
|
-
// paths that install it
|
|
13
|
+
// paths that install it.
|
|
14
14
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// Legacy-Code-Pfade die noch Strings durchreichen (z.B. raw SQL).
|
|
15
|
+
// The <name>Utc column uses instant() (see dialect.ts), so Drizzle returns Temporal.Instant.
|
|
16
|
+
// Previously, a PostgreSQL wire-format string "2026-04-15 09:00:00+00" had to be
|
|
17
|
+
// converted to ISO 8601 through string manipulation. The customType now handles
|
|
18
|
+
// DB↔Instant conversion, so this function is only defensive glue for legacy code
|
|
19
|
+
// paths that still pass strings (for example, raw SQL).
|
|
21
20
|
function toInstant(value) {
|
|
22
21
|
if (value instanceof Temporal.Instant)
|
|
23
22
|
return value;
|
|
@@ -27,14 +26,14 @@ function toInstant(value) {
|
|
|
27
26
|
return Temporal.Instant.from(iso);
|
|
28
27
|
}
|
|
29
28
|
/**
|
|
30
|
-
* API → DB: locatedTimestamp
|
|
29
|
+
* API → DB: flatten locatedTimestamp fields into two flat columns.
|
|
31
30
|
*
|
|
32
|
-
* - `{ at, tz }` (UI
|
|
33
|
-
* - `{ utc, tz }` (
|
|
34
|
-
* - `{ at, tz, utc }` → utc
|
|
35
|
-
*
|
|
31
|
+
* - `{ at, tz }` (UI form) → `{ <name>Utc, <name>Tz }` (`utc` calculated via Temporal)
|
|
32
|
+
* - `{ utc, tz }` (server form) → `utc` takes precedence
|
|
33
|
+
* - `{ at, tz, utc }` → `utc` takes precedence; `at` is ignored (the caller is
|
|
34
|
+
* responsible for the consistency check)
|
|
36
35
|
*
|
|
37
|
-
* Pure —
|
|
36
|
+
* Pure — does not mutate.
|
|
38
37
|
*/
|
|
39
38
|
export function flattenLocatedTimestamp(payload, entity) {
|
|
40
39
|
const T = Temporal;
|
|
@@ -53,10 +52,10 @@ export function flattenLocatedTimestamp(payload, entity) {
|
|
|
53
52
|
if (pair.tz === undefined)
|
|
54
53
|
continue;
|
|
55
54
|
const tz = pair.tz;
|
|
56
|
-
//
|
|
57
|
-
// Temporal.Instant.
|
|
58
|
-
// toInstant() (
|
|
59
|
-
// Temporal
|
|
55
|
+
// The <name>Utc column uses instant(); Drizzle expects
|
|
56
|
+
// Temporal.Instant. Conversion from pair.utc string → Instant goes through
|
|
57
|
+
// toInstant() (which handles strings and Instant values); pair.at + tz →
|
|
58
|
+
// Instant uses Temporal math.
|
|
60
59
|
const instant = pair.utc !== undefined
|
|
61
60
|
? toInstant(pair.utc)
|
|
62
61
|
: pair.at !== undefined
|
|
@@ -70,13 +69,13 @@ export function flattenLocatedTimestamp(payload, entity) {
|
|
|
70
69
|
return result;
|
|
71
70
|
}
|
|
72
71
|
/**
|
|
73
|
-
* DB → API:
|
|
72
|
+
* DB → API: rehydrate two flat columns into combined { at, tz, utc }.
|
|
74
73
|
*
|
|
75
|
-
* `at`
|
|
76
|
-
*
|
|
77
|
-
*
|
|
74
|
+
* `at` is always the wall-clock time in the stored `tz` (local to the pickup
|
|
75
|
+
* location). To obtain a user-specific view, derive it from `utc` yourself —
|
|
76
|
+
* the server does not know the user's time zone.
|
|
78
77
|
*
|
|
79
|
-
* Pure —
|
|
78
|
+
* Pure — does not mutate.
|
|
80
79
|
*/
|
|
81
80
|
export function rehydrateLocatedTimestamp(row, entity) {
|
|
82
81
|
const result = { ...row };
|
|
@@ -3,7 +3,7 @@ import { type RetiredFrameworkTable } from "./retired-framework-tables.ts";
|
|
|
3
3
|
declare const SNAPSHOT_VERSION: 1;
|
|
4
4
|
export type Snapshot = {
|
|
5
5
|
readonly version: typeof SNAPSHOT_VERSION;
|
|
6
|
-
readonly generatedAt
|
|
6
|
+
readonly generatedAt?: string;
|
|
7
7
|
readonly tables: readonly EntityTableMeta[];
|
|
8
8
|
};
|
|
9
9
|
export type ColumnChange = {
|
|
@@ -1,31 +1,36 @@
|
|
|
1
|
-
// Build-time
|
|
2
|
-
//
|
|
1
|
+
// Build-time generator: EntityTableMeta[] → reviewable SQL migration file.
|
|
2
|
+
// Replacement for `drizzle-kit generate`.
|
|
3
3
|
//
|
|
4
|
-
// **NO-MAGIC-ON-DATA
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
7
|
-
// -
|
|
8
|
-
// (BRIN
|
|
9
|
-
// - Append-only:
|
|
10
|
-
//
|
|
11
|
-
// Sequence-Number.
|
|
4
|
+
// **NO-MAGIC-ON-DATA core principle** (see drizzle-replacement.md):
|
|
5
|
+
// - The generator emits SQL files for PR review/editing.
|
|
6
|
+
// - The generator is NEVER called at app runtime.
|
|
7
|
+
// - The output is a starting point; app authors may hand-edit the SQL
|
|
8
|
+
// (BRIN index, partial index, performance tuning) before committing.
|
|
9
|
+
// - Append-only: existing migration files are NEVER overwritten.
|
|
10
|
+
// New schema diffs go into a NEW file with an incremented sequence number.
|
|
12
11
|
//
|
|
13
|
-
// **DESTRUCTIVE
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// **DESTRUCTIVE operations** (DROP TABLE/COLUMN, index removal) are emitted
|
|
13
|
+
// as commented-out statements — reviewers must explicitly uncomment them
|
|
14
|
+
// and may need to add a data-backup step beforehand. Default:
|
|
15
|
+
// never drop automatically.
|
|
17
16
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
17
|
import { errorCodeOf, isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
19
18
|
import { compareByCodepoint } from "../utils/index.js";
|
|
20
19
|
import { renderIndex, renderTableDdl } from "./render-ddl.js";
|
|
21
20
|
import { findRetiredTableDrops } from "./retired-framework-tables.js";
|
|
22
21
|
const SNAPSHOT_VERSION = 1;
|
|
22
|
+
function isSnapshot(value) {
|
|
23
|
+
return (isPlainObject(value) &&
|
|
24
|
+
value["version"] === SNAPSHOT_VERSION &&
|
|
25
|
+
(value["generatedAt"] === undefined || typeof value["generatedAt"] === "string") &&
|
|
26
|
+
Array.isArray(value["tables"]));
|
|
27
|
+
}
|
|
23
28
|
export function snapshotFromMetas(metas) {
|
|
24
29
|
// Stable ordering by tableName so the snapshot.json diff in PRs is
|
|
25
30
|
// meaningful (table-add isn't a noisy re-sort of everything). Codepoint, not
|
|
26
31
|
// localeCompare: the snapshot is serialized to byte-exact JSON and the table
|
|
27
32
|
// order carries into the generated migration SQL — locale-dependent ordering
|
|
28
|
-
// would drift between macOS-dev and Linux-CI
|
|
33
|
+
// would drift between macOS-dev and Linux-CI.
|
|
29
34
|
const sorted = [...metas].sort((a, b) => compareByCodepoint(a.tableName, b.tableName));
|
|
30
35
|
return {
|
|
31
36
|
version: SNAPSHOT_VERSION,
|
|
@@ -41,6 +46,9 @@ export function loadSnapshotJson(path) {
|
|
|
41
46
|
if (version !== SNAPSHOT_VERSION) {
|
|
42
47
|
throw new Error(`Snapshot at ${path} has version ${String(version)}, expected ${SNAPSHOT_VERSION}`);
|
|
43
48
|
}
|
|
49
|
+
if (!isSnapshot(parsed)) {
|
|
50
|
+
throw new Error(`Snapshot at ${path} is malformed: expected a tables list`);
|
|
51
|
+
}
|
|
44
52
|
return parsed;
|
|
45
53
|
}
|
|
46
54
|
catch (err) {
|
|
@@ -53,7 +61,7 @@ export function writeSnapshotJson(path, snapshot) {
|
|
|
53
61
|
writeFileSync(path, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
54
62
|
}
|
|
55
63
|
function indexMetaKey(idx) {
|
|
56
|
-
// Index
|
|
64
|
+
// Index identity is based on its name (PostgreSQL constraint: unique within the database).
|
|
57
65
|
return idx.name;
|
|
58
66
|
}
|
|
59
67
|
function indexColumnsEqual(a, b) {
|
|
@@ -273,8 +281,7 @@ function renderColumnChange(tableName, change) {
|
|
|
273
281
|
// timestamptz → date: PG's implicit ::date cast (what a bare ALTER ...
|
|
274
282
|
// TYPE without USING falls back to) reads the session TimeZone, so the
|
|
275
283
|
// same row migrates to a different calendar day depending on who/where
|
|
276
|
-
// runs it
|
|
277
|
-
// Anchor explicitly at UTC instead of trusting the session.
|
|
284
|
+
// runs it. Anchor explicitly at UTC instead of trusting the session.
|
|
278
285
|
if ((from === "timestamptz" || from === "timestamptz(3)") && to === "date") {
|
|
279
286
|
out.push(`-- ${from} → date: explicit UTC anchor — a bare cast would use the session TimeZone (non-deterministic).`);
|
|
280
287
|
out.push(`ALTER TABLE ${tbl} ALTER COLUMN ${col} TYPE date USING (${col} AT TIME ZONE 'UTC')::date;`);
|
|
@@ -383,7 +390,7 @@ export function renderMigrationSql(diff, options) {
|
|
|
383
390
|
// needsManualWhere renders CREATE as a commented WARN (renderIndex),
|
|
384
391
|
// since the generator can't express the new WHERE — the DROP has to
|
|
385
392
|
// stay commented too, or the live index gets dropped with no
|
|
386
|
-
// executable replacement
|
|
393
|
+
// executable replacement.
|
|
387
394
|
lines.push(nextIdxMeta.needsManualWhere === true
|
|
388
395
|
? `-- (review) DROP INDEX IF EXISTS ${quoteIdent(idxChange.name)};`
|
|
389
396
|
: `DROP INDEX IF EXISTS ${quoteIdent(idxChange.name)};`);
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Migration runner: applies checked-in SQL files to a database.
|
|
2
|
+
// Replaces `drizzle-kit migrate` as a runtime component.
|
|
3
3
|
//
|
|
4
|
-
// **NO-MAGIC-ON-DATA
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
7
|
-
// -
|
|
8
|
-
// `kumiko migrate apply`
|
|
9
|
-
// - Schema
|
|
4
|
+
// **NO-MAGIC-ON-DATA core principle** (see drizzle-replacement.md):
|
|
5
|
+
// - The runner reads ONLY checked-in `<dir>/*.sql` files.
|
|
6
|
+
// - It does not reach back to EntityDefinition at runtime.
|
|
7
|
+
// - It does not auto-apply on app boot — a deploy script or init container
|
|
8
|
+
// invokes `kumiko migrate apply` as a separate step.
|
|
9
|
+
// - Schema drift is detected fail-loudly through checksum mismatches.
|
|
10
10
|
//
|
|
11
|
-
// Tracking
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// Tracking table: `_kumiko_migrations(id, applied_at, checksum)`. `id` is the
|
|
12
|
+
// filename without the `.sql` suffix. The checksum is the sha256 hash of the
|
|
13
|
+
// file content at apply time. If an already-applied file is edited, the next
|
|
14
|
+
// apply fails with a clear error (production databases contain the old SQL —
|
|
15
|
+
// editing it makes production state inconsistent with committed state).
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
20
20
|
import { asRawClient, hasBegin, } from "./query.js";
|
|
21
|
-
// Advisory-lock key (random 32-bit
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// Advisory-lock key (random 32-bit integer, stable). Prevents two Pods
|
|
22
|
+
// booting at the same time from both running migrate apply (the second
|
|
23
|
+
// blocks until the first finishes).
|
|
24
24
|
export const MIGRATION_ADVISORY_LOCK_KEY = 0x6b756d69; // "kumi" in ASCII
|
|
25
25
|
const MIGRATIONS_TABLE_DDL = `
|
|
26
26
|
CREATE TABLE IF NOT EXISTS "_kumiko_migrations" (
|
|
@@ -147,7 +147,7 @@ function sha256Hex(content) {
|
|
|
147
147
|
// returns Migration[] with id + checksum + statements. `preprocess` is an
|
|
148
148
|
// optional hook applied to each file's raw content before splitting/hashing
|
|
149
149
|
// — used by replayMigrationsDir to expand its DESTRUCTIVE-marker comments
|
|
150
|
-
// without a second, drifting copy of this file-discovery logic
|
|
150
|
+
// without a second, drifting copy of this file-discovery logic.
|
|
151
151
|
export function loadMigrationsFromDir(dir, preprocess) {
|
|
152
152
|
const files = readdirSync(dir)
|
|
153
153
|
.filter((f) => f.endsWith(".sql"))
|
|
@@ -230,9 +230,9 @@ export class MigrationChecksumMismatchError extends Error {
|
|
|
230
230
|
this.name = "MigrationChecksumMismatchError";
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
-
//
|
|
234
|
-
// _kumiko_migrations
|
|
235
|
-
//
|
|
233
|
+
// Applies pending migrations. Idempotent: already-applied migrations are
|
|
234
|
+
// skipped via the _kumiko_migrations lookup. Checksum mismatches fail loudly.
|
|
235
|
+
// The advisory lock prevents concurrent-apply races.
|
|
236
236
|
export async function runMigrations(db, migrations) {
|
|
237
237
|
// Session-level advisory lock and per-migration BEGIN/COMMIT must share one
|
|
238
238
|
// session. Pool calls may land on different connections (the pool round-robins),
|
|
@@ -256,9 +256,9 @@ export async function runMigrations(db, migrations) {
|
|
|
256
256
|
skippedIds.push(m.id);
|
|
257
257
|
continue;
|
|
258
258
|
}
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// stuck in
|
|
259
|
+
// Applies the file content and INSERT tracking row in one transaction. If a
|
|
260
|
+
// statement fails, the rollback includes the tracking row, so no partial
|
|
261
|
+
// apply remains stuck in the books.
|
|
262
262
|
await conn.unsafe("BEGIN");
|
|
263
263
|
try {
|
|
264
264
|
for (const stmt of m.statements) {
|
|
@@ -289,8 +289,8 @@ export async function runMigrations(db, migrations) {
|
|
|
289
289
|
await conn.release();
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
-
// Convenience:
|
|
293
|
-
// CLI
|
|
292
|
+
// Convenience: loads and runs migrations in one call. Used by the
|
|
293
|
+
// `kumiko migrate apply` CLI command and by test setup helpers.
|
|
294
294
|
export async function runMigrationsFromDir(db, dir) {
|
|
295
295
|
const migrations = loadMigrationsFromDir(dir);
|
|
296
296
|
return runMigrations(db, migrations);
|
package/dist/db/money.d.ts
CHANGED
|
@@ -15,17 +15,17 @@ export type MoneyRead = {
|
|
|
15
15
|
* @deprecated Removed in 0.390.0. Use `amountScaled` — this name implies ISO-4217 minor units
|
|
16
16
|
* (cents), which is wrong once a currency needing a different scale than
|
|
17
17
|
* the current flat MINOR_UNIT_SCALE=100 lands (e.g. JPY, 0 decimals).
|
|
18
|
-
* Alias of `amountScaled`, kept until
|
|
19
|
-
* consumers off it.
|
|
18
|
+
* Alias of `amountScaled`, kept until the consumers migrate off it.
|
|
20
19
|
*/
|
|
21
20
|
readonly amountMinor: number;
|
|
22
21
|
};
|
|
23
22
|
/**
|
|
24
|
-
*
|
|
23
|
+
* Rehydrate two flat DB columns into a combined `{ amount, currency }` value.
|
|
25
24
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* Throws loudly for corrupt DB values (for example, a string that cannot be
|
|
26
|
+
* converted to a number), because silent data loss would hide a bug. A
|
|
27
|
+
* NULL/undefined amount removes the field from the output.
|
|
28
28
|
*
|
|
29
|
-
* Pure —
|
|
29
|
+
* Pure — does not mutate its input.
|
|
30
30
|
*/
|
|
31
31
|
export declare function rehydrateMoney(row: Record<string, unknown>, entity: EntityDefinition): Record<string, unknown>;
|
package/dist/db/money.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Automatically convert money fields in the DB layer.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// API
|
|
5
|
-
// DB
|
|
6
|
-
// Read
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
3
|
+
// Contract (see also db/located-timestamp.ts — the same compound-type pattern):
|
|
4
|
+
// API form: { amount, currency } | number — amount in MAJOR units (56799.16 EUR)
|
|
5
|
+
// DB form: <name> BIGINT (minor units, e.g. cents) + <name>Currency TEXT
|
|
6
|
+
// Read form: { amount, currency, amountScaled } — amount in MAJOR units again,
|
|
7
|
+
// amountScaled sits alongside as the exact integer value in
|
|
8
|
+
// MINOR_UNIT_SCALE units for callers that need scale-exact
|
|
9
|
+
// comparisons (e.g. invoice sums) instead of round-tripping the
|
|
10
|
+
// float amount through /100·*100.
|
|
11
11
|
// `amountMinor` stays as a @deprecated alias of the same
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// value until the remaining consumers
|
|
13
|
+
// (renderer/web/primitives/index.tsx,
|
|
14
|
+
// renderer/components/render-field.tsx) migrate off the old name.
|
|
15
15
|
//
|
|
16
16
|
// table-builder.ts's moneyAmount column has always documented BIGINT as
|
|
17
17
|
// "the integer minor unit" — this file used to just pass the API amount
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
// pass/receive ordinary decimal amounts; DB storage stays exact-integer
|
|
24
24
|
// cents (no float drift in SUM()/aggregate queries).
|
|
25
25
|
//
|
|
26
|
-
//
|
|
27
|
-
// pre-Stufe-3
|
|
28
|
-
//
|
|
26
|
+
// Permissive insert: a primitive number is accepted as amount (legacy from
|
|
27
|
+
// pre-Stufe-3 samples). Currency then falls back to entity.defaultCurrency
|
|
28
|
+
// (or DEFAULT_CURRENCIES[0] = "EUR" as the framework fallback).
|
|
29
29
|
//
|
|
30
|
-
//
|
|
31
|
-
// amount
|
|
32
|
-
//
|
|
30
|
+
// Unlike locatedTimestamp, we retain the field name `<name>` as the
|
|
31
|
+
// amount column (legacy DB convention for money — `SUM(buying_price)` remains
|
|
32
|
+
// idiomatic). `<name>Currency` is the additional column.
|
|
33
33
|
import { MINOR_UNIT_SCALE, toMinorUnits } from "../engine/minor-units.js";
|
|
34
34
|
import { DEFAULT_CURRENCIES } from "../engine/types/index.js";
|
|
35
35
|
const FRAMEWORK_DEFAULT_CURRENCY = DEFAULT_CURRENCIES[0]; // "EUR"
|
|
@@ -46,9 +46,9 @@ function toMajorUnits(amountScaled) {
|
|
|
46
46
|
// scaled the same way. `applyTotalsMatchRefinements` (schema-builder.ts)
|
|
47
47
|
// uses this internally for `EmbeddedFieldDef.totalsMatch`; exported so a
|
|
48
48
|
// custom write-handler doing its own total check doesn't have to re-derive
|
|
49
|
-
// this unwrap-and-scale step by hand
|
|
50
|
-
//
|
|
51
|
-
//
|
|
49
|
+
// this unwrap-and-scale step by hand. A hand-rolled comparison of a raw
|
|
50
|
+
// `{amount}` against a raw minor-unit row sum is exactly what silently fails
|
|
51
|
+
// 100× off.
|
|
52
52
|
export function moneyPayloadToMinorUnits(raw) {
|
|
53
53
|
if (typeof raw === "number")
|
|
54
54
|
return toMinorUnits(raw);
|
|
@@ -81,7 +81,7 @@ export function flattenMoney(payload, entity) {
|
|
|
81
81
|
}
|
|
82
82
|
else if (typeof raw === "number") {
|
|
83
83
|
amount = raw;
|
|
84
|
-
//
|
|
84
|
+
// An explicit currency key in the payload overrides the default fallback.
|
|
85
85
|
const explicitCurrency = result[`${name}Currency`];
|
|
86
86
|
currency = typeof explicitCurrency === "string" ? explicitCurrency : fallbackCurrency;
|
|
87
87
|
}
|
|
@@ -95,12 +95,13 @@ export function flattenMoney(payload, entity) {
|
|
|
95
95
|
return result;
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
98
|
+
* Rehydrate two flat DB columns into a combined `{ amount, currency }` value.
|
|
99
99
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
100
|
+
* Throws loudly for corrupt DB values (for example, a string that cannot be
|
|
101
|
+
* converted to a number), because silent data loss would hide a bug. A
|
|
102
|
+
* NULL/undefined amount removes the field from the output.
|
|
102
103
|
*
|
|
103
|
-
* Pure —
|
|
104
|
+
* Pure — does not mutate its input.
|
|
104
105
|
*/
|
|
105
106
|
export function rehydrateMoney(row, entity) {
|
|
106
107
|
const result = { ...row };
|
|
@@ -126,7 +127,8 @@ export function rehydrateMoney(row, entity) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
else if (typeof amountRaw === "string" && amountRaw !== "") {
|
|
129
|
-
// PG
|
|
130
|
+
// The PG driver sometimes returns BIGINT as a string (when it exceeds the
|
|
131
|
+
// safe integer range of 2^53).
|
|
130
132
|
amountScaled = Number(amountRaw);
|
|
131
133
|
if (!Number.isSafeInteger(amountScaled)) {
|
|
132
134
|
throw new Error(`rehydrateMoney: field "${name}" amount string "${amountRaw}" is not a safe integer — DB corruption?`);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
// One-time backfill for pre-KMS plaintext PII in kumiko_events
|
|
1
|
+
// One-time backfill for pre-KMS plaintext PII in kumiko_events.
|
|
2
2
|
//
|
|
3
|
-
// Crypto-shredding
|
|
3
|
+
// Crypto-shredding only covers NEW writes — events appended
|
|
4
4
|
// before a KMS was configured still carry plaintext (user.created email,
|
|
5
5
|
// delivery attempt recipientAddress, job payloads). This tool re-encrypts
|
|
6
6
|
// them in place, per field, under the owning subject's DEK:
|
|
7
7
|
//
|
|
8
8
|
// - entity lifecycle events (<entity>.created/updated/deleted/forgotten/
|
|
9
9
|
// restored) for every entity with PII subject annotations — generic over
|
|
10
|
-
// subject kind (user/tenant/record
|
|
10
|
+
// subject kind (user/tenant/record), since it goes through the
|
|
11
11
|
// same collectPiiEncryptedFields/resolveSubjectForField the live write
|
|
12
12
|
// path uses
|
|
13
13
|
// - custom events from the event-PII catalog (r.defineEvent piiFields) —
|
|
14
14
|
// user/tenant/self subjects, via the same resolveEventSubject the live
|
|
15
|
-
// append path uses
|
|
15
|
+
// append path uses
|
|
16
16
|
//
|
|
17
17
|
// Already-forgotten subjects must NOT get a fresh key minted for their old
|
|
18
18
|
// plaintext — three erased-detection layers write [[erased]] instead:
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// Owner resolution for entity-lifecycle events. The live write path
|
|
29
29
|
// (event-store-executor-write.ts) resolves the same chain via
|
|
30
30
|
// subject-resolver.ts: row owner, then the owner the same event sets, then
|
|
31
|
-
// the field's whenAbsent
|
|
31
|
+
// the field's whenAbsent. Backfill adds two opt-in stages and
|
|
32
32
|
// delays whenAbsent behind the projection so a known owner is never shadowed:
|
|
33
33
|
// 1. payload — the event section (plus the owner the same event sets) names
|
|
34
34
|
// the owner field; whenAbsent is not applied yet.
|
package/dist/db/queries/ddl.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function alterTableAddColumn(db, tableName, columnName, columnType,
|
|
|
12
12
|
// throwing when a column already exists with a different shape than
|
|
13
13
|
// EntityTableMeta expects — that's how it surfaces drift. Callers that
|
|
14
14
|
// just need an idempotent, race-safe backfill (e.g. event-consumer-state's
|
|
15
|
-
// multi-instance boot path
|
|
15
|
+
// multi-instance boot path) opt in explicitly.
|
|
16
16
|
ifNotExists = false) {
|
|
17
17
|
await asRawClient(db).unsafe(`ALTER TABLE ${quoteTableIdent(tableName)} ADD COLUMN ${ifNotExists ? "IF NOT EXISTS " : ""}${quoteTableIdent(columnName)} ${columnType}${defaultClause}${notNull}`);
|
|
18
18
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnyDb } from "../query.ts";
|
|
2
|
-
/**
|
|
2
|
+
/** Stable lock key for pg_advisory_xact_lock across multi-replica seed boots. */
|
|
3
3
|
export declare const ES_OPS_ADVISORY_LOCK_KEY = 1702063984;
|
|
4
4
|
export declare function acquireEsOpsAdvisoryLock(db: AnyDb): Promise<void>;
|
|
5
5
|
export declare function esOperationExists(db: AnyDb, operationId: string): Promise<boolean>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { asRawClient } from "../query.js";
|
|
2
|
-
/**
|
|
2
|
+
/** Stable lock key for pg_advisory_xact_lock across multi-replica seed boots. */
|
|
3
3
|
export const ES_OPS_ADVISORY_LOCK_KEY = 0x65_73_6f_70; // 'esop'
|
|
4
4
|
export async function acquireEsOpsAdvisoryLock(db) {
|
|
5
5
|
await asRawClient(db).unsafe(`SELECT pg_advisory_xact_lock($1)`, [ES_OPS_ADVISORY_LOCK_KEY]);
|
|
@@ -24,7 +24,7 @@ export async function lockEventConsumersShareMode(db) {
|
|
|
24
24
|
await asRawClient(db).unsafe(`LOCK TABLE "kumiko_event_consumers" IN SHARE MODE`);
|
|
25
25
|
}
|
|
26
26
|
// The MAX(id) horizon plus every currently-open id gap below it, read from a
|
|
27
|
-
// single statement snapshot. Used both to seed startFrom "now"
|
|
27
|
+
// single statement snapshot. Used both to seed startFrom "now" and to
|
|
28
28
|
// seed an MSP rebuild's handoff to the live dispatcher (msp-rebuild.ts).
|
|
29
29
|
//
|
|
30
30
|
// Scan lower bound: the consumer with the highest cursor already tracks every
|
|
@@ -84,7 +84,7 @@ export async function selectEventIdHorizonWithMissingRanges(db) {
|
|
|
84
84
|
// WithMissingRanges): a transaction holding a lower id can still be
|
|
85
85
|
// in-flight at boot, and without seeding those gaps its event would commit
|
|
86
86
|
// after the cursor already sits above it, so `id > cursor` would never
|
|
87
|
-
// deliver it
|
|
87
|
+
// deliver it. The cheap existence check skips the scan entirely for
|
|
88
88
|
// the (overwhelmingly common) case of an already-registered consumer.
|
|
89
89
|
// ON CONFLICT DO NOTHING keeps both branches safe for an existing row: the
|
|
90
90
|
// subquery only ever affects the row this statement inserts.
|
|
@@ -144,7 +144,7 @@ export async function markConsumerProcessing(db, name, instanceId) {
|
|
|
144
144
|
// repeated infra blips (the exact case this backoff exists for) dead-letter
|
|
145
145
|
// the consumer on the next real handler failure instead of after
|
|
146
146
|
// maxAttempts. Visibility is covered by last_error, updated_at, and the
|
|
147
|
-
// caller's
|
|
147
|
+
// caller's error log — status and the cursor are left as-is too, since
|
|
148
148
|
// we don't know at this point whether the consumer should be "dead" (that's
|
|
149
149
|
// the deliverEvents/maxAttempts contract, which never ran this pass).
|
|
150
150
|
//
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { createFallbackLogger } from "../../logging/utils.js";
|
|
1
2
|
import { constraintOf, isLockNotAvailable, isTableAlreadyExists, isUniqueViolation, } from "../pg-error.js";
|
|
2
3
|
import { asRawClient, runInNewTransaction, runnerCanBeginTransaction, unsafeReadRetrying, } from "../query.js";
|
|
3
4
|
import { withoutStatementTimeout } from "../statement-timeout.js";
|
|
4
5
|
import { toPostgresArrayLiteral } from "./postgres-array-literal.js";
|
|
6
|
+
const log = createFallbackLogger("event-store");
|
|
5
7
|
// Gap-finality (event-dispatcher pending_gaps) needs every holder of an
|
|
6
8
|
// event id to already have a *real* xact id by the time it inserts — Postgres
|
|
7
9
|
// assigns those lazily on first write, so a bare INSERT alone doesn't
|
|
@@ -81,9 +83,9 @@ async function buildIdempotencyKeyIndex(db) {
|
|
|
81
83
|
// skip: sibling pod already built a valid index — this race loser is done.
|
|
82
84
|
if ((await indexValidity(client)) === true)
|
|
83
85
|
return;
|
|
84
|
-
|
|
86
|
+
log.warn(`ensureIdempotencyKeyIndex: backed off on a "benign" race but ` +
|
|
85
87
|
`"events_idempotency_uq" is still missing/invalid afterward — likely a ` +
|
|
86
|
-
`lock_timeout during CREATE INDEX CONCURRENTLY, not an actual winner/loser race
|
|
88
|
+
`lock_timeout during CREATE INDEX CONCURRENTLY, not an actual winner/loser race.`, { err: e });
|
|
87
89
|
throw e;
|
|
88
90
|
}
|
|
89
91
|
throw duplicateIdempotencyKeyErrorOr(e);
|
|
@@ -14,7 +14,7 @@ export async function markProjectionRebuilding(db, projectionName) {
|
|
|
14
14
|
export async function selectEventsForProjectionRebuildBatch(db, aggregateTypes, eventTypes, afterId, limit) {
|
|
15
15
|
// Archived streams don't replay (Marten-aligned): their aggregates are
|
|
16
16
|
// frozen ops-tombstones — replaying them would resurrect rows (or, for
|
|
17
|
-
// stranded duplicate-aggregates
|
|
17
|
+
// stranded duplicate-aggregates, collide on unique indexes).
|
|
18
18
|
return unsafeReadRetrying(db, `SELECT * FROM "kumiko_events" e
|
|
19
19
|
WHERE e."aggregate_type" = ANY($1::text[])
|
|
20
20
|
AND e."type" = ANY($2::text[])
|
|
@@ -29,9 +29,9 @@ export async function selectEventsForProjectionRebuildBatch(db, aggregateTypes,
|
|
|
29
29
|
// Total subscribed events in the log — same source/type filter as
|
|
30
30
|
// selectEventsForProjectionRebuildBatch, no id cursor. Under the cutover fence
|
|
31
31
|
// this is the ground-truth count the rebuild must have applied; a shortfall
|
|
32
|
-
// means a lower-id event committed late and the id-cursor leapt past it
|
|
32
|
+
// means a lower-id event committed late and the id-cursor leapt past it.
|
|
33
33
|
export async function countSubscribedEvents(db, aggregateTypes, eventTypes) {
|
|
34
|
-
// Same archived-streams exclusion as the batch query — the
|
|
34
|
+
// Same archived-streams exclusion as the batch query — the recompute
|
|
35
35
|
// compares this count against applied events; a filter mismatch would make
|
|
36
36
|
// every rebuild with an archived stream loop the full-re-replay forever.
|
|
37
37
|
const rows = await unsafeReadRetrying(db, `SELECT count(*)::bigint AS n FROM "kumiko_events" e
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AnyDb } from "../query.ts";
|
|
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 declare function executeRawQuery<T = Record<string, unknown>>(db: AnyDb, sqlText: string, params?: readonly unknown[]): Promise<readonly T[]>;
|
|
6
|
-
/** SELECT-only escape hatch with the
|
|
6
|
+
/** SELECT-only escape hatch with the closed-connection retry. Do not pass
|
|
7
7
|
* INSERT/UPDATE/DELETE — retry re-executes the statement. */
|
|
8
8
|
export declare function executeRawQueryRead<T = Record<string, unknown>>(db: AnyDb, sqlText: string, params?: readonly unknown[]): Promise<readonly T[]>;
|
|
9
9
|
export declare function pingDatabase(db: AnyDb): Promise<void>;
|