@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,369 @@
|
|
|
1
|
+
import { getTableName } from "drizzle-orm";
|
|
2
|
+
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
createBooleanField,
|
|
6
|
+
createEntity,
|
|
7
|
+
createImageField,
|
|
8
|
+
createMultiSelectField,
|
|
9
|
+
createSelectField,
|
|
10
|
+
createTextField,
|
|
11
|
+
} from "../../engine";
|
|
12
|
+
import type { EntityRelations } from "../../engine/types";
|
|
13
|
+
import { decodeCursor, encodeCursor } from "../cursor";
|
|
14
|
+
import { buildBaseColumns, buildDrizzleTable, toTableName } from "../table-builder";
|
|
15
|
+
|
|
16
|
+
// --- Cursor encoding ---
|
|
17
|
+
|
|
18
|
+
describe("cursor encoding", () => {
|
|
19
|
+
// String-Roundtrip seit Sprint F: encodeCursor akzeptiert string|number,
|
|
20
|
+
// decodeCursor returnt immer einen String — UUID-IDs (Default) brauchen
|
|
21
|
+
// keine Number-Kapsel, Integer-IDs werden via PG-Cast in der WHERE-Clause
|
|
22
|
+
// korrekt verglichen. Detail-Tests in cursor.test.ts.
|
|
23
|
+
test.each([1, 42, 999, 100000])("encodes and decodes integer id %i", (id) => {
|
|
24
|
+
const cursor = encodeCursor(id);
|
|
25
|
+
expect(decodeCursor(cursor)).toBe(String(id));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("cursor is url-safe base64", () => {
|
|
29
|
+
const cursor = encodeCursor(12345);
|
|
30
|
+
expect(cursor).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("throws on empty/corrupted cursor", () => {
|
|
34
|
+
expect(() => decodeCursor("")).toThrow(/invalid cursor/i);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// --- Base columns ---
|
|
39
|
+
|
|
40
|
+
describe("buildBaseColumns", () => {
|
|
41
|
+
test("includes standard columns", () => {
|
|
42
|
+
const cols = buildBaseColumns(false);
|
|
43
|
+
expect(cols).toHaveProperty("id");
|
|
44
|
+
expect(cols).toHaveProperty("tenantId");
|
|
45
|
+
expect(cols).toHaveProperty("insertedAt");
|
|
46
|
+
expect(cols).toHaveProperty("modifiedAt");
|
|
47
|
+
expect(cols).toHaveProperty("insertedById");
|
|
48
|
+
expect(cols).toHaveProperty("modifiedById");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("without softDelete has no isDeleted", () => {
|
|
52
|
+
const cols = buildBaseColumns(false);
|
|
53
|
+
expect(cols).not.toHaveProperty("isDeleted");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("with softDelete includes isDeleted", () => {
|
|
57
|
+
const cols = buildBaseColumns(true);
|
|
58
|
+
expect(cols).toHaveProperty("isDeleted");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// --- Table builder ---
|
|
63
|
+
|
|
64
|
+
describe("buildDrizzleTable", () => {
|
|
65
|
+
test("creates table with base columns + entity fields", () => {
|
|
66
|
+
const entity = createEntity({
|
|
67
|
+
table: "users",
|
|
68
|
+
fields: {
|
|
69
|
+
email: createTextField({ required: true }),
|
|
70
|
+
firstName: createTextField(),
|
|
71
|
+
isEnabled: createBooleanField({ default: true }),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const table = buildDrizzleTable("user", entity);
|
|
76
|
+
|
|
77
|
+
// Has base columns
|
|
78
|
+
expect(table["id"]).toBeDefined();
|
|
79
|
+
expect(table["tenantId"]).toBeDefined();
|
|
80
|
+
expect(table["insertedAt"]).toBeDefined();
|
|
81
|
+
|
|
82
|
+
// Has entity fields
|
|
83
|
+
expect(table["email"]).toBeDefined();
|
|
84
|
+
expect(table["firstName"]).toBeDefined();
|
|
85
|
+
expect(table["isEnabled"]).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("soft delete entity includes isDeleted column", () => {
|
|
89
|
+
const entity = createEntity({
|
|
90
|
+
table: "users",
|
|
91
|
+
fields: { email: createTextField() },
|
|
92
|
+
softDelete: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const table = buildDrizzleTable("user", entity);
|
|
96
|
+
expect(table["isDeleted"]).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("select field becomes text column", () => {
|
|
100
|
+
const entity = createEntity({
|
|
101
|
+
table: "users",
|
|
102
|
+
fields: {
|
|
103
|
+
locale: createSelectField({ options: ["de", "en"] as const }),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const table = buildDrizzleTable("user", entity);
|
|
108
|
+
expect(table["locale"]).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("multiSelect field becomes jsonb column with default []", () => {
|
|
112
|
+
const entity = createEntity({
|
|
113
|
+
table: "drivers",
|
|
114
|
+
fields: {
|
|
115
|
+
licenceClasses: createMultiSelectField({ options: ["B", "BE", "C"] as const }),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const table = buildDrizzleTable("driver", entity);
|
|
120
|
+
const config = getTableConfig(table);
|
|
121
|
+
const column = config.columns.find((c) => c.name === "licence_classes");
|
|
122
|
+
expect(column).toBeDefined();
|
|
123
|
+
// jsonb-customType: column-data-type ist string ("jsonb"); column-type
|
|
124
|
+
// hier reicht als Smoke — die Default-`[]`-Garantie testen wir indirekt
|
|
125
|
+
// über die Migration-Rebuild-Integration-Tests, die echte Inserts
|
|
126
|
+
// gegen Postgres machen.
|
|
127
|
+
expect(column?.dataType).toBe("json");
|
|
128
|
+
expect(column?.default).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("converts camelCase to snake_case", () => {
|
|
132
|
+
const entity = createEntity({
|
|
133
|
+
table: "users",
|
|
134
|
+
fields: {
|
|
135
|
+
firstName: createTextField(),
|
|
136
|
+
employmentType: createSelectField({ options: ["FullTime", "PartTime"] as const }),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const table = buildDrizzleTable("user", entity);
|
|
141
|
+
// Column objects exist under camelCase keys
|
|
142
|
+
expect(table["firstName"]).toBeDefined();
|
|
143
|
+
expect(table["employmentType"]).toBeDefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("featureName option prefixes table name", () => {
|
|
147
|
+
const entity = createEntity({
|
|
148
|
+
table: "orders",
|
|
149
|
+
fields: { name: createTextField() },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const table = buildDrizzleTable("order", entity, { featureName: "shop" });
|
|
153
|
+
expect(getTableName(table)).toBe("shop_orders");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("without featureName, table name is unchanged", () => {
|
|
157
|
+
const entity = createEntity({
|
|
158
|
+
table: "orders",
|
|
159
|
+
fields: { name: createTextField() },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const table = buildDrizzleTable("order", entity);
|
|
163
|
+
expect(getTableName(table)).toBe("orders");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("derives table name from entityName when table is omitted", () => {
|
|
167
|
+
const entity = createEntity({ fields: { name: createTextField() } });
|
|
168
|
+
const table = buildDrizzleTable("task", entity);
|
|
169
|
+
expect(getTableName(table)).toBe("read_tasks");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("derives table name with featureName prefix when table is omitted", () => {
|
|
173
|
+
const entity = createEntity({ fields: { name: createTextField() } });
|
|
174
|
+
const table = buildDrizzleTable("order", entity, { featureName: "shop" });
|
|
175
|
+
// featureName-Prefix landet zwischen `read_` und dem Plural — alle
|
|
176
|
+
// Read-Models starten konsistent mit `read_`, egal ob ein Feature-
|
|
177
|
+
// Prefix gesetzt ist oder nicht.
|
|
178
|
+
expect(getTableName(table)).toBe("read_shop_orders");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// --- Auto-Indices ---
|
|
183
|
+
|
|
184
|
+
describe("buildDrizzleTable auto-indices", () => {
|
|
185
|
+
test("every table gets a tenant_id index", () => {
|
|
186
|
+
const entity = createEntity({
|
|
187
|
+
table: "users",
|
|
188
|
+
fields: { email: createTextField() },
|
|
189
|
+
});
|
|
190
|
+
const table = buildDrizzleTable("user", entity);
|
|
191
|
+
const { indexes } = getTableConfig(table);
|
|
192
|
+
|
|
193
|
+
const tenantIndex = indexes.find((idx) => idx.config.name === "users_tenant_id_idx");
|
|
194
|
+
expect(tenantIndex).toBeDefined();
|
|
195
|
+
expect(tenantIndex?.config.columns.map((c) => (c as { name: string }).name)).toEqual([
|
|
196
|
+
"tenant_id",
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("file field produces an index on its column", () => {
|
|
201
|
+
const entity = createEntity({
|
|
202
|
+
table: "documents",
|
|
203
|
+
fields: {
|
|
204
|
+
title: createTextField(),
|
|
205
|
+
avatar: createImageField(),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
const table = buildDrizzleTable("document", entity);
|
|
209
|
+
const { indexes } = getTableConfig(table);
|
|
210
|
+
|
|
211
|
+
const avatarIndex = indexes.find((idx) => idx.config.name === "documents_avatar_idx");
|
|
212
|
+
expect(avatarIndex).toBeDefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("index names include feature prefix when featureName is set", () => {
|
|
216
|
+
const entity = createEntity({
|
|
217
|
+
table: "items",
|
|
218
|
+
fields: { name: createTextField() },
|
|
219
|
+
});
|
|
220
|
+
const table = buildDrizzleTable("item", entity, { featureName: "shop" });
|
|
221
|
+
const { indexes } = getTableConfig(table);
|
|
222
|
+
|
|
223
|
+
expect(indexes.some((idx) => idx.config.name === "shop_items_tenant_id_idx")).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("table without file fields or relations has only the tenant index", () => {
|
|
227
|
+
const entity = createEntity({
|
|
228
|
+
table: "notes",
|
|
229
|
+
fields: { body: createTextField() },
|
|
230
|
+
});
|
|
231
|
+
const table = buildDrizzleTable("note", entity);
|
|
232
|
+
const { indexes } = getTableConfig(table);
|
|
233
|
+
|
|
234
|
+
expect(indexes).toHaveLength(1);
|
|
235
|
+
expect(indexes[0]?.config.name).toBe("notes_tenant_id_idx");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("belongsTo relations produce an index on their foreign key column", () => {
|
|
239
|
+
const entity = createEntity({
|
|
240
|
+
table: "tasks",
|
|
241
|
+
fields: {
|
|
242
|
+
title: createTextField({ required: true }),
|
|
243
|
+
assigneeId: createTextField(),
|
|
244
|
+
projectId: createTextField(),
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
const relations: EntityRelations = {
|
|
248
|
+
assignee: { type: "belongsTo", target: "user", foreignKey: "assigneeId" },
|
|
249
|
+
project: { type: "belongsTo", target: "project", foreignKey: "projectId" },
|
|
250
|
+
};
|
|
251
|
+
const table = buildDrizzleTable("task", entity, { relations });
|
|
252
|
+
const { indexes } = getTableConfig(table);
|
|
253
|
+
|
|
254
|
+
const names = indexes.map((i) => i.config.name);
|
|
255
|
+
expect(names).toContain("tasks_tenant_id_idx");
|
|
256
|
+
expect(names).toContain("tasks_assignee_id_idx");
|
|
257
|
+
expect(names).toContain("tasks_project_id_idx");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("hasMany / manyToMany relations do NOT produce indexes on this table (their FK lives on the other side)", () => {
|
|
261
|
+
const entity = createEntity({
|
|
262
|
+
table: "teams",
|
|
263
|
+
fields: { name: createTextField() },
|
|
264
|
+
});
|
|
265
|
+
const relations: EntityRelations = {
|
|
266
|
+
members: { type: "hasMany", target: "user", foreignKey: "teamId" },
|
|
267
|
+
tags: {
|
|
268
|
+
type: "manyToMany",
|
|
269
|
+
target: "tag",
|
|
270
|
+
through: { table: "team_tags", sourceKey: "teamId", targetKey: "tagId" },
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const table = buildDrizzleTable("team", entity, { relations });
|
|
274
|
+
const { indexes } = getTableConfig(table);
|
|
275
|
+
|
|
276
|
+
// Only the tenant index — hasMany FK lives on the "user" table; the join
|
|
277
|
+
// table for manyToMany isn't owned by this entity either.
|
|
278
|
+
expect(indexes).toHaveLength(1);
|
|
279
|
+
expect(indexes[0]?.config.name).toBe("teams_tenant_id_idx");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("relation and file field on the same column deduplicate to one index", () => {
|
|
283
|
+
const entity = createEntity({
|
|
284
|
+
table: "photos",
|
|
285
|
+
fields: {
|
|
286
|
+
title: createTextField(),
|
|
287
|
+
ownerId: createImageField(), // contrived: name collides with an FK relation below
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
const relations: EntityRelations = {
|
|
291
|
+
owner: { type: "belongsTo", target: "user", foreignKey: "ownerId" },
|
|
292
|
+
};
|
|
293
|
+
const table = buildDrizzleTable("photo", entity, { relations });
|
|
294
|
+
const { indexes } = getTableConfig(table);
|
|
295
|
+
|
|
296
|
+
const names = indexes.map((i) => i.config.name);
|
|
297
|
+
// Exactly one owner_id index, not two
|
|
298
|
+
expect(names.filter((n) => n === "photos_owner_id_idx")).toHaveLength(1);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// --- toTableName ---
|
|
303
|
+
|
|
304
|
+
describe("toTableName", () => {
|
|
305
|
+
test.each([
|
|
306
|
+
["task", "read_tasks"],
|
|
307
|
+
["user", "read_users"],
|
|
308
|
+
["tenant", "read_tenants"],
|
|
309
|
+
])("simple plural: %s → %s", (input, expected) => {
|
|
310
|
+
expect(toTableName(input)).toBe(expected);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test.each([
|
|
314
|
+
["category", "read_categories"],
|
|
315
|
+
["entity", "read_entities"],
|
|
316
|
+
["policy", "read_policies"],
|
|
317
|
+
])("y → ies: %s → %s", (input, expected) => {
|
|
318
|
+
expect(toTableName(input)).toBe(expected);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test.each([
|
|
322
|
+
["key", "read_keys"],
|
|
323
|
+
["survey", "read_surveys"],
|
|
324
|
+
["day", "read_days"],
|
|
325
|
+
])("vowel+y stays: %s → %s", (input, expected) => {
|
|
326
|
+
expect(toTableName(input)).toBe(expected);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test.each([
|
|
330
|
+
["status", "read_statuses"],
|
|
331
|
+
["address", "read_addresses"],
|
|
332
|
+
["match", "read_matches"],
|
|
333
|
+
["tax", "read_taxes"],
|
|
334
|
+
["wish", "read_wishes"],
|
|
335
|
+
])("sibilant → es: %s → %s", (input, expected) => {
|
|
336
|
+
expect(toTableName(input)).toBe(expected);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test.each([
|
|
340
|
+
["memberTask", "read_member_tasks"],
|
|
341
|
+
["userProfile", "read_user_profiles"],
|
|
342
|
+
["orderItem", "read_order_items"],
|
|
343
|
+
])("camelCase → snake_case + plural: %s → %s", (input, expected) => {
|
|
344
|
+
expect(toTableName(input)).toBe(expected);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test.each([
|
|
348
|
+
["tenant-membership", "read_tenant_memberships"],
|
|
349
|
+
["user-profile-address", "read_user_profile_addresses"],
|
|
350
|
+
["invoice-issuer", "read_invoice_issuers"],
|
|
351
|
+
])("kebab-case → snake_case + plural: %s → %s", (input, expected) => {
|
|
352
|
+
expect(toTableName(input)).toBe(expected);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// --- Sorting in CursorQueryOptions ---
|
|
357
|
+
|
|
358
|
+
describe("sorting", () => {
|
|
359
|
+
test("CursorQueryOptions accepts sort and sortDirection", () => {
|
|
360
|
+
// Type-level test: this should compile
|
|
361
|
+
const opts: import("../cursor").CursorQueryOptions = {
|
|
362
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
363
|
+
sort: "lastName",
|
|
364
|
+
sortDirection: "asc",
|
|
365
|
+
};
|
|
366
|
+
expect(opts.sort).toBe("lastName");
|
|
367
|
+
expect(opts.sortDirection).toBe("asc");
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// instant() customType ist der Backing-Type für sowohl `type: "timestamp"`
|
|
2
|
+
// als auch (heute aliased) `type: "date"`. Caller-Code schickt aber zwei
|
|
3
|
+
// verschiedene String-Formate: zod-validate für date akzeptiert nur
|
|
4
|
+
// YYYY-MM-DD, zod-validate für timestamp akzeptiert ISO-datetime. toDriver
|
|
5
|
+
// muss BEIDE Formate coercen können — der Mismatch hat einen 500
|
|
6
|
+
// internal_error in samples/apps/showcase produziert (Showcase-seed
|
|
7
|
+
// schickte YYYY-MM-DD via item:create → dialect.toDriver → Temporal.Instant
|
|
8
|
+
// wirft "Cannot parse: 2026-04-10").
|
|
9
|
+
//
|
|
10
|
+
// Tests pinnen alle drei Pfade (string-iso, date-only-string, instant) +
|
|
11
|
+
// die invalid-Probe (echte Garbage muss weiterhin throwen — kein silent
|
|
12
|
+
// swallowing).
|
|
13
|
+
|
|
14
|
+
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
15
|
+
|
|
16
|
+
await ensureTemporalPolyfill();
|
|
17
|
+
|
|
18
|
+
import { describe, expect, test } from "vitest";
|
|
19
|
+
import { instantToDriver as toDriver } from "../dialect";
|
|
20
|
+
|
|
21
|
+
describe("instant() customType — toDriver", () => {
|
|
22
|
+
test("ISO-datetime mit Z: durchgereicht", () => {
|
|
23
|
+
expect(toDriver("2026-04-10T13:45:00Z")).toBe("2026-04-10T13:45:00Z");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("ISO-datetime mit Offset: normalisiert auf UTC-Z", () => {
|
|
27
|
+
// Temporal.Instant.from normalisiert +02:00 → Z mit korrekter Zeit.
|
|
28
|
+
expect(toDriver("2026-04-10T13:45:00+02:00")).toBe("2026-04-10T11:45:00Z");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("YYYY-MM-DD: coerced auf start-of-day UTC", () => {
|
|
32
|
+
// Forgiving overload für type:"date" — Zod-Validation lässt nur
|
|
33
|
+
// YYYY-MM-DD durch, dialect normalisiert auf instant.
|
|
34
|
+
expect(toDriver("2026-04-10")).toBe("2026-04-10T00:00:00Z");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("Temporal.Instant: durchgereicht über .toString()", () => {
|
|
38
|
+
expect(toDriver(Temporal.Instant.from("2026-04-10T13:45:00Z"))).toBe("2026-04-10T13:45:00Z");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("Garbage-String: wirft RangeError (kein silent swallow)", () => {
|
|
42
|
+
expect(() => toDriver("not-a-date")).toThrow(/Cannot parse/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("Date-only mit Trailing-Whitespace: kein Match (strict regex)", () => {
|
|
46
|
+
// Strict damit "2026-04-10 extra" nicht silently zu start-of-day
|
|
47
|
+
// wird — das ist garantiert ein Caller-Bug, nicht "nett gemeint".
|
|
48
|
+
expect(() => toDriver("2026-04-10 ")).toThrow(/Cannot parse/);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2
|
+
import postgres from "postgres";
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
4
|
+
import { createBooleanField, createEntity, createTextField } from "../../engine";
|
|
5
|
+
import { createEntityTable, testTenantId } from "../../stack";
|
|
6
|
+
import { applyCursorQuery, encodeCursor } from "../cursor";
|
|
7
|
+
import { buildDrizzleTable } from "../table-builder";
|
|
8
|
+
|
|
9
|
+
function requireEnv(name: string): string {
|
|
10
|
+
const value = process.env[name];
|
|
11
|
+
if (!value) throw new Error(`Missing required env var: ${name}`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TEST_DB_URL = requireEnv("TEST_DATABASE_URL");
|
|
16
|
+
|
|
17
|
+
type Row = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
const entity = createEntity({
|
|
20
|
+
table: "test_users",
|
|
21
|
+
// cursor-pagination uses gt(id, cursor) which relies on ordered integer ids.
|
|
22
|
+
// This test exercises that classic serial-PK path deliberately.
|
|
23
|
+
idType: "serial",
|
|
24
|
+
fields: {
|
|
25
|
+
email: createTextField({ required: true, searchable: true }),
|
|
26
|
+
firstName: createTextField({ searchable: true }),
|
|
27
|
+
isEnabled: createBooleanField({ default: true }),
|
|
28
|
+
},
|
|
29
|
+
softDelete: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const table = buildDrizzleTable("testUser", entity);
|
|
33
|
+
|
|
34
|
+
let client: ReturnType<typeof postgres>;
|
|
35
|
+
let db: ReturnType<typeof drizzle>;
|
|
36
|
+
|
|
37
|
+
beforeAll(async () => {
|
|
38
|
+
const adminClient = postgres(TEST_DB_URL.replace(/\/[^/]+$/, "/postgres"), {
|
|
39
|
+
onnotice: () => {},
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
await adminClient`DROP DATABASE IF EXISTS kumiko_test_step7`;
|
|
43
|
+
await adminClient`CREATE DATABASE kumiko_test_step7`;
|
|
44
|
+
} finally {
|
|
45
|
+
await adminClient.end();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const testUrl = TEST_DB_URL.replace(/\/[^/]+$/, "/kumiko_test_step7");
|
|
49
|
+
client = postgres(testUrl);
|
|
50
|
+
db = drizzle(client);
|
|
51
|
+
|
|
52
|
+
await createEntityTable(db, entity);
|
|
53
|
+
|
|
54
|
+
const rows = [
|
|
55
|
+
{
|
|
56
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
57
|
+
email: "admin@test.de",
|
|
58
|
+
firstName: "Admin",
|
|
59
|
+
},
|
|
60
|
+
{ tenantId: "00000000-0000-4000-8000-000000000001", email: "marc@test.de", firstName: "Marc" },
|
|
61
|
+
{ tenantId: "00000000-0000-4000-8000-000000000001", email: "anna@test.de", firstName: "Anna" },
|
|
62
|
+
{
|
|
63
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
64
|
+
email: "deleted@test.de",
|
|
65
|
+
firstName: "Deleted",
|
|
66
|
+
isDeleted: true,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
tenantId: "00000000-0000-4000-8000-000000000002",
|
|
70
|
+
email: "other@test.de",
|
|
71
|
+
firstName: "Other",
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
for (const row of rows) {
|
|
76
|
+
await db.insert(table).values({
|
|
77
|
+
tenantId: row.tenantId,
|
|
78
|
+
email: row.email,
|
|
79
|
+
firstName: row.firstName,
|
|
80
|
+
isEnabled: true,
|
|
81
|
+
isDeleted: row.isDeleted ?? false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterAll(async () => {
|
|
87
|
+
await client.end();
|
|
88
|
+
const adminClient = postgres(TEST_DB_URL.replace(/\/[^/]+$/, "/postgres"), {
|
|
89
|
+
onnotice: () => {},
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
await adminClient`DROP DATABASE IF EXISTS kumiko_test_step7`;
|
|
93
|
+
} finally {
|
|
94
|
+
await adminClient.end();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
async function query(options: Parameters<typeof applyCursorQuery>[2]): Promise<Row[]> {
|
|
99
|
+
return applyCursorQuery(db.select().from(table).$dynamic(), table, options);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Tests ---
|
|
103
|
+
|
|
104
|
+
describe("tenant isolation", () => {
|
|
105
|
+
test("only returns rows for specified tenant", async () => {
|
|
106
|
+
const rows = await query({ tenantId: testTenantId(1) });
|
|
107
|
+
expect(rows.every((r) => r["tenantId"] === testTenantId(1))).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("tenant 2 only sees own data", async () => {
|
|
111
|
+
const rows = await query({ tenantId: "00000000-0000-4000-8000-000000000002" });
|
|
112
|
+
expect(rows).toHaveLength(1);
|
|
113
|
+
expect(rows[0]?.["email"]).toBe("other@test.de");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("soft delete filtering", () => {
|
|
118
|
+
test("excludes soft-deleted rows", async () => {
|
|
119
|
+
const rows = await query({ tenantId: "00000000-0000-4000-8000-000000000001" });
|
|
120
|
+
expect(rows.find((r) => r["email"] === "deleted@test.de")).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("cursor pagination", () => {
|
|
125
|
+
test("limits results", async () => {
|
|
126
|
+
const rows = await query({ tenantId: "00000000-0000-4000-8000-000000000001", limit: 2 });
|
|
127
|
+
expect(rows).toHaveLength(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("cursor skips past previous results", async () => {
|
|
131
|
+
const page1 = await query({ tenantId: "00000000-0000-4000-8000-000000000001", limit: 2 });
|
|
132
|
+
expect(page1).toHaveLength(2);
|
|
133
|
+
|
|
134
|
+
const lastId = page1[page1.length - 1]?.["id"] as number;
|
|
135
|
+
const page2 = await query({
|
|
136
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
137
|
+
limit: 2,
|
|
138
|
+
cursor: encodeCursor(lastId),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const page1Ids = page1.map((r) => r["id"]);
|
|
142
|
+
const page2Ids = page2.map((r) => r["id"]);
|
|
143
|
+
expect(page1Ids.some((id) => page2Ids.includes(id))).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("filterIds (search results from SearchAdapter)", () => {
|
|
148
|
+
test("filters by ID list from search adapter", async () => {
|
|
149
|
+
// SearchAdapter returns IDs, cursor query filters by them
|
|
150
|
+
const rows = await query({
|
|
151
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
152
|
+
filterIds: [1, 2],
|
|
153
|
+
});
|
|
154
|
+
expect(rows).toHaveLength(2);
|
|
155
|
+
expect(rows.every((r) => [1, 2].includes(r["id"] as number))).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("empty filterIds returns nothing", async () => {
|
|
159
|
+
const rows = await query({ tenantId: "00000000-0000-4000-8000-000000000001", filterIds: [] });
|
|
160
|
+
expect(rows).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("sorting", () => {
|
|
165
|
+
test("sorts by column ASC", async () => {
|
|
166
|
+
const rows = await query({
|
|
167
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
168
|
+
sort: "firstName",
|
|
169
|
+
sortDirection: "asc",
|
|
170
|
+
});
|
|
171
|
+
const names = rows.map((r) => r["firstName"]);
|
|
172
|
+
const sorted = [...names].sort();
|
|
173
|
+
expect(names).toEqual(sorted);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("sorts by column DESC", async () => {
|
|
177
|
+
const rows = await query({
|
|
178
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
179
|
+
sort: "firstName",
|
|
180
|
+
sortDirection: "desc",
|
|
181
|
+
});
|
|
182
|
+
const names = rows.map((r) => r["firstName"]);
|
|
183
|
+
const sorted = [...names].sort().reverse();
|
|
184
|
+
expect(names).toEqual(sorted);
|
|
185
|
+
});
|
|
186
|
+
});
|