@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,59 @@
|
|
|
1
|
+
import type { SQL } from "drizzle-orm";
|
|
2
|
+
import { describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { fetchOne } from "../row-helpers";
|
|
4
|
+
|
|
5
|
+
// Drizzle builder chain mocked structurally — fetchOne only calls
|
|
6
|
+
// db.select().from(table).where(where).limit(1) and reads the first row.
|
|
7
|
+
function makeFakeDb(rows: unknown[]) {
|
|
8
|
+
const limit = vi.fn().mockResolvedValue(rows);
|
|
9
|
+
const where = vi.fn(() => ({ limit }));
|
|
10
|
+
const from = vi.fn(() => ({ where }));
|
|
11
|
+
const select = vi.fn(() => ({ from }));
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: test shim — we never feed this to a real DbRunner type check.
|
|
13
|
+
return { db: { select } as any, select, from, where, limit };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fakeTable = {} as never;
|
|
17
|
+
const fakeCond1 = { __c: 1 } as unknown as SQL;
|
|
18
|
+
const fakeCond2 = { __c: 2 } as unknown as SQL;
|
|
19
|
+
|
|
20
|
+
describe("fetchOne", () => {
|
|
21
|
+
test("returns the first row when the query yields at least one match", async () => {
|
|
22
|
+
const { db } = makeFakeDb([
|
|
23
|
+
{ id: 1, name: "alice" },
|
|
24
|
+
{ id: 2, name: "bob" },
|
|
25
|
+
]);
|
|
26
|
+
const row = await fetchOne<{ id: number; name: string }>(db, fakeTable, fakeCond1);
|
|
27
|
+
expect(row).toEqual({ id: 1, name: "alice" });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("returns undefined on an empty result", async () => {
|
|
31
|
+
const { db } = makeFakeDb([]);
|
|
32
|
+
const row = await fetchOne(db, fakeTable, fakeCond1);
|
|
33
|
+
expect(row).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("applies limit(1) — no need to pull the whole table", async () => {
|
|
37
|
+
const { db, limit } = makeFakeDb([]);
|
|
38
|
+
await fetchOne(db, fakeTable, fakeCond1);
|
|
39
|
+
expect(limit).toHaveBeenCalledWith(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("passes the single condition directly to .where (no AND wrapping)", async () => {
|
|
43
|
+
const { db, where } = makeFakeDb([]);
|
|
44
|
+
await fetchOne(db, fakeTable, fakeCond1);
|
|
45
|
+
expect(where).toHaveBeenCalledWith(fakeCond1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("combines multiple conditions with AND", async () => {
|
|
49
|
+
const { db, where } = makeFakeDb([]);
|
|
50
|
+
await fetchOne(db, fakeTable, fakeCond1, fakeCond2);
|
|
51
|
+
const calls = where.mock.calls as unknown as readonly (readonly unknown[])[];
|
|
52
|
+
const arg = calls[0]?.[0];
|
|
53
|
+
// drizzle's and() returns an SQL expression — we can't cheaply inspect
|
|
54
|
+
// its innards, but it must not be the raw first condition and must be
|
|
55
|
+
// defined (i.e. the helper actually composed something).
|
|
56
|
+
expect(arg).toBeDefined();
|
|
57
|
+
expect(arg).not.toBe(fakeCond1);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
createBooleanField,
|
|
5
|
+
createDateField,
|
|
6
|
+
createEntity,
|
|
7
|
+
createNumberField,
|
|
8
|
+
createTextField,
|
|
9
|
+
defineFeature,
|
|
10
|
+
} from "../../engine";
|
|
11
|
+
import type { FeatureDefinition } from "../../engine/types";
|
|
12
|
+
import { createTestDb, pushTables, type TestDb } from "../../stack";
|
|
13
|
+
import { buildDrizzleTable } from "../table-builder";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Integration tests for the schema migration workflow.
|
|
17
|
+
* Tests real developer scenarios: new feature, add field, change field, etc.
|
|
18
|
+
*
|
|
19
|
+
* Each test simulates:
|
|
20
|
+
* 1. Developer defines/changes entities
|
|
21
|
+
* 2. buildDrizzleTable creates Drizzle table objects
|
|
22
|
+
* 3. Schema is applied to a real database via pushTables (drizzle-kit push)
|
|
23
|
+
* 4. We verify the DB state matches expectations
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
let testDb: TestDb;
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
testDb = await createTestDb();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
await testDb?.cleanup();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Helper: apply schema by building Drizzle tables and pushing via drizzle-kit
|
|
37
|
+
async function applySchema(features: readonly FeatureDefinition[]): Promise<void> {
|
|
38
|
+
const tables: Record<string, unknown> = {};
|
|
39
|
+
for (const feature of features) {
|
|
40
|
+
for (const [entityName, entity] of Object.entries(feature.entities)) {
|
|
41
|
+
tables[entityName] = buildDrizzleTable(entityName, entity);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await pushTables(testDb.db, tables);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Helper: read column info from information_schema
|
|
48
|
+
async function getTableColumns(
|
|
49
|
+
tableName: string,
|
|
50
|
+
): Promise<Map<string, { dataType: string; isNullable: boolean }>> {
|
|
51
|
+
const rows = await testDb.db.execute<{
|
|
52
|
+
column_name: string;
|
|
53
|
+
data_type: string;
|
|
54
|
+
is_nullable: string;
|
|
55
|
+
}>(
|
|
56
|
+
sql`SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = ${tableName} ORDER BY ordinal_position`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const result = new Map<string, { dataType: string; isNullable: boolean }>();
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
result.set(row.column_name, {
|
|
62
|
+
dataType: row.data_type,
|
|
63
|
+
isNullable: row.is_nullable === "YES",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("schema migration workflows", () => {
|
|
70
|
+
test("workflow 1: new feature creates table with all base + field columns", async () => {
|
|
71
|
+
const feature = defineFeature("blog", (r) => {
|
|
72
|
+
r.entity(
|
|
73
|
+
"post",
|
|
74
|
+
createEntity({
|
|
75
|
+
table: "wf1_posts",
|
|
76
|
+
fields: {
|
|
77
|
+
title: createTextField({ required: true }),
|
|
78
|
+
body: createTextField(),
|
|
79
|
+
viewCount: createNumberField(),
|
|
80
|
+
publishedAt: createDateField(),
|
|
81
|
+
isDraft: createBooleanField({ default: true }),
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await applySchema([feature]);
|
|
88
|
+
|
|
89
|
+
const columns = await getTableColumns("wf1_posts");
|
|
90
|
+
|
|
91
|
+
// Base columns
|
|
92
|
+
expect(columns.has("id")).toBe(true);
|
|
93
|
+
expect(columns.has("tenant_id")).toBe(true);
|
|
94
|
+
expect(columns.has("version")).toBe(true);
|
|
95
|
+
expect(columns.has("inserted_at")).toBe(true);
|
|
96
|
+
expect(columns.has("modified_at")).toBe(true);
|
|
97
|
+
expect(columns.has("inserted_by_id")).toBe(true);
|
|
98
|
+
expect(columns.has("modified_by_id")).toBe(true);
|
|
99
|
+
|
|
100
|
+
// Entity fields
|
|
101
|
+
expect(columns.get("title")?.dataType).toBe("text");
|
|
102
|
+
expect(columns.get("body")?.dataType).toBe("text");
|
|
103
|
+
expect(columns.get("view_count")?.dataType).toBe("integer");
|
|
104
|
+
expect(columns.get("published_at")?.dataType).toContain("timestamp");
|
|
105
|
+
expect(columns.get("is_draft")?.dataType).toBe("boolean");
|
|
106
|
+
expect(columns.get("is_draft")?.isNullable).toBe(false); // has default → NOT NULL
|
|
107
|
+
|
|
108
|
+
// No soft delete
|
|
109
|
+
expect(columns.has("is_deleted")).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("workflow 1b: tenant_id index is created on every table", async () => {
|
|
113
|
+
const feature = defineFeature("indexed-blog", (r) => {
|
|
114
|
+
r.entity(
|
|
115
|
+
"article",
|
|
116
|
+
createEntity({
|
|
117
|
+
table: "wf1b_articles",
|
|
118
|
+
fields: { title: createTextField() },
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
await applySchema([feature]);
|
|
123
|
+
|
|
124
|
+
const indexRows = await testDb.db.execute<{ indexname: string; indexdef: string }>(
|
|
125
|
+
sql.raw(
|
|
126
|
+
`SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'wf1b_articles' AND indexname = 'wf1b_articles_tenant_id_idx'`,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
expect(indexRows.length).toBe(1);
|
|
130
|
+
expect(indexRows[0]?.indexdef).toContain("tenant_id");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("workflow 2: add field to existing entity → ADD COLUMN", async () => {
|
|
134
|
+
// Initial entity with just email
|
|
135
|
+
const initialEntity = createEntity({
|
|
136
|
+
table: "wf2_users",
|
|
137
|
+
fields: { email: createTextField() },
|
|
138
|
+
});
|
|
139
|
+
await pushTables(testDb.db, { user: buildDrizzleTable("user", initialEntity) });
|
|
140
|
+
|
|
141
|
+
// Developer adds a new field
|
|
142
|
+
const updatedEntity = createEntity({
|
|
143
|
+
table: "wf2_users",
|
|
144
|
+
fields: {
|
|
145
|
+
email: createTextField(),
|
|
146
|
+
displayName: createTextField(), // NEW FIELD
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Push updated schema — drizzle-kit generates ALTER TABLE ADD COLUMN
|
|
151
|
+
await pushTables(
|
|
152
|
+
testDb.db,
|
|
153
|
+
{ user: buildDrizzleTable("user", updatedEntity) },
|
|
154
|
+
{ user: buildDrizzleTable("user", initialEntity) },
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const columns = await getTableColumns("wf2_users");
|
|
158
|
+
expect(columns.has("display_name")).toBe(true);
|
|
159
|
+
expect(columns.has("email")).toBe(true);
|
|
160
|
+
expect(columns.get("display_name")?.dataType).toBe("text");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("workflow 3: add required boolean field with default → safe ADD COLUMN", async () => {
|
|
164
|
+
// Initial entity with just name
|
|
165
|
+
const initialEntity = createEntity({
|
|
166
|
+
table: "wf3_projects",
|
|
167
|
+
fields: { name: createTextField() },
|
|
168
|
+
});
|
|
169
|
+
const initialTable = buildDrizzleTable("project", initialEntity);
|
|
170
|
+
await pushTables(testDb.db, { project: initialTable });
|
|
171
|
+
|
|
172
|
+
// Insert a row first (to prove ADD COLUMN with default doesn't break existing rows)
|
|
173
|
+
await testDb.db
|
|
174
|
+
.insert(initialTable)
|
|
175
|
+
.values({ tenantId: "00000000-0000-4000-8000-000000000001", name: "Test Project" });
|
|
176
|
+
|
|
177
|
+
// Developer adds boolean field with default
|
|
178
|
+
const updatedEntity = createEntity({
|
|
179
|
+
table: "wf3_projects",
|
|
180
|
+
fields: { name: createTextField(), isArchived: createBooleanField({ default: false }) },
|
|
181
|
+
});
|
|
182
|
+
const updatedTable = buildDrizzleTable("project", updatedEntity);
|
|
183
|
+
await pushTables(testDb.db, { project: updatedTable }, { project: initialTable });
|
|
184
|
+
|
|
185
|
+
// Existing row should have the default value
|
|
186
|
+
const rows = await testDb.db.select().from(updatedTable);
|
|
187
|
+
|
|
188
|
+
expect(rows[0]).toMatchObject({ name: "Test Project", isArchived: false });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("workflow 3b: text field with default → DEFAULT-clause backfills existing rows", async () => {
|
|
192
|
+
// Pinned der text-default-bug: `createTextField({ default: "..." })`
|
|
193
|
+
// muss die DEFAULT-Klausel ins generierte SQL durchreichen, sonst
|
|
194
|
+
// bricht ALTER TABLE ADD COLUMN auf existing data (NOT NULL ohne
|
|
195
|
+
// DEFAULT failt). Vorher hat der text-Branch in table-builder das
|
|
196
|
+
// `field.default` ignoriert; dieser Test pinst dass es jetzt
|
|
197
|
+
// greift.
|
|
198
|
+
const initialEntity = createEntity({
|
|
199
|
+
table: "wf3b_users",
|
|
200
|
+
fields: { email: createTextField({ required: true }) },
|
|
201
|
+
});
|
|
202
|
+
const initialTable = buildDrizzleTable("user", initialEntity);
|
|
203
|
+
await pushTables(testDb.db, { user: initialTable });
|
|
204
|
+
|
|
205
|
+
await testDb.db
|
|
206
|
+
.insert(initialTable)
|
|
207
|
+
.values({ tenantId: "00000000-0000-4000-8000-000000000001", email: "x@y.z" });
|
|
208
|
+
|
|
209
|
+
const updatedEntity = createEntity({
|
|
210
|
+
table: "wf3b_users",
|
|
211
|
+
fields: {
|
|
212
|
+
email: createTextField({ required: true }),
|
|
213
|
+
roles: createTextField({ required: true, default: "[]" }),
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const updatedTable = buildDrizzleTable("user", updatedEntity);
|
|
217
|
+
await pushTables(testDb.db, { user: updatedTable }, { user: initialTable });
|
|
218
|
+
|
|
219
|
+
const rows = await testDb.db.select().from(updatedTable);
|
|
220
|
+
expect(rows[0]).toMatchObject({ email: "x@y.z", roles: "[]" });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("workflow 4: activate soft delete → adds 3 columns", async () => {
|
|
224
|
+
const feature = defineFeature("tasks", (r) => {
|
|
225
|
+
r.entity(
|
|
226
|
+
"task",
|
|
227
|
+
createEntity({
|
|
228
|
+
table: "wf4_tasks",
|
|
229
|
+
fields: { title: createTextField() },
|
|
230
|
+
softDelete: true,
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await applySchema([feature]);
|
|
236
|
+
|
|
237
|
+
const columns = await getTableColumns("wf4_tasks");
|
|
238
|
+
|
|
239
|
+
expect(columns.has("is_deleted")).toBe(true);
|
|
240
|
+
expect(columns.get("is_deleted")?.isNullable).toBe(false);
|
|
241
|
+
expect(columns.has("deleted_at")).toBe(true);
|
|
242
|
+
expect(columns.get("deleted_at")?.isNullable).toBe(true);
|
|
243
|
+
expect(columns.has("deleted_by_id")).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("workflow 5: multiple features each create their own tables", async () => {
|
|
247
|
+
const blogFeature = defineFeature("blog", (r) => {
|
|
248
|
+
r.entity(
|
|
249
|
+
"article",
|
|
250
|
+
createEntity({ table: "wf5_articles", fields: { title: createTextField() } }),
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const shopFeature = defineFeature("shop", (r) => {
|
|
255
|
+
r.entity(
|
|
256
|
+
"product",
|
|
257
|
+
createEntity({
|
|
258
|
+
table: "wf5_products",
|
|
259
|
+
fields: { name: createTextField(), price: createNumberField() },
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await applySchema([blogFeature, shopFeature]);
|
|
265
|
+
|
|
266
|
+
const articleColumns = await getTableColumns("wf5_articles");
|
|
267
|
+
const productColumns = await getTableColumns("wf5_products");
|
|
268
|
+
|
|
269
|
+
expect(articleColumns.has("title")).toBe(true);
|
|
270
|
+
expect(productColumns.has("name")).toBe(true);
|
|
271
|
+
expect(productColumns.has("price")).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Unit-Tests für entity.indexes — Composite-/Unique-Index-API.
|
|
2
|
+
//
|
|
3
|
+
// Vorher mussten Apps für unique-indices über mehrere Spalten daneben eine
|
|
4
|
+
// hand-written pgTable-Definition halten — Single-Source-of-Truth gebrochen,
|
|
5
|
+
// Schema-Drift programmiert. Mit entity.indexes pflegen Author die
|
|
6
|
+
// Constraint deklarativ in der EntityDefinition; buildDrizzleTable rendert
|
|
7
|
+
// sie via uniqueIndex/index.
|
|
8
|
+
|
|
9
|
+
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import {
|
|
12
|
+
createBooleanField,
|
|
13
|
+
createEntity,
|
|
14
|
+
createTextField,
|
|
15
|
+
defineFeature,
|
|
16
|
+
validateBoot,
|
|
17
|
+
} from "../../engine";
|
|
18
|
+
import { buildDrizzleTable } from "../table-builder";
|
|
19
|
+
|
|
20
|
+
describe("buildDrizzleTable — entity.indexes", () => {
|
|
21
|
+
test("composite unique-index landet als unique=true in Drizzle table-config", () => {
|
|
22
|
+
const entity = createEntity({
|
|
23
|
+
fields: {
|
|
24
|
+
key: createTextField({ required: true }),
|
|
25
|
+
userId: createTextField({}),
|
|
26
|
+
},
|
|
27
|
+
indexes: [{ unique: true, columns: ["key", "tenantId", "userId"] }],
|
|
28
|
+
});
|
|
29
|
+
const tbl = buildDrizzleTable("config-value", entity);
|
|
30
|
+
const { indexes } = getTableConfig(tbl);
|
|
31
|
+
const composite = indexes.find(
|
|
32
|
+
(i) => i.config.name === "read_config_values_key_tenant_id_user_id_unique",
|
|
33
|
+
);
|
|
34
|
+
expect(composite).toBeDefined();
|
|
35
|
+
expect(composite?.config.unique).toBe(true);
|
|
36
|
+
expect(composite?.config.columns.map((c) => (c as { name: string }).name)).toEqual([
|
|
37
|
+
"key",
|
|
38
|
+
"tenant_id",
|
|
39
|
+
"user_id",
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("composite non-unique-index landet als unique=false in Drizzle table-config", () => {
|
|
44
|
+
const entity = createEntity({
|
|
45
|
+
fields: {
|
|
46
|
+
startedAt: createTextField({}),
|
|
47
|
+
endedAt: createTextField({}),
|
|
48
|
+
},
|
|
49
|
+
indexes: [{ columns: ["startedAt", "endedAt"] }],
|
|
50
|
+
});
|
|
51
|
+
const tbl = buildDrizzleTable("session", entity);
|
|
52
|
+
const { indexes } = getTableConfig(tbl);
|
|
53
|
+
const composite = indexes.find(
|
|
54
|
+
(i) => i.config.name === "read_sessions_started_at_ended_at_idx",
|
|
55
|
+
);
|
|
56
|
+
expect(composite).toBeDefined();
|
|
57
|
+
expect(composite?.config.unique).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("custom name override wird respektiert", () => {
|
|
61
|
+
const entity = createEntity({
|
|
62
|
+
fields: { slug: createTextField({ required: true }) },
|
|
63
|
+
indexes: [{ unique: true, columns: ["slug"], name: "my_custom_idx" }],
|
|
64
|
+
});
|
|
65
|
+
const tbl = buildDrizzleTable("page", entity);
|
|
66
|
+
const { indexes } = getTableConfig(tbl);
|
|
67
|
+
const idx = indexes.find((i) => i.config.name === "my_custom_idx");
|
|
68
|
+
expect(idx).toBeDefined();
|
|
69
|
+
expect(idx?.config.unique).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("ohne indexes — keine zusätzlichen Indices, kein Error", () => {
|
|
73
|
+
const entity = createEntity({
|
|
74
|
+
fields: {
|
|
75
|
+
title: createTextField({}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
expect(() => buildDrizzleTable("widget", entity)).not.toThrow();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("Spalten die keine DB-Spalte haben (multi-files) werden via Boot-Validator gecatched", () => {
|
|
82
|
+
// buildDrizzleTable selbst überspringt fehlende Columns silently —
|
|
83
|
+
// der Boot-Validator wirft.
|
|
84
|
+
const entity = createEntity({
|
|
85
|
+
fields: {
|
|
86
|
+
attachments: { type: "files" } as never,
|
|
87
|
+
},
|
|
88
|
+
indexes: [{ columns: ["attachments"] }],
|
|
89
|
+
});
|
|
90
|
+
// No throw at build time.
|
|
91
|
+
expect(() => buildDrizzleTable("widget", entity)).not.toThrow();
|
|
92
|
+
// But validateBoot does.
|
|
93
|
+
const feature = defineFeature("widgetFeature", (r) => {
|
|
94
|
+
r.entity("widget", entity);
|
|
95
|
+
});
|
|
96
|
+
expect(() => validateBoot([feature])).toThrow(/multi-value field/);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("validateBoot — entity.indexes", () => {
|
|
101
|
+
test("Tippfehler im column-Namen wirft", () => {
|
|
102
|
+
const feature = defineFeature("widgetFeature", (r) => {
|
|
103
|
+
r.entity("widget", {
|
|
104
|
+
fields: { title: createTextField({}) },
|
|
105
|
+
indexes: [{ columns: ["titel"] }], // typo
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
expect(() => validateBoot([feature])).toThrow(/does not match any field/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("leere column-Liste wirft", () => {
|
|
112
|
+
const feature = defineFeature("widgetFeature", (r) => {
|
|
113
|
+
r.entity("widget", {
|
|
114
|
+
fields: { title: createTextField({}) },
|
|
115
|
+
indexes: [{ columns: [] as never }],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
expect(() => validateBoot([feature])).toThrow(/empty columns list/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("single-column index nur auf tenantId ist redundant", () => {
|
|
122
|
+
const feature = defineFeature("widgetFeature", (r) => {
|
|
123
|
+
r.entity("widget", {
|
|
124
|
+
fields: { title: createTextField({}) },
|
|
125
|
+
indexes: [{ columns: ["tenantId"] }],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
expect(() => validateBoot([feature])).toThrow(/redundant/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("composite mit tenantId ist OK (z.B. für unique über 3 Cols)", () => {
|
|
132
|
+
const feature = defineFeature("widgetFeature", (r) => {
|
|
133
|
+
r.entity("widget", {
|
|
134
|
+
fields: {
|
|
135
|
+
key: createTextField({ required: true }),
|
|
136
|
+
archived: createBooleanField({}),
|
|
137
|
+
},
|
|
138
|
+
indexes: [{ unique: true, columns: ["key", "tenantId"] }],
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("base columns (id, tenantId, version) sind erlaubt", () => {
|
|
145
|
+
const feature = defineFeature("widgetFeature", (r) => {
|
|
146
|
+
r.entity("widget", {
|
|
147
|
+
fields: { key: createTextField({}) },
|
|
148
|
+
indexes: [{ columns: ["tenantId", "version"] }],
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
152
|
+
});
|
|
153
|
+
});
|