@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
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
|
3
|
+
import type {
|
|
4
|
+
EntityDefinition,
|
|
5
|
+
EntityRelations,
|
|
6
|
+
FieldDefinition,
|
|
7
|
+
FieldsMap,
|
|
8
|
+
} from "../engine/types";
|
|
9
|
+
import { assertUnreachable } from "../utils";
|
|
10
|
+
import {
|
|
11
|
+
boolean,
|
|
12
|
+
index,
|
|
13
|
+
instant,
|
|
14
|
+
integer,
|
|
15
|
+
jsonb,
|
|
16
|
+
moneyAmount,
|
|
17
|
+
table as pgTable,
|
|
18
|
+
serial,
|
|
19
|
+
type TableColumns,
|
|
20
|
+
text,
|
|
21
|
+
uniqueIndex,
|
|
22
|
+
uuid,
|
|
23
|
+
} from "./dialect";
|
|
24
|
+
|
|
25
|
+
type ColumnBuilder =
|
|
26
|
+
| ReturnType<typeof text>
|
|
27
|
+
| ReturnType<typeof integer>
|
|
28
|
+
| ReturnType<typeof boolean>
|
|
29
|
+
| ReturnType<typeof moneyAmount>
|
|
30
|
+
| ReturnType<typeof jsonb>
|
|
31
|
+
| ReturnType<typeof instant>
|
|
32
|
+
| ReturnType<typeof serial>
|
|
33
|
+
| ReturnType<typeof uuid>;
|
|
34
|
+
|
|
35
|
+
// Returns column(s) for a field. Most fields return a single entry,
|
|
36
|
+
// money returns two (amount + currency), files/images return none.
|
|
37
|
+
//
|
|
38
|
+
// `required: true` auf einem FieldDefinition mappt **immer** auf .notNull()
|
|
39
|
+
// in der DB-Spalte. Die alte Implementation hat das nur für reference-
|
|
40
|
+
// fields gemacht — text/select/number/etc. waren stillschweigend nullable
|
|
41
|
+
// in der DB, auch wenn der API-Validator required erzwungen hat. Folge:
|
|
42
|
+
// hand-written PgTable-Definitionen mussten daneben mit .notNull() pflegen,
|
|
43
|
+
// was zu Doppel-Definitionen + Schema-Drift führte. Jetzt ist r.entity
|
|
44
|
+
// die einzige Wahrheit.
|
|
45
|
+
function fieldToColumns(
|
|
46
|
+
name: string,
|
|
47
|
+
field: FieldDefinition,
|
|
48
|
+
entity: EntityDefinition,
|
|
49
|
+
): Record<string, ColumnBuilder> {
|
|
50
|
+
const snakeName = toSnakeCase(name);
|
|
51
|
+
|
|
52
|
+
switch (field.type) {
|
|
53
|
+
case "text":
|
|
54
|
+
case "longText": {
|
|
55
|
+
// Beide mappen auf PG `text` (unbounded). Unterschied lebt nur
|
|
56
|
+
// im Type-Layer: longText hat kein sortable/searchable/filterable
|
|
57
|
+
// (Sprint 5b vorab). Reihenfolge default() VOR notNull(): drizzle's
|
|
58
|
+
// column-builder chained beides; ohne default() hat die generierte
|
|
59
|
+
// SQL keinen DEFAULT-clause (bricht ALTER TABLE ADD COLUMN auf
|
|
60
|
+
// existing rows). longText hat heute kein default-feld im type,
|
|
61
|
+
// aber der check `field.default !== undefined` ist defensive.
|
|
62
|
+
const base = text(snakeName);
|
|
63
|
+
const withDefault =
|
|
64
|
+
"default" in field && field.default !== undefined ? base.default(field.default) : base;
|
|
65
|
+
return { [name]: field.required ? withDefault.notNull() : withDefault };
|
|
66
|
+
}
|
|
67
|
+
case "boolean":
|
|
68
|
+
return {
|
|
69
|
+
[name]:
|
|
70
|
+
field.default !== undefined
|
|
71
|
+
? boolean(snakeName).default(field.default).notNull()
|
|
72
|
+
: field.required
|
|
73
|
+
? boolean(snakeName).notNull()
|
|
74
|
+
: boolean(snakeName),
|
|
75
|
+
};
|
|
76
|
+
case "select": {
|
|
77
|
+
const col = text(snakeName);
|
|
78
|
+
return { [name]: field.required ? col.notNull() : col };
|
|
79
|
+
}
|
|
80
|
+
case "multiSelect":
|
|
81
|
+
// jsonb-Array<string> mit Default `[]` und immer NOT NULL.
|
|
82
|
+
//
|
|
83
|
+
// Der `required`-Flag auf MultiSelectFieldDef wird hier bewusst
|
|
84
|
+
// ignoriert: Mit Default `[]` ist das Feld strukturell never-null
|
|
85
|
+
// (Insert ohne Wert → leeres Array, nicht NULL). Read-Side-Code
|
|
86
|
+
// braucht keinen null-check, das ist API-Garantie. Wer "wirklich
|
|
87
|
+
// null" will (= "Feld noch nie gesetzt") nutzt einen separaten
|
|
88
|
+
// Status-Field oder ein optional-typed reference statt eines
|
|
89
|
+
// multi-select.
|
|
90
|
+
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
91
|
+
case "number": {
|
|
92
|
+
const col = integer(snakeName);
|
|
93
|
+
return { [name]: field.required ? col.notNull() : col };
|
|
94
|
+
}
|
|
95
|
+
case "reference":
|
|
96
|
+
// Tier 2.7e-3: FK-Style UUID-Spalte. Multi-Mode (Tier 2.7e-Multi)
|
|
97
|
+
// speichert UUIDs als jsonb-Array<string>. Single-Mode bleibt
|
|
98
|
+
// klassische UUID-Spalte (NOT NULL nur bei required).
|
|
99
|
+
if (field.multiple === true) {
|
|
100
|
+
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
[name]: field.required ? uuid(snakeName).notNull() : uuid(snakeName),
|
|
104
|
+
};
|
|
105
|
+
case "money":
|
|
106
|
+
// BIGINT storing the integer minor unit (cents for EUR, yen for JPY —
|
|
107
|
+
// the currency column tells you which). INTEGER would cap at ~21 M EUR
|
|
108
|
+
// which is too tight for B2B invoices, property values or balance
|
|
109
|
+
// aggregates. BIGINT handles up to ~90 trillion EUR safely in JS.
|
|
110
|
+
// Currency hat immer einen Default, ist also strukturell .notNull().
|
|
111
|
+
return {
|
|
112
|
+
[name]: field.required ? moneyAmount(snakeName).notNull() : moneyAmount(snakeName),
|
|
113
|
+
[`${name}Currency`]: text(`${snakeName}_currency`)
|
|
114
|
+
.default(entity.defaultCurrency ?? "EUR")
|
|
115
|
+
.notNull(),
|
|
116
|
+
};
|
|
117
|
+
case "embedded":
|
|
118
|
+
// jsonb mit default `{}` und immer NOT NULL — analog zu multiSelect.
|
|
119
|
+
// `required` wird bewusst ignoriert weil der Default das Feld
|
|
120
|
+
// strukturell never-null macht. Wer optional-embedded möchte (=
|
|
121
|
+
// "Feld komplett weglassen können") modelliert das über ein
|
|
122
|
+
// wrapper-feld mit boolean-flag oder discriminierte-union.
|
|
123
|
+
return { [name]: jsonb(snakeName).default({}).notNull() };
|
|
124
|
+
case "date": {
|
|
125
|
+
// TODO(Sprint G): semantisch falsch — `type:"date"` sollte
|
|
126
|
+
// Temporal.PlainDate sein (PG `date` Spalte, kein TZ). Heute aliased auf
|
|
127
|
+
// instant() = TIMESTAMPTZ damit Caller die gleiche API nutzen wie für
|
|
128
|
+
// type:"timestamp". Echte PlainDate-Migration kommt nach Sprint F.
|
|
129
|
+
const col = instant(snakeName);
|
|
130
|
+
return { [name]: field.required ? col.notNull() : col };
|
|
131
|
+
}
|
|
132
|
+
case "timestamp": {
|
|
133
|
+
// UTC-Instant — gespeichert als TIMESTAMPTZ in PG, gelesen/geschrieben
|
|
134
|
+
// als Temporal.Instant via instant() customType (siehe dialect.ts).
|
|
135
|
+
// Sprint F: Single-Mode-Welt — Caller-Code kennt nur Temporal.Instant,
|
|
136
|
+
// nie JS-Date. Auch Vergleiche (lte/gt/orderBy) akzeptieren Instants
|
|
137
|
+
// direkt, kein .toString()-Cast nötig.
|
|
138
|
+
const col = instant(snakeName);
|
|
139
|
+
return { [name]: field.required ? col.notNull() : col };
|
|
140
|
+
}
|
|
141
|
+
case "tz": {
|
|
142
|
+
// IANA-Zonenname als TEXT — Validierung über Zod-Schema (kommt im
|
|
143
|
+
// Validator-Schritt). Snake-Convention: `pickup_tz`.
|
|
144
|
+
const col = text(snakeName);
|
|
145
|
+
return { [name]: field.required ? col.notNull() : col };
|
|
146
|
+
}
|
|
147
|
+
case "locatedTimestamp": {
|
|
148
|
+
// ZWEI Spalten als atomares Pair: <name>_utc TIMESTAMPTZ + <name>_tz TEXT.
|
|
149
|
+
// _utc ist instant() (Temporal.Instant), _tz ist text (IANA-Name).
|
|
150
|
+
// Auto-Convert (at+tz → utc beim Insert; utc+tz → at beim Read) wird
|
|
151
|
+
// im Executor verdrahtet (Phase C). required propagiert auf beide.
|
|
152
|
+
const utc = instant(`${snakeName}_utc`);
|
|
153
|
+
const tz = text(`${snakeName}_tz`);
|
|
154
|
+
return {
|
|
155
|
+
[`${name}Utc`]: field.required ? utc.notNull() : utc,
|
|
156
|
+
[`${name}Tz`]: field.required ? tz.notNull() : tz,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
case "file":
|
|
160
|
+
case "image": {
|
|
161
|
+
// Single file: stores fileRefId as UUID — must match fileRefsTable.id
|
|
162
|
+
// (uuid column). Anything narrower (integer, text length-limited) would
|
|
163
|
+
// silently truncate or type-coerce at INSERT time and the FK reference
|
|
164
|
+
// would be unusable.
|
|
165
|
+
const col = uuid(snakeName);
|
|
166
|
+
return { [name]: field.required ? col.notNull() : col };
|
|
167
|
+
}
|
|
168
|
+
case "files":
|
|
169
|
+
case "images":
|
|
170
|
+
// Multi file: no column in entity table, resolved via FileRef table
|
|
171
|
+
// over (entityType, entityId, fieldName). A bridge-table with
|
|
172
|
+
// CASCADE + sort-order is a later improvement; today plural files
|
|
173
|
+
// live entirely in fileRefsTable.
|
|
174
|
+
return {};
|
|
175
|
+
default:
|
|
176
|
+
assertUnreachable(field, "field type");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Accepts both camelCase (`tenantMembership`) and kebab-case (`tenant-membership`)
|
|
181
|
+
// entity / field names. Kebab is the canonical form for new multi-word entity
|
|
182
|
+
// types (consistent across r.entity, event-types, table names) — camelCase is
|
|
183
|
+
// kept working for already-shipped code.
|
|
184
|
+
export function toSnakeCase(str: string): string {
|
|
185
|
+
return str.replace(/-/g, "_").replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Derives a table name from an entity name:
|
|
190
|
+
* 1. camelCase → snake_case (e.g. "memberTask" → "member_task")
|
|
191
|
+
* 2. Simple English pluralization (category→categories, status→statuses, task→tasks)
|
|
192
|
+
* 3. `read_` prefix — markiert die Tabelle als Event-Sourced-Read-Model,
|
|
193
|
+
* damit im DB-Tool sofort erkennbar ist dass App-Code nicht direkt
|
|
194
|
+
* reinschreibt. Event-Store (kumiko_events) + Framework-State
|
|
195
|
+
* (kumiko_*) haben ihren eigenen Prefix, normale App-Side-Tables
|
|
196
|
+
* (ohne ES-Anbindung) haben keinen — die drei Kategorien sind damit
|
|
197
|
+
* im Tabellenbrowser unterscheidbar.
|
|
198
|
+
*/
|
|
199
|
+
const ES_PLURAL_SUFFIXES = ["s", "sh", "ch", "x"] as const;
|
|
200
|
+
|
|
201
|
+
export const READ_MODEL_PREFIX = "read_";
|
|
202
|
+
|
|
203
|
+
export function toTableName(entityName: string): string {
|
|
204
|
+
const snake = toSnakeCase(entityName);
|
|
205
|
+
let plural: string;
|
|
206
|
+
if (snake.endsWith("y") && !/[aeiou]y$/.test(snake)) {
|
|
207
|
+
plural = `${snake.slice(0, -1)}ies`;
|
|
208
|
+
} else if (ES_PLURAL_SUFFIXES.some((suffix) => snake.endsWith(suffix))) {
|
|
209
|
+
plural = `${snake}es`;
|
|
210
|
+
} else {
|
|
211
|
+
plural = `${snake}s`;
|
|
212
|
+
}
|
|
213
|
+
return `${READ_MODEL_PREFIX}${plural}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Drizzle's PgTableWithColumns<any> erbt eine `[k: string]: any` Index-
|
|
217
|
+
// Signature die in strict-mode (noUncheckedIndexedAccess + TS4111) jeden
|
|
218
|
+
// konsumierenden Code zwingt auf Bracket-Notation auch für bekannte
|
|
219
|
+
// Spalten. Da wir die Tabelle dynamisch aus EntityDefinition bauen,
|
|
220
|
+
// kann TS Drizzle's volle Spalten-Inferenz nicht liefern — wir spiegeln
|
|
221
|
+
// stattdessen die `fieldToColumns`-Logik im Type-System: jeder Field-Type
|
|
222
|
+
// mappt auf einen konkreten data-Type, ColumnsForEntity baut daraus den
|
|
223
|
+
// vollständigen Property-Bag.
|
|
224
|
+
//
|
|
225
|
+
// Mit konkret-inferiertem `F` (via createEntity({ fields: { ... } }))
|
|
226
|
+
// kommt das volle data-typing durch Drizzle's `eq()`/`select()`/`row.x`
|
|
227
|
+
// am Call-Site an. Der alte `EntityDefinition` ohne Generic-Param fällt
|
|
228
|
+
// auf `FieldsMap` zurück (= breaking-change-frei) — dort kennt TS die
|
|
229
|
+
// Field-Types nicht, das Mapping kollabiert auf `AnyPgColumn`.
|
|
230
|
+
//
|
|
231
|
+
// Lock-step-Vertrag: jeder Branch hier muss zur Runtime-Entscheidung in
|
|
232
|
+
// fieldToColumns passen. Type-Tests gegen repräsentative Entities (siehe
|
|
233
|
+
// db/__tests__/drizzle-table-types.test.ts) catchen Drift.
|
|
234
|
+
|
|
235
|
+
// Single drizzle column with concrete data + nullability — preserves
|
|
236
|
+
// Drizzle's `.select`/`eq`/`lt`-Inferenz für T.
|
|
237
|
+
type Col<T> = AnyPgColumn<{ data: T; notNull: true }>;
|
|
238
|
+
type NullCol<T> = AnyPgColumn<{ data: T; notNull: false }>;
|
|
239
|
+
|
|
240
|
+
// Per-field column shape — matches `fieldToColumns`. Money +
|
|
241
|
+
// locatedTimestamp produce two-column pairs; files/images contribute no
|
|
242
|
+
// columns (resolved via FileRef table). `notNull` propagiert von
|
|
243
|
+
// `field.required` (literal preserved by createXField generics).
|
|
244
|
+
type ColumnsForField<K extends string, F extends FieldDefinition> = F extends {
|
|
245
|
+
type: "text" | "select" | "tz";
|
|
246
|
+
}
|
|
247
|
+
? F extends { required: true }
|
|
248
|
+
? { readonly [P in K]: Col<string> }
|
|
249
|
+
: { readonly [P in K]: NullCol<string> }
|
|
250
|
+
: F extends { type: "boolean" }
|
|
251
|
+
? // boolean default OR required → notNull (DB has DEFAULT, structurally never-null)
|
|
252
|
+
F extends { default: boolean } | { required: true }
|
|
253
|
+
? { readonly [P in K]: Col<boolean> }
|
|
254
|
+
: { readonly [P in K]: NullCol<boolean> }
|
|
255
|
+
: F extends { type: "multiSelect" }
|
|
256
|
+
? // jsonb default `[]`, immer notNull
|
|
257
|
+
{ readonly [P in K]: Col<readonly string[]> }
|
|
258
|
+
: F extends { type: "number" }
|
|
259
|
+
? F extends { required: true }
|
|
260
|
+
? { readonly [P in K]: Col<number> }
|
|
261
|
+
: { readonly [P in K]: NullCol<number> }
|
|
262
|
+
: F extends { type: "money" }
|
|
263
|
+
? F extends { required: true }
|
|
264
|
+
? { readonly [P in K]: Col<number> } & {
|
|
265
|
+
readonly [P in `${K}Currency`]: Col<string>;
|
|
266
|
+
}
|
|
267
|
+
: { readonly [P in K]: NullCol<number> } & {
|
|
268
|
+
readonly [P in `${K}Currency`]: Col<string>;
|
|
269
|
+
}
|
|
270
|
+
: F extends { type: "reference"; multiple: true }
|
|
271
|
+
? { readonly [P in K]: Col<readonly string[]> }
|
|
272
|
+
: F extends { type: "reference" }
|
|
273
|
+
? F extends { required: true }
|
|
274
|
+
? { readonly [P in K]: Col<string> }
|
|
275
|
+
: { readonly [P in K]: NullCol<string> }
|
|
276
|
+
: F extends { type: "embedded" }
|
|
277
|
+
? // jsonb default `{}`, immer notNull
|
|
278
|
+
{ readonly [P in K]: Col<Readonly<Record<string, unknown>>> }
|
|
279
|
+
: F extends { type: "date" | "timestamp" }
|
|
280
|
+
? F extends { required: true }
|
|
281
|
+
? { readonly [P in K]: Col<Temporal.Instant> }
|
|
282
|
+
: { readonly [P in K]: NullCol<Temporal.Instant> }
|
|
283
|
+
: F extends { type: "locatedTimestamp" }
|
|
284
|
+
? F extends { required: true }
|
|
285
|
+
? { readonly [P in `${K}Utc`]: Col<Temporal.Instant> } & {
|
|
286
|
+
readonly [P in `${K}Tz`]: Col<string>;
|
|
287
|
+
}
|
|
288
|
+
: { readonly [P in `${K}Utc`]: NullCol<Temporal.Instant> } & {
|
|
289
|
+
readonly [P in `${K}Tz`]: NullCol<string>;
|
|
290
|
+
}
|
|
291
|
+
: F extends { type: "file" | "image" }
|
|
292
|
+
? F extends { required: true }
|
|
293
|
+
? { readonly [P in K]: Col<string> }
|
|
294
|
+
: { readonly [P in K]: NullCol<string> }
|
|
295
|
+
: F extends { type: "files" | "images" }
|
|
296
|
+
? Record<never, never>
|
|
297
|
+
: never;
|
|
298
|
+
|
|
299
|
+
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
|
|
300
|
+
k: infer I,
|
|
301
|
+
) => void
|
|
302
|
+
? I
|
|
303
|
+
: never;
|
|
304
|
+
|
|
305
|
+
type ColumnsForEntity<F extends FieldsMap> = UnionToIntersection<
|
|
306
|
+
{
|
|
307
|
+
[K in keyof F & string]: ColumnsForField<K, F[K]>;
|
|
308
|
+
}[keyof F & string]
|
|
309
|
+
>;
|
|
310
|
+
|
|
311
|
+
// Base-Spalten von buildBaseColumns — `idType: "serial"` returnt number,
|
|
312
|
+
// sonst uuid-as-string. `insertedAt` hat `default(now())`, ist also
|
|
313
|
+
// strukturell non-null (Drizzle's `notNull` flag matcht das).
|
|
314
|
+
type BaseColumnsType<E extends EntityDefinition> = {
|
|
315
|
+
readonly id: E extends { idType: "serial" } ? Col<number> : Col<string>;
|
|
316
|
+
readonly tenantId: Col<string>;
|
|
317
|
+
readonly version: Col<number>;
|
|
318
|
+
readonly insertedAt: Col<Temporal.Instant>;
|
|
319
|
+
readonly modifiedAt: NullCol<Temporal.Instant>;
|
|
320
|
+
readonly insertedById: NullCol<string>;
|
|
321
|
+
readonly modifiedById: NullCol<string>;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// SoftDelete-Spalten existieren nur wenn entity.softDelete === true. Das
|
|
325
|
+
// Type-Level kann das nicht klein narrowen ohne Generic auf softDelete,
|
|
326
|
+
// also unionen wir beide Sets — Lean-Entities sehen die never-präsenten
|
|
327
|
+
// Spalten als typed-existierend, was dem alten `<any>`-Verhalten matcht.
|
|
328
|
+
type SoftDeleteColumnsType = {
|
|
329
|
+
readonly isDeleted: Col<boolean>;
|
|
330
|
+
readonly deletedAt: NullCol<Temporal.Instant>;
|
|
331
|
+
readonly deletedById: NullCol<string>;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export type DrizzleTable<E extends EntityDefinition = EntityDefinition> =
|
|
335
|
+
TableColumns<// biome-ignore lint/suspicious/noExplicitAny: drizzle's internal table-config stays generic; we layer typed columns on top via the intersection below.
|
|
336
|
+
any> &
|
|
337
|
+
BaseColumnsType<E> &
|
|
338
|
+
SoftDeleteColumnsType &
|
|
339
|
+
ColumnsForEntity<E["fields"]>;
|
|
340
|
+
|
|
341
|
+
export function buildBaseColumns(softDelete: boolean, idType: "serial" | "uuid" = "uuid") {
|
|
342
|
+
const idColumn =
|
|
343
|
+
idType === "uuid"
|
|
344
|
+
? uuid("id").primaryKey().default(sql`gen_random_uuid()`)
|
|
345
|
+
: serial("id").primaryKey();
|
|
346
|
+
|
|
347
|
+
const base = {
|
|
348
|
+
id: idColumn,
|
|
349
|
+
tenantId: uuid("tenant_id").notNull(),
|
|
350
|
+
version: integer("version").default(1).notNull(),
|
|
351
|
+
// Sprint F: Temporal.Instant durchgängig (siehe instant() in dialect.ts).
|
|
352
|
+
// Vorher mode default "date" → Inkonsistenz mit user-defined timestamp
|
|
353
|
+
// Felder (mode "string"). Jetzt: ein Mode für alle Timestamps.
|
|
354
|
+
// customType doesn't expose Drizzle's `defaultNow()` shortcut — use raw
|
|
355
|
+
// SQL so PG sets the value on insert and we don't need to pass an
|
|
356
|
+
// Instant from JS for every row create.
|
|
357
|
+
insertedAt: instant("inserted_at").default(sql`now()`).notNull(),
|
|
358
|
+
modifiedAt: instant("modified_at"),
|
|
359
|
+
// User-IDs are stringified UUIDs post-ES migration. Text (not uuid) so the
|
|
360
|
+
// columns accept system actors ("SYSTEM", "SEED", etc.) and legacy-shaped
|
|
361
|
+
// integer ids during transitional tests.
|
|
362
|
+
insertedById: text("inserted_by_id"),
|
|
363
|
+
modifiedById: text("modified_by_id"),
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (softDelete) {
|
|
367
|
+
return {
|
|
368
|
+
...base,
|
|
369
|
+
isDeleted: boolean("is_deleted").default(false).notNull(),
|
|
370
|
+
deletedAt: instant("deleted_at"),
|
|
371
|
+
deletedById: text("deleted_by_id"),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return base;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export type BuildDrizzleTableOptions = {
|
|
379
|
+
readonly featureName?: string;
|
|
380
|
+
// Relations declared for this entity. When present, every belongsTo
|
|
381
|
+
// foreignKey gets an index — otherwise joins and `WHERE fk = ?` filters
|
|
382
|
+
// sequential-scan the child table. Pass the output of
|
|
383
|
+
// `registry.getRelations(entityName)` or the raw relations block.
|
|
384
|
+
readonly relations?: EntityRelations;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
export function buildDrizzleTable<E extends EntityDefinition>(
|
|
388
|
+
entityName: string,
|
|
389
|
+
entity: E,
|
|
390
|
+
options?: BuildDrizzleTableOptions,
|
|
391
|
+
): DrizzleTable<E> {
|
|
392
|
+
const baseColumns = buildBaseColumns(entity.softDelete ?? false, entity.idType ?? "uuid");
|
|
393
|
+
const fieldColumns: Record<string, ColumnBuilder> = {};
|
|
394
|
+
|
|
395
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
396
|
+
const cols = fieldToColumns(name, field, entity);
|
|
397
|
+
Object.assign(fieldColumns, cols);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Default table name derived from entityName (e.g. "memberTask" → "read_member_tasks")
|
|
401
|
+
const baseTableName = entity.table ?? toTableName(entityName);
|
|
402
|
+
// featureName-prefix wird zwischen read_ und den base-Namen geschoben,
|
|
403
|
+
// damit alle read-models einheitlich mit `read_` starten — egal ob
|
|
404
|
+
// featureName gesetzt ist oder nicht. Beispiel:
|
|
405
|
+
// featureName="shop", base="read_orders" → "read_shop_orders"
|
|
406
|
+
// featureName=undef, base="read_orders" → "read_orders"
|
|
407
|
+
// featureName="shop", base="orders" (no read_) → "shop_orders"
|
|
408
|
+
const tableName = options?.featureName
|
|
409
|
+
? baseTableName.startsWith(READ_MODEL_PREFIX)
|
|
410
|
+
? `${READ_MODEL_PREFIX}${options.featureName}_${baseTableName.slice(READ_MODEL_PREFIX.length)}`
|
|
411
|
+
: `${options.featureName}_${baseTableName}`
|
|
412
|
+
: baseTableName;
|
|
413
|
+
|
|
414
|
+
// Build the list of foreign-key columns to index. Sources:
|
|
415
|
+
// (a) single-file / single-image fields store a fileRef id and are queried
|
|
416
|
+
// by that id whenever a detail view resolves attachments.
|
|
417
|
+
// (b) belongsTo relations declared via r.relation() — the FK column is the
|
|
418
|
+
// parent-side lookup key; without an index every child join scans the
|
|
419
|
+
// full table.
|
|
420
|
+
// `Set` keeps the list deduplicated when (a) and (b) name the same column.
|
|
421
|
+
const foreignKeyFields = new Set<string>();
|
|
422
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
423
|
+
if (field.type === "file" || field.type === "image") {
|
|
424
|
+
foreignKeyFields.add(name);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (options?.relations) {
|
|
428
|
+
for (const rel of Object.values(options.relations)) {
|
|
429
|
+
if (rel.type === "belongsTo") foreignKeyFields.add(rel.foreignKey);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Cast back to DrizzleTable<E>: drizzle-kit's pgTable returns a fully
|
|
434
|
+
// inferred PgTableWithColumns over the *exact* column-builder map we
|
|
435
|
+
// hand in. Our typed signature narrows that to the static names from
|
|
436
|
+
// EntityDefinition (kept in sync with fieldToColumns + buildBaseColumns).
|
|
437
|
+
// Drizzle's runtime instance carries every needed method on top.
|
|
438
|
+
return pgTable(
|
|
439
|
+
tableName,
|
|
440
|
+
{
|
|
441
|
+
...baseColumns,
|
|
442
|
+
...fieldColumns,
|
|
443
|
+
},
|
|
444
|
+
// Every multi-tenant query filters by tenant_id. Without this index, list
|
|
445
|
+
// queries scan the whole table across all tenants. Applies to every table
|
|
446
|
+
// built via buildDrizzleTable since every entity inherits tenantId.
|
|
447
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle's table callback is generic; we access columns by their JS property name.
|
|
448
|
+
(table: any) => {
|
|
449
|
+
const indexes = [index(`${tableName}_tenant_id_idx`).on(table.tenantId)];
|
|
450
|
+
for (const fieldName of foreignKeyFields) {
|
|
451
|
+
const column = table[fieldName];
|
|
452
|
+
if (column) {
|
|
453
|
+
indexes.push(index(`${tableName}_${toSnakeCase(fieldName)}_idx`).on(column));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// entity.indexes = composite/unique-Indices die der Author explizit
|
|
457
|
+
// deklariert hat. Spalten werden via field-name (camelCase) angesprochen,
|
|
458
|
+
// der Index-Name folgt der Convention <table>_<col1>_<col2>_<unique|idx>
|
|
459
|
+
// — Override via index.name möglich.
|
|
460
|
+
for (const def of entity.indexes ?? []) {
|
|
461
|
+
const cols = def.columns
|
|
462
|
+
.map((fieldName) => table[fieldName])
|
|
463
|
+
.filter((col): col is unknown => col !== undefined);
|
|
464
|
+
if (cols.length !== def.columns.length) continue; // Boot-Validator catched das
|
|
465
|
+
const suffix = def.unique === true ? "unique" : "idx";
|
|
466
|
+
const indexName =
|
|
467
|
+
def.name ?? `${tableName}_${def.columns.map((c) => toSnakeCase(c)).join("_")}_${suffix}`;
|
|
468
|
+
const builder = def.unique === true ? uniqueIndex(indexName) : index(indexName);
|
|
469
|
+
// biome-ignore lint/suspicious/noExplicitAny: drizzle's .on(...cols) is variadic generic
|
|
470
|
+
indexes.push((builder.on as any)(...cols));
|
|
471
|
+
}
|
|
472
|
+
return indexes;
|
|
473
|
+
},
|
|
474
|
+
) as unknown as DrizzleTable<E>;
|
|
475
|
+
}
|