@cosmicdrift/kumiko-framework 0.1.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/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export { assertExistsIn } from "./assert-exists-in";
|
|
2
|
+
export { flattenCompoundTypes, rehydrateCompoundTypes } from "./compound-types";
|
|
3
|
+
export type { DbConnection, DbConnectionOptions, DbRow, DbRunner, DbTx } from "./connection";
|
|
4
|
+
export { createDbConnection, dbConnectionOptionsFromEnv } from "./connection";
|
|
5
|
+
export type { CursorQueryOptions, CursorResult } from "./cursor";
|
|
6
|
+
export { applyCursorQuery, decodeCursor, encodeCursor } from "./cursor";
|
|
7
|
+
export type { SelectQuery, TableColumns } from "./dialect";
|
|
8
|
+
export {
|
|
9
|
+
boolean,
|
|
10
|
+
instant,
|
|
11
|
+
integer,
|
|
12
|
+
jsonb,
|
|
13
|
+
primaryKey,
|
|
14
|
+
serial,
|
|
15
|
+
table,
|
|
16
|
+
text,
|
|
17
|
+
timestamp,
|
|
18
|
+
uniqueIndex,
|
|
19
|
+
uuid,
|
|
20
|
+
} from "./dialect";
|
|
21
|
+
export type { EagerLoadEntityResolver, EagerloadedRow } from "./eagerload";
|
|
22
|
+
export {
|
|
23
|
+
collectReferenceFields,
|
|
24
|
+
enrichRowWithReferences,
|
|
25
|
+
enrichWithReferences,
|
|
26
|
+
} from "./eagerload";
|
|
27
|
+
export type { EncryptionProvider } from "./encryption";
|
|
28
|
+
export { createEncryptionProvider } from "./encryption";
|
|
29
|
+
export type {
|
|
30
|
+
EntityLifecycleVerb,
|
|
31
|
+
EventStoreExecutor,
|
|
32
|
+
EventStoreExecutorOptions,
|
|
33
|
+
} from "./event-store-executor";
|
|
34
|
+
export { createEventStoreExecutor, entityEventName } from "./event-store-executor";
|
|
35
|
+
export { flattenLocatedTimestamp, rehydrateLocatedTimestamp } from "./located-timestamp";
|
|
36
|
+
export { flattenMoney, rehydrateMoney } from "./money";
|
|
37
|
+
export {
|
|
38
|
+
constraintOf,
|
|
39
|
+
extractPgError,
|
|
40
|
+
isTableAlreadyExists,
|
|
41
|
+
isUniqueViolation,
|
|
42
|
+
type PgErrorInfo,
|
|
43
|
+
} from "./pg-error";
|
|
44
|
+
export { seedReferenceData } from "./reference-data";
|
|
45
|
+
export { fetchOne } from "./row-helpers";
|
|
46
|
+
export { tableExists } from "./schema-inspection";
|
|
47
|
+
export {
|
|
48
|
+
buildBaseColumns,
|
|
49
|
+
buildDrizzleTable,
|
|
50
|
+
type DrizzleTable,
|
|
51
|
+
toSnakeCase,
|
|
52
|
+
toTableName,
|
|
53
|
+
} from "./table-builder";
|
|
54
|
+
export type { TenantDb, TenantDbMode } from "./tenant-db";
|
|
55
|
+
export { castTenantRows, createTenantDb } from "./tenant-db";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Auto-Convert für locatedTimestamp-Felder im DB-Layer.
|
|
2
|
+
//
|
|
3
|
+
// Vertrag (siehe auch db/money.ts — gleicher 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
|
+
//
|
|
8
|
+
// `at`-Default-Sicht beim Read: Pickup-Ort-lokal (utc projiziert in
|
|
9
|
+
// gespeicherter tz). Server kennt User-TZ nicht — User-spezifische
|
|
10
|
+
// Anzeige passiert client-seitig aus utc.
|
|
11
|
+
|
|
12
|
+
import type { EntityDefinition } from "../engine/types";
|
|
13
|
+
|
|
14
|
+
// Sprint F: <name>Utc-Spalte ist jetzt instant() (siehe dialect.ts) —
|
|
15
|
+
// Drizzle gibt direkt Temporal.Instant zurück. Vor Sprint F kam ein PG-
|
|
16
|
+
// Wire-Format-String "2026-04-15 09:00:00+00" rein der via String-Massage
|
|
17
|
+
// zu ISO-8601 gemacht werden musste. Heute übernimmt der customType die
|
|
18
|
+
// Konversion DB↔Instant — diese Funktion ist nur noch defensive Glue für
|
|
19
|
+
// Legacy-Code-Pfade die noch Strings durchreichen (z.B. raw SQL).
|
|
20
|
+
function toInstant(value: unknown): Temporal.Instant | undefined {
|
|
21
|
+
if (value instanceof Temporal.Instant) return value;
|
|
22
|
+
if (typeof value !== "string") return undefined;
|
|
23
|
+
const iso = value.includes("T") ? value : value.replace(" ", "T").replace(/\+00$/, "Z");
|
|
24
|
+
return Temporal.Instant.from(iso);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* API → DB: locatedTimestamp-Felder zu zwei flachen Spalten flatten.
|
|
29
|
+
*
|
|
30
|
+
* - `{ at, tz }` (UI-Form) → `{ <name>Utc, <name>Tz }` (utc via Temporal berechnet)
|
|
31
|
+
* - `{ utc, tz }` (Server-Form) → utc gewinnt direkt
|
|
32
|
+
* - `{ at, tz, utc }` → utc gewinnt; at wird ignoriert (Konsistenz-Check ist
|
|
33
|
+
* Caller-Verantwortung)
|
|
34
|
+
*
|
|
35
|
+
* Pure — mutiert nicht.
|
|
36
|
+
*/
|
|
37
|
+
export function flattenLocatedTimestamp(
|
|
38
|
+
payload: Record<string, unknown>,
|
|
39
|
+
entity: EntityDefinition,
|
|
40
|
+
): Record<string, unknown> {
|
|
41
|
+
const T = Temporal;
|
|
42
|
+
const result: Record<string, unknown> = { ...payload };
|
|
43
|
+
|
|
44
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
45
|
+
if (field.type !== "locatedTimestamp") continue;
|
|
46
|
+
|
|
47
|
+
const raw = result[name];
|
|
48
|
+
if (raw === undefined || raw === null) continue;
|
|
49
|
+
if (typeof raw !== "object") {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`flattenLocatedTimestamp: field "${name}" expects { at, tz } or { utc, tz } object, got ${typeof raw}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const pair = raw as { at?: string; tz?: string; utc?: string };
|
|
55
|
+
|
|
56
|
+
delete result[name];
|
|
57
|
+
|
|
58
|
+
if (pair.tz === undefined) continue;
|
|
59
|
+
const tz = pair.tz;
|
|
60
|
+
// Sprint F: <name>Utc-Spalte ist instant() — Drizzle erwartet
|
|
61
|
+
// Temporal.Instant. Konvertierung pair.utc-string → Instant geht via
|
|
62
|
+
// toInstant() (kennt String + Instant); pair.at + tz → Instant via
|
|
63
|
+
// Temporal-Math.
|
|
64
|
+
const instant: Temporal.Instant | undefined =
|
|
65
|
+
pair.utc !== undefined
|
|
66
|
+
? toInstant(pair.utc)
|
|
67
|
+
: pair.at !== undefined
|
|
68
|
+
? T.PlainDateTime.from(pair.at).toZonedDateTime(tz).toInstant()
|
|
69
|
+
: undefined;
|
|
70
|
+
if (instant === undefined) continue;
|
|
71
|
+
|
|
72
|
+
result[`${name}Utc`] = instant;
|
|
73
|
+
result[`${name}Tz`] = tz;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* DB → API: zwei flache Spalten zu combined { at, tz, utc } rehydraten.
|
|
81
|
+
*
|
|
82
|
+
* `at` ist immer Wall-Clock in der gespeicherten `tz` (Pickup-Ort-lokal).
|
|
83
|
+
* Wer User-Sicht braucht, leitet aus `utc` selbst ab — der Server kennt
|
|
84
|
+
* keine User-TZ.
|
|
85
|
+
*
|
|
86
|
+
* Pure — mutiert nicht.
|
|
87
|
+
*/
|
|
88
|
+
export function rehydrateLocatedTimestamp(
|
|
89
|
+
row: Record<string, unknown>,
|
|
90
|
+
entity: EntityDefinition,
|
|
91
|
+
): Record<string, unknown> {
|
|
92
|
+
const result: Record<string, unknown> = { ...row };
|
|
93
|
+
|
|
94
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
95
|
+
if (field.type !== "locatedTimestamp") continue;
|
|
96
|
+
|
|
97
|
+
const utcRaw = result[`${name}Utc`];
|
|
98
|
+
const tzRaw = result[`${name}Tz`];
|
|
99
|
+
|
|
100
|
+
delete result[`${name}Utc`];
|
|
101
|
+
delete result[`${name}Tz`];
|
|
102
|
+
|
|
103
|
+
if (typeof tzRaw !== "string") continue;
|
|
104
|
+
const utcInstant = toInstant(utcRaw);
|
|
105
|
+
if (utcInstant === undefined) continue;
|
|
106
|
+
|
|
107
|
+
const localZdt = utcInstant.toZonedDateTimeISO(tzRaw);
|
|
108
|
+
const at = localZdt.toPlainDateTime().toString();
|
|
109
|
+
|
|
110
|
+
result[name] = { at, tz: tzRaw, utc: utcInstant.toString() };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
package/src/db/money.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Auto-Convert für money-Felder im DB-Layer.
|
|
2
|
+
//
|
|
3
|
+
// Vertrag (siehe auch db/located-timestamp.ts — gleicher Compound-Type-Pattern):
|
|
4
|
+
// API-Form: { amount, currency } | number (permissiv für Legacy)
|
|
5
|
+
// DB-Form: <name> BIGINT + <name>Currency TEXT
|
|
6
|
+
// Read-Form: { amount, currency }
|
|
7
|
+
//
|
|
8
|
+
// Permissiv-Insert: primitive number wird als amount akzeptiert (Legacy aus
|
|
9
|
+
// pre-Stufe-3-Samples). Currency fällt dann auf entity.defaultCurrency
|
|
10
|
+
// zurück (oder DEFAULT_CURRENCIES[0] = "EUR" als Framework-Fallback).
|
|
11
|
+
//
|
|
12
|
+
// Anders als locatedTimestamp behalten wir den Field-Namen `<name>` als
|
|
13
|
+
// amount-Spalte (Legacy DB-Convention für Money — `SUM(buying_price)` bleibt
|
|
14
|
+
// idiomatisch). `<name>Currency` ist die zusätzliche Spalte.
|
|
15
|
+
|
|
16
|
+
import type { EntityDefinition } from "../engine/types";
|
|
17
|
+
import { DEFAULT_CURRENCIES } from "../engine/types";
|
|
18
|
+
|
|
19
|
+
const FRAMEWORK_DEFAULT_CURRENCY = DEFAULT_CURRENCIES[0]; // "EUR"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* API → DB: money-Felder zu zwei flachen Spalten flatten.
|
|
23
|
+
*
|
|
24
|
+
* - `{ amount, currency }` → `{ <name>: amount, <name>Currency: currency }`
|
|
25
|
+
* - `number` (legacy) → `{ <name>: number, <name>Currency: defaultCurrency }`
|
|
26
|
+
*
|
|
27
|
+
* Pure — mutiert nicht.
|
|
28
|
+
*/
|
|
29
|
+
export function flattenMoney(
|
|
30
|
+
payload: Record<string, unknown>,
|
|
31
|
+
entity: EntityDefinition,
|
|
32
|
+
): Record<string, unknown> {
|
|
33
|
+
const result: Record<string, unknown> = { ...payload };
|
|
34
|
+
const fallbackCurrency = entity.defaultCurrency ?? FRAMEWORK_DEFAULT_CURRENCY;
|
|
35
|
+
|
|
36
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
37
|
+
if (field.type !== "money") continue;
|
|
38
|
+
|
|
39
|
+
const raw = result[name];
|
|
40
|
+
if (raw === undefined || raw === null) continue;
|
|
41
|
+
|
|
42
|
+
let amount: number;
|
|
43
|
+
let currency: string;
|
|
44
|
+
|
|
45
|
+
if (typeof raw === "object" && "amount" in raw) {
|
|
46
|
+
const pair = raw as { amount: number; currency?: string };
|
|
47
|
+
amount = pair.amount;
|
|
48
|
+
currency = pair.currency ?? fallbackCurrency;
|
|
49
|
+
} else if (typeof raw === "number") {
|
|
50
|
+
amount = raw;
|
|
51
|
+
// Expliziter currency-key im Payload überschreibt den Default-Fallback.
|
|
52
|
+
const explicitCurrency = result[`${name}Currency`];
|
|
53
|
+
currency = typeof explicitCurrency === "string" ? explicitCurrency : fallbackCurrency;
|
|
54
|
+
} else {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`flattenMoney: field "${name}" expects { amount, currency } object or number, got ${typeof raw}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
delete result[name];
|
|
61
|
+
result[name] = amount;
|
|
62
|
+
result[`${name}Currency`] = currency;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* DB → API: zwei flache Spalten zu combined { amount, currency } rehydraten.
|
|
70
|
+
*
|
|
71
|
+
* Wirft loud bei korrupter DB-Form (string das nicht zur Zahl wird) — silent
|
|
72
|
+
* data-loss wäre Bug-Vektor. NULL/undefined amount → field aus Output entfernt.
|
|
73
|
+
*
|
|
74
|
+
* Pure — mutiert nicht.
|
|
75
|
+
*/
|
|
76
|
+
export function rehydrateMoney(
|
|
77
|
+
row: Record<string, unknown>,
|
|
78
|
+
entity: EntityDefinition,
|
|
79
|
+
): Record<string, unknown> {
|
|
80
|
+
const result: Record<string, unknown> = { ...row };
|
|
81
|
+
const fallbackCurrency = entity.defaultCurrency ?? FRAMEWORK_DEFAULT_CURRENCY;
|
|
82
|
+
|
|
83
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
84
|
+
if (field.type !== "money") continue;
|
|
85
|
+
|
|
86
|
+
const amountRaw = result[name];
|
|
87
|
+
const currencyRaw = result[`${name}Currency`];
|
|
88
|
+
|
|
89
|
+
delete result[`${name}Currency`];
|
|
90
|
+
|
|
91
|
+
if (amountRaw === null || amountRaw === undefined) {
|
|
92
|
+
delete result[name];
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let amount: number;
|
|
97
|
+
if (typeof amountRaw === "number") {
|
|
98
|
+
amount = amountRaw;
|
|
99
|
+
} else if (typeof amountRaw === "string" && amountRaw !== "") {
|
|
100
|
+
// PG-driver liefert BIGINT manchmal als String (>2^53 sicher).
|
|
101
|
+
amount = Number(amountRaw);
|
|
102
|
+
if (Number.isNaN(amount)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`rehydrateMoney: field "${name}" amount string "${amountRaw}" is not a number — DB corruption?`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`rehydrateMoney: field "${name}" amount has unexpected type ${typeof amountRaw}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const currency =
|
|
114
|
+
typeof currencyRaw === "string" && currencyRaw !== "" ? currencyRaw : fallbackCurrency;
|
|
115
|
+
|
|
116
|
+
result[name] = { amount, currency };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Drizzle wraps postgres-js errors in `DrizzleQueryError`; the original PG
|
|
2
|
+
// error (with SQLSTATE `code` and `constraint_name`) lives in `.cause`. We
|
|
3
|
+
// unwrap both layers so callers don't have to know which layer produced the
|
|
4
|
+
// error. Used by the event-store to distinguish a unique-violation on the
|
|
5
|
+
// aggregate-version index (optimistic-concurrency conflict) from the one on
|
|
6
|
+
// the request-id idempotency index (replay signal).
|
|
7
|
+
|
|
8
|
+
export type PgErrorInfo = {
|
|
9
|
+
readonly code: string | undefined;
|
|
10
|
+
readonly constraint_name: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function extractPgError(e: unknown): PgErrorInfo | null {
|
|
14
|
+
if (typeof e !== "object" || e === null) return null;
|
|
15
|
+
const layers: unknown[] = [e];
|
|
16
|
+
// @cast-boundary error-details — DrizzleQueryError wraps PG-error in .cause
|
|
17
|
+
const cause = (e as { cause?: unknown }).cause;
|
|
18
|
+
if (typeof cause === "object" && cause !== null) layers.push(cause);
|
|
19
|
+
|
|
20
|
+
for (const layer of layers) {
|
|
21
|
+
// @cast-boundary error-details — postgres-js error shape (code, constraint_name)
|
|
22
|
+
const code = (layer as { code?: string }).code;
|
|
23
|
+
const constraintName = (layer as { constraint_name?: string }).constraint_name;
|
|
24
|
+
if (code !== undefined || constraintName !== undefined) {
|
|
25
|
+
return { code, constraint_name: constraintName };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isUniqueViolation(e: unknown): boolean {
|
|
32
|
+
return extractPgError(e)?.code === "23505";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// PG SQLSTATE 42P07 — "relation already exists". Raised when CREATE
|
|
36
|
+
// TABLE (or drizzle-kit's generated equivalent) runs against a table
|
|
37
|
+
// that's already been created. Useful for idempotent boot-paths like
|
|
38
|
+
// the dev-server, where a persistent DB carries the table over from
|
|
39
|
+
// the previous restart.
|
|
40
|
+
export function isTableAlreadyExists(e: unknown): boolean {
|
|
41
|
+
return extractPgError(e)?.code === "42P07";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function constraintOf(e: unknown): string | undefined {
|
|
45
|
+
return extractPgError(e)?.constraint_name;
|
|
46
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import type { ReferenceDataDef } from "../engine/types";
|
|
3
|
+
import { SYSTEM_TENANT_ID } from "../engine/types";
|
|
4
|
+
import type { DbConnection, DbRow } from "./connection";
|
|
5
|
+
import type { TableColumns } from "./dialect";
|
|
6
|
+
import { toSnakeCase } from "./table-builder";
|
|
7
|
+
|
|
8
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle dynamic tables
|
|
9
|
+
type Table = TableColumns<any>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Seed reference data at boot time.
|
|
13
|
+
* For each ReferenceDataDef: upsert rows (insert missing, update changed, never delete).
|
|
14
|
+
* Upsert key defaults to the first field in the data object.
|
|
15
|
+
*/
|
|
16
|
+
export async function seedReferenceData(
|
|
17
|
+
defs: readonly ReferenceDataDef[],
|
|
18
|
+
tables: ReadonlyMap<string, Table>,
|
|
19
|
+
db: DbConnection,
|
|
20
|
+
): Promise<{ inserted: number; updated: number }> {
|
|
21
|
+
let inserted = 0;
|
|
22
|
+
let updated = 0;
|
|
23
|
+
|
|
24
|
+
for (const def of defs) {
|
|
25
|
+
const table = tables.get(def.entityName);
|
|
26
|
+
if (!table) continue;
|
|
27
|
+
if (def.data.length === 0) continue;
|
|
28
|
+
|
|
29
|
+
const firstRow = def.data[0];
|
|
30
|
+
if (!firstRow) continue;
|
|
31
|
+
const firstKey = Object.keys(firstRow)[0];
|
|
32
|
+
if (!firstKey) continue;
|
|
33
|
+
const upsertKey = def.upsertKey ?? firstKey;
|
|
34
|
+
const snakeKey = toSnakeCase(upsertKey);
|
|
35
|
+
|
|
36
|
+
for (const row of def.data) {
|
|
37
|
+
const keyValue = row[upsertKey];
|
|
38
|
+
if (keyValue === undefined) continue;
|
|
39
|
+
|
|
40
|
+
// Check if row exists
|
|
41
|
+
const [existing] = await db
|
|
42
|
+
.select()
|
|
43
|
+
.from(table)
|
|
44
|
+
.where(eq(table[upsertKey] ?? table[snakeKey], keyValue))
|
|
45
|
+
.limit(1);
|
|
46
|
+
|
|
47
|
+
if (existing) {
|
|
48
|
+
// Update if any field changed
|
|
49
|
+
const existingData = existing as DbRow;
|
|
50
|
+
const changes: Record<string, unknown> = {};
|
|
51
|
+
for (const [field, value] of Object.entries(row)) {
|
|
52
|
+
if (field === upsertKey) continue;
|
|
53
|
+
if (existingData[field] !== value) {
|
|
54
|
+
changes[field] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (Object.keys(changes).length > 0) {
|
|
58
|
+
await db
|
|
59
|
+
.update(table)
|
|
60
|
+
.set(changes)
|
|
61
|
+
.where(eq(table[upsertKey] ?? table[snakeKey], keyValue));
|
|
62
|
+
updated++;
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
await db.insert(table).values({
|
|
66
|
+
...row,
|
|
67
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
68
|
+
version: 1,
|
|
69
|
+
insertedAt: Temporal.Now.instant(),
|
|
70
|
+
});
|
|
71
|
+
inserted++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { inserted, updated };
|
|
77
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { and, type SQL } from "drizzle-orm";
|
|
2
|
+
import type { DbRow } from "./connection";
|
|
3
|
+
import type { TableColumns } from "./dialect";
|
|
4
|
+
|
|
5
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mirrors the erased ProjectionTable / event-store-executor pattern — the framework doesn't know user column shapes.
|
|
6
|
+
type AnyTable = TableColumns<any>;
|
|
7
|
+
|
|
8
|
+
// Minimal DB surface fetchOne uses — structurally satisfied by raw DbRunner
|
|
9
|
+
// (connection / tx) AND TenantDb (tenant-scoped wrapper). Both expose the
|
|
10
|
+
// same `select().from().where().limit()` chain with compatible rows, so the
|
|
11
|
+
// helper types against the shared shape instead of a union that TS can't
|
|
12
|
+
// narrow cleanly.
|
|
13
|
+
type SelectChainDb = {
|
|
14
|
+
select: () => {
|
|
15
|
+
from: (table: AnyTable) => {
|
|
16
|
+
where: (cond: SQL | undefined) => {
|
|
17
|
+
limit: (n: number) => PromiseLike<readonly Record<string, unknown>[]>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// SELECT * FROM <table> WHERE <...conditions> LIMIT 1 → first row or undefined.
|
|
24
|
+
// Collapses the "const [row] = await db.select()...limit(1)" destructure
|
|
25
|
+
// that repeats in every detail-query-style handler and existence-check.
|
|
26
|
+
//
|
|
27
|
+
// Conditions are variadic and non-empty — the tuple `[SQL, ...SQL[]]` rejects
|
|
28
|
+
// `fetchOne(db, table)` (would silently pick any row) and `fetchOne(db, table,
|
|
29
|
+
// undefined)` (would do the same) at compile time. Multiple conditions are
|
|
30
|
+
// combined with AND.
|
|
31
|
+
//
|
|
32
|
+
// const existing = await fetchOne<{ id: number }>(db, userTable,
|
|
33
|
+
// eq(userTable.email, payload.email));
|
|
34
|
+
// if (existing) return writeFailure(new ConflictError({ ... }));
|
|
35
|
+
//
|
|
36
|
+
// const row = await fetchOne(db, membershipTable,
|
|
37
|
+
// eq(membershipTable.userId, userId),
|
|
38
|
+
// eq(membershipTable.tenantId, tenantId),
|
|
39
|
+
// );
|
|
40
|
+
//
|
|
41
|
+
// For dynamic condition arrays (length known only at runtime), spread
|
|
42
|
+
// explicitly: `fetchOne(db, table, first, ...rest)`. Raw `...arr` with
|
|
43
|
+
// `arr: SQL[]` won't type-check because TS can't prove the array is non-
|
|
44
|
+
// empty — a feature, not a bug.
|
|
45
|
+
export async function fetchOne<TRow = DbRow>(
|
|
46
|
+
db: SelectChainDb,
|
|
47
|
+
table: AnyTable,
|
|
48
|
+
...conditions: readonly [SQL, ...SQL[]]
|
|
49
|
+
): Promise<TRow | undefined> {
|
|
50
|
+
const where = conditions.length === 1 ? conditions[0] : and(...conditions);
|
|
51
|
+
const rows = await db.select().from(table).where(where).limit(1);
|
|
52
|
+
return rows[0] as TRow | undefined;
|
|
53
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import type { DbConnection, DbTx } from "./connection";
|
|
3
|
+
|
|
4
|
+
// True when `<fullyQualifiedName>` refers to an existing relation in the
|
|
5
|
+
// current database. Thin wrapper over `to_regclass`, which returns NULL
|
|
6
|
+
// when the name doesn't resolve — the only postgres query that cheaply
|
|
7
|
+
// reports existence without raising an error on a missing relation.
|
|
8
|
+
//
|
|
9
|
+
// Used by framework-managed tables (events, archived_streams, snapshots,
|
|
10
|
+
// projections, event-consumers) whose createX() is called from multiple
|
|
11
|
+
// boot paths (setupTestStack, production boot, manual test setups). The
|
|
12
|
+
// guard keeps those calls idempotent without having to interpret the
|
|
13
|
+
// "already exists" error code.
|
|
14
|
+
//
|
|
15
|
+
// if (await tableExists(db, "public.events")) return;
|
|
16
|
+
// await pushTables(db, { events: eventsTable });
|
|
17
|
+
export async function tableExists(
|
|
18
|
+
db: DbConnection | DbTx,
|
|
19
|
+
fullyQualifiedName: string,
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
const rows = await db.execute<{ exists: boolean }>(
|
|
22
|
+
sql`SELECT to_regclass(${fullyQualifiedName}) IS NOT NULL AS exists`,
|
|
23
|
+
);
|
|
24
|
+
return rows[0]?.exists ?? false;
|
|
25
|
+
}
|