@cosmicdrift/kumiko-framework 0.386.0 → 0.388.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -1
- package/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +40 -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-error-route.js +8 -2
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -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 +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +203 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +293 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +236 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1250
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +31 -27
- 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/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +7 -5
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/aggregate-sql.d.ts +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- 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/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- 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 +3 -3
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +99 -0
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +105 -112
- package/dist/db/event-store-executor-context.d.ts +3 -2
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +112 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +203 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +50 -704
- package/dist/db/event-store-executor-write-steps.d.ts +20 -0
- package/dist/db/event-store-executor-write-steps.js +100 -0
- package/dist/db/event-store-executor-write.js +401 -510
- package/dist/db/index.d.ts +6 -2
- package/dist/db/index.js +5 -1
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- 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 +97 -105
- package/dist/db/migrate-runner.js +29 -26
- 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 +3 -16
- package/dist/db/query.js +8 -761
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/reference-data.js +6 -1
- 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 +137 -190
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +48 -11
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +6 -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/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +4 -4
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +77 -61
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +6 -4
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +22 -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 +258 -187
- 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.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +23 -17
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +12 -17
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +26 -24
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +102 -869
- 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 +130 -170
- 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 +21 -13
- package/dist/engine/define-handler.js +13 -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.d.ts +13 -0
- package/dist/engine/entity-handlers.js +22 -34
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- 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 +2 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- 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 +4 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
- package/dist/engine/feature-ast/patterns.d.ts +11 -2
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +50 -89
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +12 -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/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- 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.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +7 -5
- 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 +13 -13
- 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 +251 -233
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- 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/strip-disabled-features-from-app-schema.js +51 -14
- 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 +9 -28
- 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.d.ts +4 -1
- package/dist/entrypoint/index.js +10 -4
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- 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 +29 -29
- 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 +7 -8
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +12 -6
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +7 -9
- 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 +22 -13
- 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/resolve-content-type.js +43 -0
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/upload-ledger.js +7 -0
- 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 +188 -212
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +69 -4
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +57 -669
- 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/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- 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/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +20 -13
- package/dist/pipeline/dispatch-shared.d.ts +14 -3
- package/dist/pipeline/dispatch-shared.js +67 -721
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +297 -190
- 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-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +4 -6
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
- package/dist/pipeline/handler-context-dispatch.js +89 -0
- package/dist/pipeline/handler-context-events.d.ts +43 -0
- package/dist/pipeline/handler-context-events.js +237 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- 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/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +15 -13
- 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/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -510
- package/dist/search/index.js +4 -4
- package/dist/search/meilisearch-adapter.js +2 -1
- 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 +159 -130
- package/dist/utils/compare.js +1 -1
- package/package.json +5 -5
- package/src/changes.json +358 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/feature-layout.ts +10 -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/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +192 -153
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -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))
|
|
@@ -37,7 +37,12 @@ export async function seedReferenceData(defs, registry, db) {
|
|
|
37
37
|
const keyValue = row[upsertKey];
|
|
38
38
|
if (keyValue === undefined)
|
|
39
39
|
continue;
|
|
40
|
-
|
|
40
|
+
// Soft-deleted rows are invisible to executor.update, so treat them as missing and re-create.
|
|
41
|
+
const existing = await fetchOne(db, table, {
|
|
42
|
+
[upsertKey]: keyValue,
|
|
43
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
44
|
+
...(entity.softDelete && { isDeleted: false }),
|
|
45
|
+
});
|
|
41
46
|
if (existing) {
|
|
42
47
|
const changes = {};
|
|
43
48
|
for (const [field, value] of Object.entries(row)) {
|
|
@@ -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>;
|
package/dist/db/table-builder.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
2
2
|
import { toSnakeCase } from "../utils/case.js";
|
|
3
|
-
import { assertUnreachable } from "../utils/index.js";
|
|
4
3
|
import { markExecutorManagedTable } from "./executor-managed-tables.js";
|
|
5
4
|
import { columnHandleName } from "./table-introspection.js";
|
|
6
5
|
export { toSnakeCase } from "../utils/case.js";
|
|
@@ -13,173 +12,120 @@ export const SOFT_DELETE_LIVE_ROW_PREDICATE = `"is_deleted" = false`;
|
|
|
13
12
|
export function physicalColumnName(table, field) {
|
|
14
13
|
return columnHandleName(table, field) ?? toSnakeCase(field);
|
|
15
14
|
}
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// SQL keinen DEFAULT-clause (bricht ALTER TABLE ADD COLUMN auf
|
|
36
|
-
// existing rows). longText hat heute kein default-feld im type,
|
|
37
|
-
// aber der check `field.default !== undefined` ist defensive.
|
|
38
|
-
const base = text(snakeName);
|
|
39
|
-
const withDefault = "default" in field && field.default !== undefined ? base.default(field.default) : base;
|
|
40
|
-
const mainCol = { [name]: field.required ? withDefault.notNull() : withDefault };
|
|
41
|
-
// lookupable → nullable HMAC-Blind-Index-Spalte (lock-step mit
|
|
42
|
-
// fieldToColumnMeta in entity-table-meta.ts).
|
|
43
|
-
if (field.type === "text" && field.lookupable === true) {
|
|
44
|
-
return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
|
|
45
|
-
}
|
|
46
|
-
return mainCol;
|
|
47
|
-
}
|
|
48
|
-
case "boolean":
|
|
49
|
-
return {
|
|
50
|
-
[name]: field.default !== undefined
|
|
51
|
-
? boolean(snakeName).default(field.default).notNull()
|
|
52
|
-
: field.required
|
|
53
|
-
? boolean(snakeName).notNull()
|
|
54
|
-
: boolean(snakeName),
|
|
55
|
-
};
|
|
56
|
-
case "select": {
|
|
57
|
-
// default() durchreichen — entity-table-meta rendert deklarierte
|
|
58
|
-
// defaults, dieser Builder MUSS lock-step bleiben (sonst trägt das
|
|
59
|
-
// Meta am buildEntityTable-Objekt eine andere Spalte als die
|
|
60
|
-
// Migration, #255-Follow-up-Befund).
|
|
61
|
-
const base = text(snakeName);
|
|
62
|
-
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
63
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
64
|
-
}
|
|
65
|
-
case "multiSelect":
|
|
66
|
-
// jsonb-Array<string> mit Default `[]` und immer NOT NULL.
|
|
67
|
-
//
|
|
68
|
-
// Der `required`-Flag auf MultiSelectFieldDef wird hier bewusst
|
|
69
|
-
// ignoriert: Mit Default `[]` ist das Feld strukturell never-null
|
|
70
|
-
// (Insert ohne Wert → leeres Array, nicht NULL). Read-Side-Code
|
|
71
|
-
// braucht keinen null-check, das ist API-Garantie. Wer "wirklich
|
|
72
|
-
// null" will (= "Feld noch nie gesetzt") nutzt einen separaten
|
|
73
|
-
// Status-Field oder ein optional-typed reference statt eines
|
|
74
|
-
// multi-select.
|
|
75
|
-
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
76
|
-
case "number": {
|
|
77
|
-
const base = field.integer === true ? integer(snakeName) : doublePrecision(snakeName);
|
|
78
|
-
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
79
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
80
|
-
}
|
|
81
|
-
case "decimal": {
|
|
82
|
-
const base = decimalColumn(snakeName, field.precision, field.scale);
|
|
83
|
-
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
84
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
85
|
-
}
|
|
86
|
-
case "bigInt": {
|
|
87
|
-
// 64-bit-Integer fuer Audit-Counter, Byte-Sizes, Cumulative-Sums.
|
|
88
|
-
// mode:"number" liefert JS-`number` (sicher bis 2^53 ≈ 9 PB) statt
|
|
89
|
-
// JS-`bigint` — JSON-serialisierbar, Frontend-tauglich. Wer >2^53
|
|
90
|
-
// braucht (Astronomie-Astronomie), nutzt einen Text-Field mit
|
|
91
|
-
// eigenem Codec.
|
|
92
|
-
const base = bigint(snakeName, { mode: "number" });
|
|
93
|
-
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
94
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
95
|
-
}
|
|
96
|
-
case "reference":
|
|
97
|
-
// Tier 2.7e-3: FK-Style UUID-Spalte. Multi-Mode (Tier 2.7e-Multi)
|
|
98
|
-
// speichert UUIDs als jsonb-Array<string>. Single-Mode bleibt
|
|
99
|
-
// klassische UUID-Spalte (NOT NULL nur bei required).
|
|
100
|
-
if (field.multiple === true) {
|
|
101
|
-
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
[name]: field.required ? uuid(snakeName).notNull() : uuid(snakeName),
|
|
105
|
-
};
|
|
106
|
-
case "money":
|
|
107
|
-
// BIGINT storing the integer minor unit (cents for EUR, yen for JPY —
|
|
108
|
-
// the currency column tells you which). INTEGER would cap at ~21 M EUR
|
|
109
|
-
// which is too tight for B2B invoices, property values or balance
|
|
110
|
-
// aggregates. BIGINT handles up to ~90 trillion EUR safely in JS.
|
|
111
|
-
// Currency hat immer einen Default, ist also strukturell .notNull().
|
|
112
|
-
return {
|
|
113
|
-
[name]: field.required ? moneyAmount(snakeName).notNull() : moneyAmount(snakeName),
|
|
114
|
-
[`${name}Currency`]: text(`${snakeName}_currency`)
|
|
115
|
-
.default(entity.defaultCurrency ?? "EUR")
|
|
116
|
-
.notNull(),
|
|
117
|
-
};
|
|
118
|
-
case "embedded":
|
|
119
|
-
// jsonb mit default `{}` und immer NOT NULL — analog zu multiSelect.
|
|
120
|
-
// `required` wird bewusst ignoriert weil der Default das Feld
|
|
121
|
-
// strukturell never-null macht. Wer optional-embedded möchte (=
|
|
122
|
-
// "Feld komplett weglassen können") modelliert das über ein
|
|
123
|
-
// wrapper-feld mit boolean-flag oder discriminierte-union.
|
|
124
|
-
// multiple → the value is a list of rows, so the empty default is `[]`.
|
|
125
|
-
return field.multiple === true
|
|
126
|
-
? { [name]: jsonb(snakeName).default([]).notNull() }
|
|
127
|
-
: { [name]: jsonb(snakeName).default({}).notNull() };
|
|
128
|
-
case "jsonb":
|
|
129
|
-
// Free-form jsonb — keys nicht schema-validated. Default `{}`, NOT NULL
|
|
130
|
-
// (analog zu embedded). Use-case: custom-fields-Bundle's host-entity-
|
|
131
|
-
// `customFields`-Spalte (tenant-definierte dynamische keys).
|
|
132
|
-
return { [name]: jsonb(snakeName).default({}).notNull() };
|
|
133
|
-
case "date": {
|
|
134
|
-
// Real PG `date` — no time-of-day, no timezone. Temporal.PlainDate
|
|
135
|
-
// end-to-end (see plainDate() in dialect.ts). Was aliased on instant()
|
|
136
|
-
// = TIMESTAMPTZ until kumiko-framework#1924 (TZ-dependent round-trip).
|
|
137
|
-
const col = plainDate(snakeName);
|
|
138
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
139
|
-
}
|
|
140
|
-
case "timestamp": {
|
|
141
|
-
// UTC-Instant — gespeichert als TIMESTAMPTZ in PG, gelesen/geschrieben
|
|
142
|
-
// als Temporal.Instant via instant() customType (siehe dialect.ts).
|
|
143
|
-
// Sprint F: Single-Mode-Welt — Caller-Code kennt nur Temporal.Instant,
|
|
144
|
-
// nie JS-Date. Auch Vergleiche (lte/gt/orderBy) akzeptieren Instants
|
|
145
|
-
// direkt, kein .toString()-Cast nötig.
|
|
146
|
-
const col = instant(snakeName);
|
|
147
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
148
|
-
}
|
|
149
|
-
case "tz": {
|
|
150
|
-
// IANA-Zonenname als TEXT — Validierung über Zod-Schema (kommt im
|
|
151
|
-
// Validator-Schritt). Snake-Convention: `pickup_tz`.
|
|
152
|
-
const col = text(snakeName);
|
|
153
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
154
|
-
}
|
|
155
|
-
case "locatedTimestamp": {
|
|
156
|
-
// ZWEI Spalten als atomares Pair: <name>_utc TIMESTAMPTZ + <name>_tz TEXT.
|
|
157
|
-
// _utc ist instant() (Temporal.Instant), _tz ist text (IANA-Name).
|
|
158
|
-
// Auto-Convert (at+tz → utc beim Insert; utc+tz → at beim Read) wird
|
|
159
|
-
// im Executor verdrahtet (Phase C). required propagiert auf beide.
|
|
160
|
-
const utc = instant(`${snakeName}_utc`);
|
|
161
|
-
const tz = text(`${snakeName}_tz`);
|
|
162
|
-
return {
|
|
163
|
-
[`${name}Utc`]: field.required ? utc.notNull() : utc,
|
|
164
|
-
[`${name}Tz`]: field.required ? tz.notNull() : tz,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
case "file":
|
|
168
|
-
case "image": {
|
|
169
|
-
// Single file: stores fileRefId as UUID — must match fileRefsTable.id
|
|
170
|
-
// (uuid column). Anything narrower (integer, text length-limited) would
|
|
171
|
-
// silently truncate or type-coerce at INSERT time and the FK reference
|
|
172
|
-
// would be unusable.
|
|
173
|
-
const col = uuid(snakeName);
|
|
174
|
-
return { [name]: field.required ? col.notNull() : col };
|
|
175
|
-
}
|
|
176
|
-
case "files":
|
|
177
|
-
case "images":
|
|
178
|
-
// Ordered fileRef UUID array, same shape as multiSelect: never null.
|
|
179
|
-
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
180
|
-
default:
|
|
181
|
-
assertUnreachable(field, "field type");
|
|
15
|
+
// `required: true` maps to NOT NULL for every field type, so r.entity stays the
|
|
16
|
+
// single source of truth and hand-written tables never add .notNull() on top.
|
|
17
|
+
function notNullWhenRequired(column, required) {
|
|
18
|
+
return required ? column.notNull() : column;
|
|
19
|
+
}
|
|
20
|
+
// Without a DEFAULT clause ALTER TABLE ADD COLUMN breaks on existing rows, and
|
|
21
|
+
// entity-table-meta renders declared defaults too, so both builders stay in lockstep.
|
|
22
|
+
function withDeclaredDefault(column, value) {
|
|
23
|
+
return value !== undefined ? column.default(value) : column;
|
|
24
|
+
}
|
|
25
|
+
// longText differs from text only in the type layer (no sortable/searchable/filterable).
|
|
26
|
+
function textColumns(name, snakeName, field) {
|
|
27
|
+
const declaredDefault = "default" in field ? field.default : undefined;
|
|
28
|
+
const mainCol = {
|
|
29
|
+
[name]: notNullWhenRequired(withDeclaredDefault(text(snakeName), declaredDefault), field.required),
|
|
30
|
+
};
|
|
31
|
+
// Nullable HMAC blind-index column, in lockstep with fieldToColumnMeta in entity-table-meta.ts.
|
|
32
|
+
if (field.type === "text" && field.lookupable === true) {
|
|
33
|
+
return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
|
|
182
34
|
}
|
|
35
|
+
return mainCol;
|
|
36
|
+
}
|
|
37
|
+
function booleanColumns(name, snakeName, field) {
|
|
38
|
+
if (field.default !== undefined) {
|
|
39
|
+
return { [name]: boolean(snakeName).default(field.default).notNull() };
|
|
40
|
+
}
|
|
41
|
+
return { [name]: notNullWhenRequired(boolean(snakeName), field.required) };
|
|
42
|
+
}
|
|
43
|
+
function numberColumns(name, snakeName, field) {
|
|
44
|
+
const base = field.integer === true ? integer(snakeName) : doublePrecision(snakeName);
|
|
45
|
+
return { [name]: notNullWhenRequired(withDeclaredDefault(base, field.default), field.required) };
|
|
46
|
+
}
|
|
47
|
+
function referenceColumns(name, snakeName, field) {
|
|
48
|
+
if (field.multiple === true)
|
|
49
|
+
return { [name]: emptyJsonbArray(snakeName) };
|
|
50
|
+
return { [name]: notNullWhenRequired(uuid(snakeName), field.required) };
|
|
51
|
+
}
|
|
52
|
+
// BIGINT minor units: INTEGER would cap at about 21 M EUR. The currency column
|
|
53
|
+
// always has a default, so it is structurally NOT NULL.
|
|
54
|
+
function moneyColumns(name, snakeName, field, entity) {
|
|
55
|
+
return {
|
|
56
|
+
[name]: notNullWhenRequired(moneyAmount(snakeName), field.required),
|
|
57
|
+
[`${name}Currency`]: text(`${snakeName}_currency`)
|
|
58
|
+
.default(entity.defaultCurrency ?? "EUR")
|
|
59
|
+
.notNull(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// <name>_utc + <name>_tz form an atomic pair the executor converts on write and
|
|
63
|
+
// read, so `required` propagates to both columns.
|
|
64
|
+
function locatedTimestampColumns(name, snakeName, field) {
|
|
65
|
+
return {
|
|
66
|
+
[`${name}Utc`]: notNullWhenRequired(instant(`${snakeName}_utc`), field.required),
|
|
67
|
+
[`${name}Tz`]: notNullWhenRequired(text(`${snakeName}_tz`), field.required),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// The `[]` default makes the column structurally never null, so `required` is
|
|
71
|
+
// intentionally ignored and read-side code needs no null check.
|
|
72
|
+
function emptyJsonbArray(snakeName) {
|
|
73
|
+
return jsonb(snakeName).default([]).notNull();
|
|
74
|
+
}
|
|
75
|
+
// Same reasoning as emptyJsonbArray; optional embedded data needs a wrapper field.
|
|
76
|
+
function emptyJsonbObject(snakeName) {
|
|
77
|
+
return jsonb(snakeName).default({}).notNull();
|
|
78
|
+
}
|
|
79
|
+
function singleColumnNotNullWhenRequired(column) {
|
|
80
|
+
return (name, snakeName, field) => ({
|
|
81
|
+
[name]: notNullWhenRequired(column(snakeName), field.required),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Each entry has a type-level twin in ColumnsForField and a meta-level twin in
|
|
85
|
+
// fieldToColumnMeta (entity-table-meta.ts).
|
|
86
|
+
const COLUMN_EMITTERS = {
|
|
87
|
+
text: textColumns,
|
|
88
|
+
longText: textColumns,
|
|
89
|
+
boolean: booleanColumns,
|
|
90
|
+
select: (name, snakeName, field) => ({
|
|
91
|
+
[name]: notNullWhenRequired(withDeclaredDefault(text(snakeName), field.default), field.required),
|
|
92
|
+
}),
|
|
93
|
+
multiSelect: (name, snakeName) => ({ [name]: emptyJsonbArray(snakeName) }),
|
|
94
|
+
number: numberColumns,
|
|
95
|
+
decimal: (name, snakeName, field) => ({
|
|
96
|
+
[name]: notNullWhenRequired(withDeclaredDefault(decimalColumn(snakeName, field.precision, field.scale), field.default), field.required),
|
|
97
|
+
}),
|
|
98
|
+
// mode "number" keeps the value JSON-serializable (safe up to 2^53).
|
|
99
|
+
bigInt: (name, snakeName, field) => ({
|
|
100
|
+
[name]: notNullWhenRequired(withDeclaredDefault(bigint(snakeName, { mode: "number" }), field.default), field.required),
|
|
101
|
+
}),
|
|
102
|
+
reference: referenceColumns,
|
|
103
|
+
money: moneyColumns,
|
|
104
|
+
embedded: (name, snakeName, field) => ({
|
|
105
|
+
[name]: field.multiple === true ? emptyJsonbArray(snakeName) : emptyJsonbObject(snakeName),
|
|
106
|
+
}),
|
|
107
|
+
jsonb: (name, snakeName) => ({ [name]: emptyJsonbObject(snakeName) }),
|
|
108
|
+
date: singleColumnNotNullWhenRequired(plainDate),
|
|
109
|
+
timestamp: singleColumnNotNullWhenRequired(instant),
|
|
110
|
+
tz: singleColumnNotNullWhenRequired(text),
|
|
111
|
+
locatedTimestamp: locatedTimestampColumns,
|
|
112
|
+
// Must match fileRefsTable.id (uuid); anything narrower breaks the reference.
|
|
113
|
+
file: singleColumnNotNullWhenRequired(uuid),
|
|
114
|
+
image: singleColumnNotNullWhenRequired(uuid),
|
|
115
|
+
files: (name, snakeName) => ({ [name]: emptyJsonbArray(snakeName) }),
|
|
116
|
+
images: (name, snakeName) => ({ [name]: emptyJsonbArray(snakeName) }),
|
|
117
|
+
};
|
|
118
|
+
function emitFieldColumns(type, name, field, entity) {
|
|
119
|
+
// A type from untyped input must not reach an Object.prototype member.
|
|
120
|
+
if (!Object.hasOwn(COLUMN_EMITTERS, type)) {
|
|
121
|
+
throw new Error(`[Kumiko] unhandled field type: ${String(field)}`);
|
|
122
|
+
}
|
|
123
|
+
const emit = COLUMN_EMITTERS[type];
|
|
124
|
+
return emit(name, toSnakeCase(name), field, entity);
|
|
125
|
+
}
|
|
126
|
+
// Most fields return one column; money and locatedTimestamp return two.
|
|
127
|
+
function fieldToColumns(name, field, entity) {
|
|
128
|
+
return emitFieldColumns(field.type, name, field, entity);
|
|
183
129
|
}
|
|
184
130
|
// Accepts both camelCase (`tenantMembership`) and kebab-case (`tenant-membership`)
|
|
185
131
|
// entity / field names. Implementation lives in utils/case — re-exported here
|
|
@@ -188,12 +134,12 @@ function fieldToColumns(name, field, entity) {
|
|
|
188
134
|
* Derives a table name from an entity name:
|
|
189
135
|
* 1. camelCase → snake_case (e.g. "memberTask" → "member_task")
|
|
190
136
|
* 2. Simple English pluralization (category→categories, status→statuses, task→tasks)
|
|
191
|
-
* 3. `read_` prefix —
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* (
|
|
195
|
-
*
|
|
196
|
-
*
|
|
137
|
+
* 3. `read_` prefix — marks the table as an event-sourced read model so
|
|
138
|
+
* it is immediately distinguishable in database tools and application
|
|
139
|
+
* code is not expected to write to it directly. The event store
|
|
140
|
+
* (`kumiko_events`) and framework state (`kumiko_*`) use their own
|
|
141
|
+
* prefixes; ordinary app-side tables without event-sourcing have none.
|
|
142
|
+
* The three categories are therefore distinguishable in the table browser.
|
|
197
143
|
*/
|
|
198
144
|
const ES_PLURAL_SUFFIXES = ["s", "sh", "ch", "x"];
|
|
199
145
|
export const READ_MODEL_PREFIX = "read_";
|
|
@@ -219,12 +165,13 @@ export function buildBaseColumns(softDelete, idType = "uuid") {
|
|
|
219
165
|
id: idColumn,
|
|
220
166
|
tenantId: uuid("tenant_id").notNull(),
|
|
221
167
|
version: integer("version").default(1).notNull(),
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
168
|
+
// Temporal.Instant is used throughout (see instant() in dialect.ts).
|
|
169
|
+
// Previously the default mode was "date", which was inconsistent with
|
|
170
|
+
// user-defined timestamp fields (mode "string"). A single mode is now
|
|
171
|
+
// used for all timestamps.
|
|
225
172
|
// customType doesn't expose Drizzle's `defaultNow()` shortcut — use raw
|
|
226
|
-
// SQL so PG sets the value on insert and
|
|
227
|
-
// Instant
|
|
173
|
+
// SQL so PG sets the value on insert and callers need not pass an
|
|
174
|
+
// Instant for every row creation.
|
|
228
175
|
insertedAt: instant("inserted_at").default(sql `now()`).notNull(),
|
|
229
176
|
modifiedAt: instant("modified_at"),
|
|
230
177
|
// User-IDs are stringified UUIDs post-ES migration. Text (not uuid) so the
|
|
@@ -295,9 +242,9 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
295
242
|
}
|
|
296
243
|
// Default table name derived from entityName (e.g. "memberTask" → "read_member_tasks")
|
|
297
244
|
const baseTableName = entity.table ?? toTableName(entityName);
|
|
298
|
-
// featureName
|
|
299
|
-
//
|
|
300
|
-
// featureName
|
|
245
|
+
// The featureName prefix is inserted between read_ and the base name so
|
|
246
|
+
// all read models consistently start with `read_`, whether or not
|
|
247
|
+
// featureName is set. Examples:
|
|
301
248
|
// featureName="shop", base="read_orders" → "read_shop_orders"
|
|
302
249
|
// featureName=undef, base="read_orders" → "read_orders"
|
|
303
250
|
// featureName="shop", base="orders" (no read_) → "shop_orders"
|
|
@@ -349,7 +296,7 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
349
296
|
indexes[`${tableName}_${toSnakeCase(fieldName)}_idx`] = index(`${tableName}_${toSnakeCase(fieldName)}_idx`).on(column);
|
|
350
297
|
}
|
|
351
298
|
}
|
|
352
|
-
// lookupable
|
|
299
|
+
// lookupable fields: index the bidx column (kept in lockstep with
|
|
353
300
|
// deriveEntityTableMeta).
|
|
354
301
|
const bidxFieldByField = new Map();
|
|
355
302
|
for (const [name, field] of Object.entries(entity.fields)) {
|
|
@@ -362,10 +309,10 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
362
309
|
indexes[idxName] = index(idxName).on(column);
|
|
363
310
|
}
|
|
364
311
|
}
|
|
365
|
-
// entity.indexes
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
312
|
+
// entity.indexes contains composite/unique indexes explicitly declared by
|
|
313
|
+
// the author. Columns are addressed by field name (camelCase), and the
|
|
314
|
+
// index name follows <table>_<col1>_<col2>_<unique|idx>; index.name can
|
|
315
|
+
// override the convention.
|
|
369
316
|
for (const def of entity.indexes ?? []) {
|
|
370
317
|
const cols = def.columns
|
|
371
318
|
.map((fieldName) => tHandle[fieldName])
|
|
@@ -388,12 +335,12 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
388
335
|
}
|
|
389
336
|
else if (def.unique === true && entity.softDelete === true) {
|
|
390
337
|
// Soft-deleted rows keep their value, so a full unique index would
|
|
391
|
-
// permanently block reuse; kept in lock-step with entity-table-meta.ts
|
|
338
|
+
// permanently block reuse; kept in lock-step with entity-table-meta.ts.
|
|
392
339
|
chain = chain.where(sql.raw(SOFT_DELETE_LIVE_ROW_PREDICATE));
|
|
393
340
|
}
|
|
394
341
|
indexes[indexName] = chain;
|
|
395
|
-
//
|
|
396
|
-
// (
|
|
342
|
+
// Partial bidx counterpart for unique indexes over lookupable fields
|
|
343
|
+
// (kept in lockstep with deriveEntityTableMeta).
|
|
397
344
|
if (def.unique === true && def.where === undefined) {
|
|
398
345
|
const bidxFieldNames = def.columns.map((c) => bidxFieldByField.get(c) ?? c);
|
|
399
346
|
if (bidxFieldNames.some((c, i) => c !== def.columns[i])) {
|
|
@@ -406,7 +353,7 @@ export function buildEntityTable(entityName, entity, options) {
|
|
|
406
353
|
.map((c) => `"${toSnakeCase(c)}" IS NOT NULL`);
|
|
407
354
|
// Soft-deleted rows keep their bidx hash — without this, a
|
|
408
355
|
// restored row can't reuse a value a soft-deleted sibling still
|
|
409
|
-
// holds
|
|
356
|
+
// holds.
|
|
410
357
|
const whereText = (entity.softDelete === true
|
|
411
358
|
? [...notNullParts, SOFT_DELETE_LIVE_ROW_PREDICATE]
|
|
412
359
|
: notNullParts).join(" AND ");
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TableLike } from "./query.ts";
|
|
2
|
+
export type TableInfo = {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly columnOf: (field: string) => string;
|
|
5
|
+
readonly pgTypeOf: (column: string) => string | undefined;
|
|
6
|
+
readonly bigintJsModeOf: (column: string) => "number" | "bigint" | undefined;
|
|
7
|
+
readonly fieldOf: (column: string) => string;
|
|
8
|
+
readonly hasColumn: (fieldOrColumn: string) => boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function extractTableInfo(table: TableLike): TableInfo;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { camelCase as envCamelCase } from "../env/index.js";
|
|
2
|
+
import { toSnakeCase } from "./table-builder.js";
|
|
3
|
+
import { asEntityTableMeta } from "./table-introspection.js";
|
|
4
|
+
// Idempotent snake_case → camelCase. `env.camelCase` always lowercases first
|
|
5
|
+
// (designed for SHOUT_CASE input) — for already-camelCase keys (mock rows
|
|
6
|
+
// in tests, projection-aliased columns) it would silently produce "tenantid"
|
|
7
|
+
// instead of leaving "tenantId" alone. Guard with an underscore check so
|
|
8
|
+
// the conversion only fires when the key is actually snake-shaped.
|
|
9
|
+
function snakeToCamel(key) {
|
|
10
|
+
if (!key.includes("_"))
|
|
11
|
+
return key;
|
|
12
|
+
return envCamelCase(key);
|
|
13
|
+
}
|
|
14
|
+
export function extractTableInfo(table) {
|
|
15
|
+
const meta = asEntityTableMeta(table);
|
|
16
|
+
if (!meta) {
|
|
17
|
+
throw new Error("extractTableInfo: table is not a kumiko EntityTableMeta — " +
|
|
18
|
+
"build it via buildEntityTable / deriveEntityTableMeta / table().");
|
|
19
|
+
}
|
|
20
|
+
const colByField = new Map();
|
|
21
|
+
const fieldByCol = new Map();
|
|
22
|
+
const typeByCol = new Map();
|
|
23
|
+
const bigintModeByCol = new Map();
|
|
24
|
+
for (const c of meta.columns) {
|
|
25
|
+
typeByCol.set(c.name, c.pgType);
|
|
26
|
+
if (c.bigintJsMode !== undefined)
|
|
27
|
+
bigintModeByCol.set(c.name, c.bigintJsMode);
|
|
28
|
+
// EntityTableMeta column names are snake_case. Map snake → snake AND
|
|
29
|
+
// derive a camelCase JS field-name so result rows can be renamed back
|
|
30
|
+
// to the API shape (`aggregate_id` → `aggregateId`).
|
|
31
|
+
colByField.set(c.name, c.name);
|
|
32
|
+
const camel = snakeToCamel(c.name);
|
|
33
|
+
if (camel !== c.name)
|
|
34
|
+
colByField.set(camel, c.name);
|
|
35
|
+
fieldByCol.set(c.name, camel === c.name ? c.name : camel);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
name: meta.tableName,
|
|
39
|
+
columnOf: (field) => colByField.get(field) ?? toSnakeCase(field),
|
|
40
|
+
pgTypeOf: (col) => typeByCol.get(col),
|
|
41
|
+
bigintJsModeOf: (col) => bigintModeByCol.get(col),
|
|
42
|
+
fieldOf: (col) => fieldByCol.get(col) ?? snakeToCamel(col),
|
|
43
|
+
hasColumn: (fieldOrColumn) => colByField.has(fieldOrColumn) || fieldByCol.has(fieldOrColumn),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
2
|
+
export declare function isTemporalInstant(v: unknown): v is Temporal.Instant;
|
|
3
|
+
export declare function isTemporalPlainDate(v: unknown): v is Temporal.PlainDate;
|
|
4
|
+
export declare function isTemporalPlainDateTimeLike(v: unknown): v is {
|
|
5
|
+
toPlainDate(): Temporal.PlainDate;
|
|
6
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
export function isTemporalInstant(v) {
|
|
3
|
+
return propertyOf(v, Symbol.toStringTag) === "Temporal.Instant";
|
|
4
|
+
}
|
|
5
|
+
export function isTemporalPlainDate(v) {
|
|
6
|
+
return propertyOf(v, Symbol.toStringTag) === "Temporal.PlainDate";
|
|
7
|
+
}
|
|
8
|
+
export function isTemporalPlainDateTimeLike(v) {
|
|
9
|
+
const tag = propertyOf(v, Symbol.toStringTag);
|
|
10
|
+
return ((tag === "Temporal.PlainDateTime" || tag === "Temporal.ZonedDateTime") &&
|
|
11
|
+
typeof v === "object" &&
|
|
12
|
+
v !== null &&
|
|
13
|
+
"toPlainDate" in v &&
|
|
14
|
+
typeof v.toPlainDate === "function");
|
|
15
|
+
}
|
|
@@ -6,5 +6,6 @@ export declare function isDbRunner(value: unknown): value is DbRunner;
|
|
|
6
6
|
export declare function bindTenantDbRunner(tenantDb: TenantDb, runner: DbRunner): void;
|
|
7
7
|
export declare function tenantDbRunner(tenantDb: TenantDb): DbRunner;
|
|
8
8
|
export declare function denyTenantDbWrites(tenantDb: TenantDb, reason: string): void;
|
|
9
|
+
export declare function tenantDbWritesDeniedReason(tenantDb: TenantDb): string | undefined;
|
|
9
10
|
export declare function tenantDbWritesDeniedError(operation: string, reason: string): InternalError;
|
|
10
11
|
export declare function tenantDbWriteRunner(tenantDb: TenantDb, operation: string): DbRunner;
|