@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,54 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { redisClientOptionsFromEnv } from "../index";
|
|
3
|
+
|
|
4
|
+
describe("redisClientOptionsFromEnv", () => {
|
|
5
|
+
test("empty env → empty options", () => {
|
|
6
|
+
expect(redisClientOptionsFromEnv({})).toEqual({});
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("reads all three supported keys", () => {
|
|
10
|
+
const opts = redisClientOptionsFromEnv({
|
|
11
|
+
REDIS_CONNECT_TIMEOUT_MS: "3000",
|
|
12
|
+
REDIS_COMMAND_TIMEOUT_MS: "5000",
|
|
13
|
+
REDIS_MAX_RETRIES_PER_REQUEST: "2",
|
|
14
|
+
});
|
|
15
|
+
expect(opts).toEqual({
|
|
16
|
+
connectTimeoutMs: 3000,
|
|
17
|
+
commandTimeoutMs: 5000,
|
|
18
|
+
maxRetriesPerRequest: 2,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("empty string is treated as unset", () => {
|
|
23
|
+
const opts = redisClientOptionsFromEnv({
|
|
24
|
+
REDIS_CONNECT_TIMEOUT_MS: "",
|
|
25
|
+
REDIS_COMMAND_TIMEOUT_MS: "7500",
|
|
26
|
+
});
|
|
27
|
+
expect(opts).toEqual({ commandTimeoutMs: 7500 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("zero is allowed (e.g. 0 retries = fail-fast)", () => {
|
|
31
|
+
const opts = redisClientOptionsFromEnv({
|
|
32
|
+
REDIS_MAX_RETRIES_PER_REQUEST: "0",
|
|
33
|
+
});
|
|
34
|
+
expect(opts).toEqual({ maxRetriesPerRequest: 0 });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("negative number → throws", () => {
|
|
38
|
+
expect(() => redisClientOptionsFromEnv({ REDIS_CONNECT_TIMEOUT_MS: "-1" })).toThrow(
|
|
39
|
+
/REDIS_CONNECT_TIMEOUT_MS="-1".*non-negative/i,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("non-numeric → throws", () => {
|
|
44
|
+
expect(() => redisClientOptionsFromEnv({ REDIS_COMMAND_TIMEOUT_MS: "ten" })).toThrow(
|
|
45
|
+
/REDIS_COMMAND_TIMEOUT_MS="ten"/,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("decimal → throws (ioredis expects integer ms)", () => {
|
|
50
|
+
expect(() => redisClientOptionsFromEnv({ REDIS_CONNECT_TIMEOUT_MS: "250.5" })).toThrow(
|
|
51
|
+
/REDIS_CONNECT_TIMEOUT_MS="250.5".*integer/i,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Thin factory for ioredis clients with Prod-sensible defaults.
|
|
2
|
+
// ioredis itself doesn't enforce a command-timeout — an unreachable Redis
|
|
3
|
+
// will silently hang a request forever unless the caller sets
|
|
4
|
+
// `commandTimeout`. Same story for connectTimeout: the default is 10s,
|
|
5
|
+
// which is too long for a /health/ready probe with a 2s budget.
|
|
6
|
+
//
|
|
7
|
+
// Apps are expected to build their Redis clients via this helper so
|
|
8
|
+
// production defaults stay consistent and env-var wiring lives in one
|
|
9
|
+
// place. Tests / one-off scripts can still `new Redis()` directly.
|
|
10
|
+
|
|
11
|
+
import IoRedis, { type Redis, type RedisOptions } from "ioredis";
|
|
12
|
+
import { readPositiveIntEnv } from "../utils/env-parse";
|
|
13
|
+
|
|
14
|
+
// Connection-tuning options. The fields mirror the most consequential
|
|
15
|
+
// ioredis settings; anything else can be passed through via `extra` to
|
|
16
|
+
// keep the helper from re-declaring the full ioredis surface.
|
|
17
|
+
export type RedisClientOptions = {
|
|
18
|
+
// Milliseconds to wait when opening the TCP connection. Default 5s —
|
|
19
|
+
// long enough for a cold cache/standby to handshake, short enough that
|
|
20
|
+
// /health/ready can surface "Redis is gone" inside its 2s-ish probe
|
|
21
|
+
// window (probe runs in parallel, so 5s here + 2s timeout on the probe
|
|
22
|
+
// side gives the probe a real answer instead of a hang).
|
|
23
|
+
readonly connectTimeoutMs?: number;
|
|
24
|
+
// Milliseconds to wait for a single command to round-trip. Default 10s.
|
|
25
|
+
// Without this, ioredis waits forever on a stalled connection — a
|
|
26
|
+
// single bad network partition quietly exhausts the whole command queue.
|
|
27
|
+
readonly commandTimeoutMs?: number;
|
|
28
|
+
// Retries per request before bubbling up as an error. Default 3 — enough
|
|
29
|
+
// to ride out a transient blip but not so many that a dead Redis keeps
|
|
30
|
+
// requests stuck waiting for retries.
|
|
31
|
+
readonly maxRetriesPerRequest?: number;
|
|
32
|
+
// Escape hatch for any ioredis option the helper doesn't expose
|
|
33
|
+
// explicitly (TLS config, Sentinel, ReadyCheck toggles, …).
|
|
34
|
+
readonly extra?: Readonly<
|
|
35
|
+
Omit<RedisOptions, "connectTimeout" | "commandTimeout" | "maxRetriesPerRequest">
|
|
36
|
+
>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5_000;
|
|
40
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 10_000;
|
|
41
|
+
const DEFAULT_MAX_RETRIES_PER_REQUEST = 3;
|
|
42
|
+
|
|
43
|
+
export function createRedisClient(url: string, options: RedisClientOptions = {}): Redis {
|
|
44
|
+
const connectTimeout = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
45
|
+
const commandTimeout = options.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
46
|
+
const maxRetriesPerRequest = options.maxRetriesPerRequest ?? DEFAULT_MAX_RETRIES_PER_REQUEST;
|
|
47
|
+
|
|
48
|
+
return new IoRedis(url, {
|
|
49
|
+
...options.extra,
|
|
50
|
+
connectTimeout,
|
|
51
|
+
commandTimeout,
|
|
52
|
+
maxRetriesPerRequest,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Env-var reader — mirrors dbConnectionOptionsFromEnv's contract. Strict:
|
|
57
|
+
// malformed values throw at boot rather than silently falling back to
|
|
58
|
+
// defaults.
|
|
59
|
+
export function redisClientOptionsFromEnv(
|
|
60
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
61
|
+
): RedisClientOptions {
|
|
62
|
+
const opts: RedisClientOptions & {
|
|
63
|
+
connectTimeoutMs?: number;
|
|
64
|
+
commandTimeoutMs?: number;
|
|
65
|
+
maxRetriesPerRequest?: number;
|
|
66
|
+
} = {};
|
|
67
|
+
const connect = readPositiveIntEnv(env, "REDIS_CONNECT_TIMEOUT_MS");
|
|
68
|
+
const command = readPositiveIntEnv(env, "REDIS_COMMAND_TIMEOUT_MS");
|
|
69
|
+
const retries = readPositiveIntEnv(env, "REDIS_MAX_RETRIES_PER_REQUEST");
|
|
70
|
+
if (connect !== undefined) opts.connectTimeoutMs = connect;
|
|
71
|
+
if (command !== undefined) opts.commandTimeoutMs = command;
|
|
72
|
+
if (retries !== undefined) opts.maxRetriesPerRequest = retries;
|
|
73
|
+
return opts;
|
|
74
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { generateId as uuid } from "@cosmicdrift/kumiko-framework/utils";
|
|
3
|
+
import { Meilisearch } from "meilisearch";
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
5
|
+
import { createMeilisearchAdapter } from "../meilisearch-adapter";
|
|
6
|
+
import type { SearchAdapter } from "../types";
|
|
7
|
+
|
|
8
|
+
const MEILI_URL = process.env["MEILI_URL"] ?? "http://localhost:17700";
|
|
9
|
+
const MEILI_KEY = process.env["MEILI_MASTER_KEY"] ?? "kumiko-dev-key";
|
|
10
|
+
|
|
11
|
+
// Use a fake tenantId to get a unique index name
|
|
12
|
+
const TENANT = uuid();
|
|
13
|
+
|
|
14
|
+
let adapter: SearchAdapter;
|
|
15
|
+
let client: Meilisearch;
|
|
16
|
+
let indexPrefix: string;
|
|
17
|
+
|
|
18
|
+
// Mirrors meilisearch-adapter.ts's tenantIndex() — used by tests that need
|
|
19
|
+
// to talk to Meilisearch directly (e.g. stats before/after a no-op).
|
|
20
|
+
const tenantIndex = (prefix: string, tenantId: TenantId): string => `${prefix}t${tenantId}`;
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
client = new Meilisearch({ host: MEILI_URL, apiKey: MEILI_KEY });
|
|
24
|
+
indexPrefix = `test_${uuid().slice(-6)}_`;
|
|
25
|
+
adapter = createMeilisearchAdapter({
|
|
26
|
+
url: MEILI_URL,
|
|
27
|
+
apiKey: MEILI_KEY,
|
|
28
|
+
indexPrefix,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await adapter.configure(TENANT, {
|
|
32
|
+
searchableFields: ["email", "firstName", "lastName", "notes", "_roles"],
|
|
33
|
+
rankingFields: ["email", "firstName", "lastName", "notes", "_roles"],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Seed data with different entity types and weights
|
|
37
|
+
await adapter.index(TENANT, {
|
|
38
|
+
entityType: "user",
|
|
39
|
+
entityId: 1,
|
|
40
|
+
weight: 10,
|
|
41
|
+
fields: {
|
|
42
|
+
email: "marc.weber@company.de",
|
|
43
|
+
firstName: "Marc",
|
|
44
|
+
lastName: "Weber",
|
|
45
|
+
notes: "Senior developer",
|
|
46
|
+
_roles: "Admin, Developer",
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
await adapter.index(TENANT, {
|
|
50
|
+
entityType: "user",
|
|
51
|
+
entityId: 2,
|
|
52
|
+
weight: 10,
|
|
53
|
+
fields: {
|
|
54
|
+
email: "anna.schmidt@company.de",
|
|
55
|
+
firstName: "Anna",
|
|
56
|
+
lastName: "Schmidt",
|
|
57
|
+
notes: "Project manager",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
await adapter.index(TENANT, {
|
|
61
|
+
entityType: "user",
|
|
62
|
+
entityId: 3,
|
|
63
|
+
weight: 10,
|
|
64
|
+
fields: {
|
|
65
|
+
email: "admin@company.de",
|
|
66
|
+
firstName: "Admin",
|
|
67
|
+
lastName: "User",
|
|
68
|
+
notes: "System administrator",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
await adapter.index(TENANT, {
|
|
72
|
+
entityType: "role",
|
|
73
|
+
entityId: 1,
|
|
74
|
+
weight: 1,
|
|
75
|
+
fields: { firstName: "Admin" },
|
|
76
|
+
});
|
|
77
|
+
await adapter.index(TENANT, {
|
|
78
|
+
entityType: "role",
|
|
79
|
+
entityId: 2,
|
|
80
|
+
weight: 1,
|
|
81
|
+
fields: { firstName: "Developer" },
|
|
82
|
+
});
|
|
83
|
+
await adapter.index(TENANT, {
|
|
84
|
+
entityType: "department",
|
|
85
|
+
entityId: 1,
|
|
86
|
+
weight: 5,
|
|
87
|
+
fields: { firstName: "Engineering" },
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
afterAll(async () => {
|
|
92
|
+
// Clean up all test indices
|
|
93
|
+
const indices = await client.getIndexes();
|
|
94
|
+
for (const idx of indices.results) {
|
|
95
|
+
if (idx.uid.startsWith("test_")) {
|
|
96
|
+
try {
|
|
97
|
+
await client.index(idx.uid).delete().waitTask();
|
|
98
|
+
} catch {
|
|
99
|
+
/* ok */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// --- Basic search ---
|
|
106
|
+
|
|
107
|
+
describe("basic search", () => {
|
|
108
|
+
test("finds user by name", async () => {
|
|
109
|
+
const results = await adapter.search(TENANT, "anna");
|
|
110
|
+
expect(results.some((r) => r.entityId === 2 && r.entityType === "user")).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("returns empty for no match", async () => {
|
|
114
|
+
const results = await adapter.search(TENANT, "zzzznonexistent99999");
|
|
115
|
+
expect(results).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// --- Partial matching ---
|
|
120
|
+
|
|
121
|
+
describe("partial matching", () => {
|
|
122
|
+
test("finds by prefix", async () => {
|
|
123
|
+
const results = await adapter.search(TENANT, "mar");
|
|
124
|
+
expect(results.some((r) => r.entityId === 1 && r.entityType === "user")).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// --- Typo tolerance ---
|
|
129
|
+
|
|
130
|
+
describe("typo tolerance", () => {
|
|
131
|
+
test("finds despite typos", async () => {
|
|
132
|
+
const results = await adapter.search(TENANT, "schmit");
|
|
133
|
+
expect(results.some((r) => r.entityId === 2)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// --- Filter by entity type (list search) ---
|
|
138
|
+
|
|
139
|
+
describe("list search (filterType)", () => {
|
|
140
|
+
test("only returns specified entity type", async () => {
|
|
141
|
+
const results = await adapter.search(TENANT, "admin", { filterType: "user" });
|
|
142
|
+
expect(results.every((r) => r.entityType === "user")).toBe(true);
|
|
143
|
+
expect(results.length).toBeGreaterThan(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("role filter returns only roles", async () => {
|
|
147
|
+
const results = await adapter.search(TENANT, "admin", { filterType: "role" });
|
|
148
|
+
expect(results.every((r) => r.entityType === "role")).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// --- Global search with weight ---
|
|
153
|
+
|
|
154
|
+
describe("global search with searchWeight", () => {
|
|
155
|
+
test("user (weight 10) ranks before role (weight 1) for same query", async () => {
|
|
156
|
+
const results = await adapter.search(TENANT, "admin");
|
|
157
|
+
const userIdx = results.findIndex((r) => r.entityType === "user");
|
|
158
|
+
const roleIdx = results.findIndex((r) => r.entityType === "role");
|
|
159
|
+
// User should appear before Role due to _weight:desc sort
|
|
160
|
+
if (userIdx >= 0 && roleIdx >= 0) {
|
|
161
|
+
expect(userIdx).toBeLessThan(roleIdx);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// --- Resolved relation data ---
|
|
167
|
+
|
|
168
|
+
describe("relation data in search", () => {
|
|
169
|
+
test("finds user by role name in _roles field", async () => {
|
|
170
|
+
const results = await adapter.search(TENANT, "developer", { filterType: "user" });
|
|
171
|
+
expect(results.some((r) => r.entityId === 1)).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// --- Remove ---
|
|
176
|
+
|
|
177
|
+
describe("remove", () => {
|
|
178
|
+
test("removed document not found", async () => {
|
|
179
|
+
// Create temp doc
|
|
180
|
+
await adapter.index(TENANT, {
|
|
181
|
+
entityType: "temp",
|
|
182
|
+
entityId: 999,
|
|
183
|
+
weight: 1,
|
|
184
|
+
fields: { firstName: "DeleteMe" },
|
|
185
|
+
});
|
|
186
|
+
let results = await adapter.search(TENANT, "deleteme");
|
|
187
|
+
expect(results.some((r) => r.entityId === 999)).toBe(true);
|
|
188
|
+
|
|
189
|
+
await adapter.remove(TENANT, "temp", 999);
|
|
190
|
+
results = await adapter.search(TENANT, "deleteme");
|
|
191
|
+
expect(results.some((r) => r.entityId === 999)).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// --- Batch variants ---
|
|
196
|
+
|
|
197
|
+
describe("indexBatch / removeBatch", () => {
|
|
198
|
+
test("indexBatch indexes multiple docs in a single task", async () => {
|
|
199
|
+
const docs = Array.from({ length: 5 }, (_, i) => ({
|
|
200
|
+
entityType: "batch" as const,
|
|
201
|
+
entityId: 1000 + i,
|
|
202
|
+
weight: 1,
|
|
203
|
+
fields: { firstName: `Bulk${i}`, notes: "batchtoken" },
|
|
204
|
+
}));
|
|
205
|
+
await adapter.indexBatch?.(TENANT, docs);
|
|
206
|
+
|
|
207
|
+
const hits = await adapter.search(TENANT, "batchtoken", { limit: 20, filterType: "batch" });
|
|
208
|
+
expect(hits.length).toBe(5);
|
|
209
|
+
const ids = hits.map((h) => h.entityId).sort();
|
|
210
|
+
expect(ids).toEqual([1000, 1001, 1002, 1003, 1004]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("removeBatch removes multiple docs in a single task", async () => {
|
|
214
|
+
await adapter.removeBatch?.(
|
|
215
|
+
TENANT,
|
|
216
|
+
[1000, 1001, 1002, 1003, 1004].map((id) => ({ entityType: "batch", entityId: id })),
|
|
217
|
+
);
|
|
218
|
+
const hits = await adapter.search(TENANT, "batchtoken", { limit: 20, filterType: "batch" });
|
|
219
|
+
expect(hits.length).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("indexBatch no-ops on empty array — no Meilisearch task created", async () => {
|
|
223
|
+
// We verify the "no-op" contract by peeking at Meilisearch's own
|
|
224
|
+
// IndexStats.numberOfDocuments + isIndexing directly. If the adapter had
|
|
225
|
+
// accidentally sent an addDocuments request (even with an empty body),
|
|
226
|
+
// isIndexing would flip to true or a task would land in the queue.
|
|
227
|
+
// numberOfDocuments must also stay unchanged — the empty batch must not
|
|
228
|
+
// replace, delete, or otherwise touch existing docs.
|
|
229
|
+
const index = client.index(tenantIndex(indexPrefix, TENANT));
|
|
230
|
+
const before = await index.getStats();
|
|
231
|
+
await expect(adapter.indexBatch?.(TENANT, [])).resolves.toBeUndefined();
|
|
232
|
+
const after = await index.getStats();
|
|
233
|
+
expect(after.numberOfDocuments).toBe(before.numberOfDocuments);
|
|
234
|
+
expect(after.isIndexing).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { createInMemorySearchAdapter } from "../in-memory-adapter";
|
|
3
|
+
import type { SearchAdapter } from "../types";
|
|
4
|
+
|
|
5
|
+
const TENANT = "00000000-0000-4000-8000-000000000001";
|
|
6
|
+
let adapter: SearchAdapter;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
adapter = createInMemorySearchAdapter();
|
|
10
|
+
await adapter.configure(TENANT, {
|
|
11
|
+
searchableFields: ["email", "firstName", "lastName", "_roles", "_department"],
|
|
12
|
+
rankingFields: ["email", "firstName", "lastName", "_roles", "_department"],
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// --- Basic search ---
|
|
17
|
+
|
|
18
|
+
describe("basic search", () => {
|
|
19
|
+
test("finds by field value", async () => {
|
|
20
|
+
await adapter.index(TENANT, {
|
|
21
|
+
entityType: "user",
|
|
22
|
+
entityId: 1,
|
|
23
|
+
weight: 10,
|
|
24
|
+
fields: { email: "marc@test.de", firstName: "Marc" },
|
|
25
|
+
});
|
|
26
|
+
await adapter.index(TENANT, {
|
|
27
|
+
entityType: "user",
|
|
28
|
+
entityId: 2,
|
|
29
|
+
weight: 10,
|
|
30
|
+
fields: { email: "anna@test.de", firstName: "Anna" },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const results = await adapter.search(TENANT, "marc");
|
|
34
|
+
expect(results).toHaveLength(1);
|
|
35
|
+
expect(results[0]?.entityId).toBe(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("search is case-insensitive", async () => {
|
|
39
|
+
await adapter.index(TENANT, {
|
|
40
|
+
entityType: "user",
|
|
41
|
+
entityId: 1,
|
|
42
|
+
weight: 1,
|
|
43
|
+
fields: { firstName: "Marc" },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(await adapter.search(TENANT, "MARC")).toHaveLength(1);
|
|
47
|
+
expect(await adapter.search(TENANT, "marc")).toHaveLength(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("returns empty for no matches", async () => {
|
|
51
|
+
await adapter.index(TENANT, {
|
|
52
|
+
entityType: "user",
|
|
53
|
+
entityId: 1,
|
|
54
|
+
weight: 1,
|
|
55
|
+
fields: { firstName: "Marc" },
|
|
56
|
+
});
|
|
57
|
+
expect(await adapter.search(TENANT, "nonexistent")).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// --- Partial matching ---
|
|
62
|
+
|
|
63
|
+
describe("partial matching", () => {
|
|
64
|
+
test("finds by prefix", async () => {
|
|
65
|
+
await adapter.index(TENANT, {
|
|
66
|
+
entityType: "user",
|
|
67
|
+
entityId: 1,
|
|
68
|
+
weight: 1,
|
|
69
|
+
fields: { firstName: "Alexander" },
|
|
70
|
+
});
|
|
71
|
+
expect(await adapter.search(TENANT, "alex")).toHaveLength(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("finds by substring in email", async () => {
|
|
75
|
+
await adapter.index(TENANT, {
|
|
76
|
+
entityType: "user",
|
|
77
|
+
entityId: 1,
|
|
78
|
+
weight: 1,
|
|
79
|
+
fields: { email: "marc.weber@company.de" },
|
|
80
|
+
});
|
|
81
|
+
expect(await adapter.search(TENANT, "weber")).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// --- Tenant isolation ---
|
|
86
|
+
|
|
87
|
+
describe("tenant isolation", () => {
|
|
88
|
+
test("tenant 1 cannot see tenant 2 data", async () => {
|
|
89
|
+
await adapter.configure("00000000-0000-4000-8000-000000000002", {
|
|
90
|
+
searchableFields: ["firstName"],
|
|
91
|
+
});
|
|
92
|
+
await adapter.index("00000000-0000-4000-8000-000000000001", {
|
|
93
|
+
entityType: "user",
|
|
94
|
+
entityId: 1,
|
|
95
|
+
weight: 1,
|
|
96
|
+
fields: { firstName: "Marc" },
|
|
97
|
+
});
|
|
98
|
+
await adapter.index("00000000-0000-4000-8000-000000000002", {
|
|
99
|
+
entityType: "user",
|
|
100
|
+
entityId: 2,
|
|
101
|
+
weight: 1,
|
|
102
|
+
fields: { firstName: "Marc" },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const t1 = await adapter.search("00000000-0000-4000-8000-000000000001", "marc");
|
|
106
|
+
const t2 = await adapter.search("00000000-0000-4000-8000-000000000002", "marc");
|
|
107
|
+
|
|
108
|
+
expect(t1).toHaveLength(1);
|
|
109
|
+
expect(t1[0]?.entityId).toBe(1);
|
|
110
|
+
expect(t2).toHaveLength(1);
|
|
111
|
+
expect(t2[0]?.entityId).toBe(2);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// --- Entity type filtering (list search) ---
|
|
116
|
+
|
|
117
|
+
describe("list search (filterType)", () => {
|
|
118
|
+
test("filters by entity type", async () => {
|
|
119
|
+
await adapter.index(TENANT, {
|
|
120
|
+
entityType: "user",
|
|
121
|
+
entityId: 1,
|
|
122
|
+
weight: 10,
|
|
123
|
+
fields: { firstName: "Marc" },
|
|
124
|
+
});
|
|
125
|
+
await adapter.index(TENANT, {
|
|
126
|
+
entityType: "role",
|
|
127
|
+
entityId: 1,
|
|
128
|
+
weight: 1,
|
|
129
|
+
fields: { firstName: "Marc Admin" },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const users = await adapter.search(TENANT, "marc", { filterType: "user" });
|
|
133
|
+
const roles = await adapter.search(TENANT, "marc", { filterType: "role" });
|
|
134
|
+
|
|
135
|
+
expect(users).toHaveLength(1);
|
|
136
|
+
expect(users[0]?.entityType).toBe("user");
|
|
137
|
+
expect(roles).toHaveLength(1);
|
|
138
|
+
expect(roles[0]?.entityType).toBe("role");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// --- Global search (no filter) ---
|
|
143
|
+
|
|
144
|
+
describe("global search (no filterType)", () => {
|
|
145
|
+
test("returns all entity types sorted by weight", async () => {
|
|
146
|
+
await adapter.index(TENANT, {
|
|
147
|
+
entityType: "user",
|
|
148
|
+
entityId: 1,
|
|
149
|
+
weight: 10,
|
|
150
|
+
fields: { firstName: "Admin" },
|
|
151
|
+
});
|
|
152
|
+
await adapter.index(TENANT, {
|
|
153
|
+
entityType: "role",
|
|
154
|
+
entityId: 1,
|
|
155
|
+
weight: 1,
|
|
156
|
+
fields: { firstName: "Admin Role" },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const results = await adapter.search(TENANT, "admin");
|
|
160
|
+
expect(results).toHaveLength(2);
|
|
161
|
+
// User (weight 10) should rank before Role (weight 1)
|
|
162
|
+
expect(results[0]?.entityType).toBe("user");
|
|
163
|
+
expect(results[1]?.entityType).toBe("role");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// --- searchWeight scoring ---
|
|
168
|
+
|
|
169
|
+
describe("searchWeight scoring", () => {
|
|
170
|
+
test("higher weight entity ranks first", async () => {
|
|
171
|
+
await adapter.index(TENANT, {
|
|
172
|
+
entityType: "vehicle",
|
|
173
|
+
entityId: 1,
|
|
174
|
+
weight: 10,
|
|
175
|
+
fields: { firstName: "BMW 320i" },
|
|
176
|
+
});
|
|
177
|
+
await adapter.index(TENANT, {
|
|
178
|
+
entityType: "workshop",
|
|
179
|
+
entityId: 1,
|
|
180
|
+
weight: 5,
|
|
181
|
+
fields: { firstName: "BMW Werkstatt" },
|
|
182
|
+
});
|
|
183
|
+
await adapter.index(TENANT, {
|
|
184
|
+
entityType: "role",
|
|
185
|
+
entityId: 1,
|
|
186
|
+
weight: 1,
|
|
187
|
+
fields: { firstName: "BMW Fleet Manager" },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const results = await adapter.search(TENANT, "bmw");
|
|
191
|
+
expect(results[0]?.entityType).toBe("vehicle");
|
|
192
|
+
expect(results[1]?.entityType).toBe("workshop");
|
|
193
|
+
expect(results[2]?.entityType).toBe("role");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// --- Relation data in search ---
|
|
198
|
+
|
|
199
|
+
describe("resolved relation data", () => {
|
|
200
|
+
test("finds user by role name via _roles field", async () => {
|
|
201
|
+
await adapter.index(TENANT, {
|
|
202
|
+
entityType: "user",
|
|
203
|
+
entityId: 1,
|
|
204
|
+
weight: 10,
|
|
205
|
+
fields: { email: "marc@test.de", firstName: "Marc", _roles: "Admin, Developer" },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const results = await adapter.search(TENANT, "developer");
|
|
209
|
+
expect(results).toHaveLength(1);
|
|
210
|
+
expect(results[0]?.entityId).toBe(1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("finds user by department name via _department field", async () => {
|
|
214
|
+
await adapter.index(TENANT, {
|
|
215
|
+
entityType: "user",
|
|
216
|
+
entityId: 1,
|
|
217
|
+
weight: 10,
|
|
218
|
+
fields: { email: "marc@test.de", _department: "Marketing" },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const results = await adapter.search(TENANT, "marketing");
|
|
222
|
+
expect(results).toHaveLength(1);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// --- Remove ---
|
|
227
|
+
|
|
228
|
+
describe("remove", () => {
|
|
229
|
+
test("removes document from search", async () => {
|
|
230
|
+
await adapter.index(TENANT, {
|
|
231
|
+
entityType: "user",
|
|
232
|
+
entityId: 1,
|
|
233
|
+
weight: 1,
|
|
234
|
+
fields: { firstName: "Marc" },
|
|
235
|
+
});
|
|
236
|
+
await adapter.remove(TENANT, "user", 1);
|
|
237
|
+
expect(await adapter.search(TENANT, "marc")).toEqual([]);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// --- Limit ---
|
|
242
|
+
|
|
243
|
+
describe("limit", () => {
|
|
244
|
+
test("respects limit", async () => {
|
|
245
|
+
for (let i = 1; i <= 10; i++) {
|
|
246
|
+
await adapter.index(TENANT, {
|
|
247
|
+
entityType: "user",
|
|
248
|
+
entityId: i,
|
|
249
|
+
weight: 1,
|
|
250
|
+
fields: { firstName: `User${i}`, lastName: "Same" },
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const results = await adapter.search(TENANT, "same", { limit: 3 });
|
|
254
|
+
expect(results).toHaveLength(3);
|
|
255
|
+
});
|
|
256
|
+
});
|