@cosmicdrift/kumiko-framework 1.0.0 → 2.0.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 +9 -38
- package/package.json +21 -2
- package/src/__tests__/anonymous-access.integration.test.ts +185 -0
- package/src/__tests__/consumer-cli.integration.test.ts +172 -0
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
- package/src/__tests__/schema-cli.integration.test.ts +50 -0
- package/src/__tests__/store-table.integration.test.ts +93 -0
- package/src/api/__tests__/api.test.ts +373 -0
- package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
- package/src/api/__tests__/batch.integration.test.ts +101 -11
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
- package/src/api/__tests__/jwt.test.ts +200 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
- package/src/api/__tests__/pat-scope.test.ts +36 -0
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
- package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
- package/src/api/__tests__/request-id-middleware.test.ts +51 -0
- package/src/api/__tests__/server-boot-guards.test.ts +71 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/__tests__/sse-broker.test.ts +57 -0
- package/src/api/__tests__/sse-route.test.ts +4 -0
- package/src/api/api-constants.ts +11 -0
- package/src/api/auth-middleware.ts +248 -40
- package/src/api/auth-routes.ts +576 -95
- package/src/api/index.ts +13 -4
- package/src/api/jwt.ts +170 -11
- package/src/api/pat-scope.ts +14 -0
- package/src/api/pii-leak-guard.ts +47 -0
- package/src/api/request-context.ts +3 -0
- package/src/api/request-id-middleware.ts +2 -0
- package/src/api/routes.ts +169 -2
- package/src/api/server.ts +112 -16
- package/src/api/sse-broker.ts +39 -0
- package/src/bun-db/__tests__/PATTERN.md +0 -1
- package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/__tests__/write-brand.test.ts +21 -0
- package/src/bun-db/connection.ts +3 -3
- package/src/bun-db/index.ts +2 -0
- package/src/bun-db/query.ts +105 -32
- package/src/consumer-cli.ts +134 -0
- package/src/crypto/__tests__/blind-index.test.ts +130 -0
- package/src/crypto/__tests__/event-pii.test.ts +161 -0
- package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
- package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
- package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
- package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
- package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
- package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
- package/src/crypto/blind-index.ts +118 -0
- package/src/crypto/event-pii.ts +70 -0
- package/src/crypto/in-memory-kms-adapter.ts +50 -0
- package/src/crypto/index.ts +67 -0
- package/src/crypto/kms-adapter.ts +2 -0
- package/src/crypto/pg-kms-adapter.ts +295 -0
- package/src/crypto/pii-field-encryption.ts +248 -0
- package/src/crypto/request-kms-cache.ts +38 -0
- package/src/crypto/subject-resolver.ts +91 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
- package/src/db/__tests__/blind-index.integration.test.ts +248 -0
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- package/src/db/__tests__/collect-table-metas.test.ts +10 -10
- package/src/db/__tests__/config-seed.integration.test.ts +13 -5
- package/src/db/__tests__/dialect-instant.test.ts +1 -4
- package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
- package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
- package/src/db/__tests__/feature-table-sources.test.ts +34 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
- package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
- package/src/db/__tests__/located-timestamp.test.ts +19 -0
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/migrate-runner.test.ts +61 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
- package/src/db/__tests__/rebuild-marker.test.ts +13 -3
- package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
- package/src/db/__tests__/schema-inspection.test.ts +7 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
- package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
- package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
- package/src/db/api.ts +2 -2
- package/src/db/apply-entity-event.ts +18 -14
- package/src/db/blind-index-cleanup.ts +55 -0
- package/src/db/bun-provider.ts +2 -2
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/config-seed.ts +9 -9
- package/src/db/connection.ts +7 -12
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +20 -26
- package/src/db/entity-field-encryption.ts +81 -24
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +63 -90
- package/src/db/event-store-executor-context.ts +404 -0
- package/src/db/event-store-executor-read.ts +275 -0
- package/src/db/event-store-executor-write.ts +644 -0
- package/src/db/event-store-executor.ts +28 -1155
- package/src/db/feature-table-sources.ts +6 -5
- package/src/db/index.ts +20 -3
- package/src/db/located-timestamp.ts +4 -0
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/migrate-runner.ts +107 -11
- package/src/db/pg-error.ts +9 -1
- package/src/db/postgres-provider.ts +2 -2
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
- package/src/db/queries/backfill-pii.ts +277 -0
- package/src/db/queries/ddl.ts +45 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/queries/event-store.ts +126 -19
- package/src/db/queries/projection-rebuild.ts +22 -8
- package/src/db/queries/shadow-swap.ts +183 -0
- package/src/db/queries/test-stack.ts +4 -30
- package/src/db/query-api.ts +1 -0
- package/src/db/query.ts +1 -0
- package/src/db/rebuild-marker.ts +18 -2
- package/src/db/reference-data.ts +2 -3
- package/src/db/replay-migration-sql.ts +257 -0
- package/src/db/schema-inspection.ts +1 -1
- package/src/db/table-builder.ts +102 -71
- package/src/db/tenant-db.ts +15 -53
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
- package/src/engine/__tests__/boot-validator.test.ts +219 -15
- package/src/engine/__tests__/build-app-schema.test.ts +82 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
- package/src/engine/__tests__/build-target.test.ts +3 -11
- package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
- package/src/engine/__tests__/config-helpers.test.ts +16 -0
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +163 -1
- package/src/engine/__tests__/entity-handlers.test.ts +80 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
- package/src/engine/__tests__/factories-time.test.ts +2 -66
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/feature-manifest.test.ts +31 -1
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/membership-roles.test.ts +4 -10
- package/src/engine/__tests__/nav.test.ts +86 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/__tests__/registry.test.ts +144 -0
- package/src/engine/__tests__/schema-builder.test.ts +18 -0
- package/src/engine/__tests__/screen.test.ts +147 -1
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
- package/src/engine/__tests__/store-table.test.ts +229 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
- package/src/engine/boot-validator/action-wiring.ts +149 -0
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/config-deps.ts +40 -4
- package/src/engine/boot-validator/entity-handler.ts +20 -21
- package/src/engine/boot-validator/entity-list-screens.ts +88 -0
- package/src/engine/boot-validator/gdpr-storage.ts +0 -20
- package/src/engine/boot-validator/i18n-keys.ts +58 -4
- package/src/engine/boot-validator/index.ts +33 -12
- package/src/engine/boot-validator/nav.ts +125 -0
- package/src/engine/boot-validator/pii-retention.ts +95 -10
- package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
- package/src/engine/boot-validator/workspaces.ts +68 -0
- package/src/engine/build-app-schema.ts +16 -0
- package/src/engine/build-config-feature-schema.ts +44 -7
- package/src/engine/codemod/pipeline-codemod.ts +5 -5
- package/src/engine/config-helpers.ts +15 -0
- package/src/engine/constants.ts +32 -6
- package/src/engine/create-app.ts +11 -0
- package/src/engine/define-feature.ts +95 -947
- package/src/engine/define-handler.ts +28 -94
- package/src/engine/define-workflow.ts +1 -1
- package/src/engine/effective-features.ts +12 -2
- package/src/engine/entity-handlers.ts +76 -11
- package/src/engine/extensions/tenant-data.ts +19 -0
- package/src/engine/extensions/user-data.ts +29 -2
- package/src/engine/factories.ts +8 -49
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
- package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
- package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
- package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +234 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +34 -31
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round1.ts +3 -3
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/extractors/round6.ts +2 -36
- package/src/engine/feature-ast/extractors/shared.ts +64 -6
- package/src/engine/feature-ast/index.ts +2 -4
- package/src/engine/feature-ast/parse.ts +130 -23
- package/src/engine/feature-ast/patch.ts +39 -50
- package/src/engine/feature-ast/patcher.ts +37 -38
- package/src/engine/feature-ast/patterns.ts +50 -62
- package/src/engine/feature-ast/render.ts +67 -44
- package/src/engine/feature-builder-state.ts +168 -0
- package/src/engine/feature-config-events-jobs.ts +403 -0
- package/src/engine/feature-entity-handlers.ts +208 -0
- package/src/engine/feature-manifest.ts +2 -1
- package/src/engine/feature-ui-extensions.ts +500 -0
- package/src/engine/field-access.ts +13 -2
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +16 -0
- package/src/engine/index.ts +23 -7
- package/src/engine/membership-roles.ts +13 -0
- package/src/engine/object-form.ts +27 -0
- package/src/engine/ownership.ts +26 -79
- package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
- package/src/engine/pattern-library/library.ts +44 -1152
- package/src/engine/pattern-library/mixed-schemas.ts +450 -0
- package/src/engine/pattern-library/opaque-schemas.ts +124 -0
- package/src/engine/pattern-library/shared-fields.ts +75 -0
- package/src/engine/pattern-library/static-schemas.ts +456 -0
- package/src/engine/pipeline.ts +6 -11
- package/src/engine/registry-facade.ts +362 -0
- package/src/engine/registry-ingest.ts +478 -0
- package/src/engine/registry-state.ts +388 -0
- package/src/engine/registry-validate.ts +642 -0
- package/src/engine/registry.ts +78 -1658
- package/src/engine/run-pipeline.ts +1 -1
- package/src/engine/schema-builder.ts +1 -0
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/soft-delete-cleanup.ts +6 -2
- package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
- package/src/engine/steps/_duration-utils.ts +2 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/config.ts +2 -482
- package/src/engine/types/define-handler.ts +2 -0
- package/src/engine/types/entity-handlers.ts +2 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +2 -972
- package/src/engine/types/fields.ts +2 -675
- package/src/engine/types/handlers.ts +2 -774
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -54
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +86 -40
- package/src/engine/types/nav.ts +2 -63
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -138
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -574
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -132
- package/src/engine/types/workspace.ts +2 -49
- package/src/engine/validate-projection-allowlist.ts +6 -6
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
- package/src/entrypoint/index.ts +49 -6
- package/src/errors/classes.ts +22 -1
- package/src/errors/field-issue.ts +0 -3
- package/src/errors/index.ts +1 -1
- package/src/errors/write-error-info.ts +10 -22
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
- package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
- package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/event-store/admin-api.ts +11 -4
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +64 -78
- package/src/event-store/events-schema.ts +11 -13
- package/src/event-store/index.ts +18 -3
- package/src/event-store/rebuild-dead-letter.ts +111 -0
- package/src/event-store/snapshot.ts +63 -40
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/build-storage-key.test.ts +28 -0
- package/src/files/__tests__/file-ref-entity.test.ts +8 -0
- package/src/files/__tests__/files.integration.test.ts +24 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/__tests__/local-provider.test.ts +31 -0
- package/src/files/__tests__/provider-resolver.test.ts +70 -0
- package/src/files/__tests__/write-stream.test.ts +13 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/file-ref-entity.ts +2 -2
- package/src/files/file-routes.ts +13 -0
- package/src/files/index.ts +1 -1
- package/src/files/local-provider.ts +27 -8
- package/src/files/provider-resolver.ts +31 -20
- package/src/files/types.ts +13 -55
- package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
- package/src/i18n/required-surface-keys.ts +120 -1
- package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
- package/src/jobs/job-runner.ts +82 -12
- package/src/logging/types.ts +1 -7
- package/src/migrations/pending-rebuilds.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +4 -1
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/index.ts +2 -0
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/standard-metrics.ts +64 -2
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -64
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
- package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
- package/src/pipeline/dispatch-batch.ts +187 -0
- package/src/pipeline/dispatch-query.ts +165 -0
- package/src/pipeline/dispatch-shared.ts +826 -0
- package/src/pipeline/dispatch-stream.ts +90 -0
- package/src/pipeline/dispatch-write.ts +451 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +44 -1372
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +30 -2
- package/src/pipeline/event-dispatcher-admin.ts +293 -0
- package/src/pipeline/event-dispatcher-delivery.ts +305 -0
- package/src/pipeline/event-dispatcher.ts +85 -542
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/msp-rebuild.ts +42 -3
- package/src/pipeline/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/projection-rebuild.ts +105 -3
- package/src/pipeline/system-hooks.ts +144 -1
- package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
- package/src/rate-limit/resolver.ts +16 -32
- package/src/schema-cli.ts +76 -16
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
- package/src/search/index.ts +6 -0
- package/src/search/meilisearch-adapter.ts +13 -12
- package/src/search/reindex-entity.ts +177 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/contains-secret.test.ts +34 -0
- package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/dek-cache.ts +26 -5
- package/src/secrets/envelope-cipher.ts +53 -0
- package/src/secrets/envelope.ts +5 -3
- package/src/secrets/index.ts +13 -1
- package/src/secrets/stored-envelope.ts +46 -0
- package/src/secrets/types.ts +2 -162
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
- package/src/stack/db.ts +2 -1
- package/src/stack/push-entity-projection-tables.ts +4 -3
- package/src/stack/redis.ts +8 -0
- package/src/stack/request-helper.ts +38 -2
- package/src/stack/table-helpers.ts +6 -4
- package/src/stack/test-stack.ts +249 -131
- package/src/testing/__tests__/late-bound.test.ts +32 -0
- package/src/testing/__tests__/wait-for.test.ts +59 -0
- package/src/testing/boot-validator-fixture.ts +121 -0
- package/src/testing/e2e-generator.ts +8 -0
- package/src/testing/file-provider-contract.ts +104 -0
- package/src/testing/handler-context.ts +3 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/late-bound.ts +5 -3
- package/src/testing/mutable-master-key-provider.ts +29 -3
- package/src/testing/wait-for.ts +3 -0
- package/src/testing/without-ambient-temporal.ts +14 -0
- package/src/time/__tests__/tz-dateline.test.ts +8 -10
- package/src/time/geo-tz.ts +1 -32
- package/src/time/index.ts +1 -0
- package/src/time/legacy-date.ts +18 -0
- package/src/time/polyfill.ts +21 -38
- package/src/time/tz-context.ts +44 -85
- package/src/ui-types/app-schema.ts +12 -0
- package/src/ui-types/index.ts +20 -7
- package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
- package/src/utils/safe-json.ts +13 -1
- package/src/__tests__/raw-table.integration.test.ts +0 -116
- package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
- package/src/db/__tests__/encryption.test.ts +0 -39
- package/src/db/encryption.ts +0 -39
- package/src/db/row-helpers.ts +0 -4
- package/src/engine/__tests__/raw-table.test.ts +0 -150
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
- package/src/engine/feature-ast/extractors/round4.ts +0 -1366
package/src/schema-cli.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
baselineMigrations,
|
|
15
15
|
createDbConnection,
|
|
16
16
|
type DbConnection,
|
|
17
|
+
diffReplayAgainstSnapshot,
|
|
17
18
|
fetchAppliedMigrations,
|
|
18
19
|
generateMigration,
|
|
19
20
|
loadMigrationsFromDir,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
readRebuildMarker,
|
|
22
23
|
rebuildTablesFromDiff,
|
|
23
24
|
type renderTablesDdl,
|
|
25
|
+
replayMigrationsDir,
|
|
24
26
|
runMigrationsFromDir,
|
|
25
27
|
tableExists,
|
|
26
28
|
writeRebuildMarker,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
createProjectionStateTable,
|
|
37
39
|
rebuildProjection,
|
|
38
40
|
} from "./pipeline";
|
|
41
|
+
import { ensureTemporalPolyfill } from "./time";
|
|
39
42
|
|
|
40
43
|
export type SchemaCliOut = {
|
|
41
44
|
readonly log: (line: string) => void;
|
|
@@ -44,17 +47,28 @@ export type SchemaCliOut = {
|
|
|
44
47
|
|
|
45
48
|
const SNAPSHOT_FILENAME = ".snapshot.json";
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
type SchemaMod = {
|
|
51
|
+
readonly entityMetas: Parameters<typeof renderTablesDdl>[0];
|
|
52
|
+
readonly features?: unknown;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Single load-path for kumiko/schema.ts (512/3) — `generate` and `validate`
|
|
56
|
+
// both need ENTITY_METAS (and validate additionally wants FEATURES); this is
|
|
57
|
+
// the one place that enforces "ENTITY_METAS must be an array" so the two
|
|
58
|
+
// commands can't drift on the check or its wording. Throws — callers that
|
|
59
|
+
// want a graceful exit code (validate) catch it themselves.
|
|
60
|
+
async function loadSchemaModFromApp(schemaFile: string): Promise<SchemaMod> {
|
|
50
61
|
// bun imports TS directly — no spawn needed.
|
|
51
|
-
const mod = (await import(schemaFile)) as { ENTITY_METAS?: unknown };
|
|
62
|
+
const mod = (await import(schemaFile)) as { ENTITY_METAS?: unknown; FEATURES?: unknown };
|
|
52
63
|
if (!Array.isArray(mod.ENTITY_METAS)) {
|
|
53
64
|
throw new Error(
|
|
54
65
|
`Schema file ${schemaFile} muss \`export const ENTITY_METAS: EntityTableMeta[]\` haben.`,
|
|
55
66
|
);
|
|
56
67
|
}
|
|
57
|
-
return
|
|
68
|
+
return {
|
|
69
|
+
entityMetas: mod.ENTITY_METAS as Parameters<typeof renderTablesDdl>[0],
|
|
70
|
+
features: mod.FEATURES,
|
|
71
|
+
};
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
function nextSequenceNumber(migrationsDir: string): number {
|
|
@@ -115,6 +129,13 @@ export async function runSchemaCli(
|
|
|
115
129
|
out: SchemaCliOut,
|
|
116
130
|
options: RunSchemaCliOptions = {},
|
|
117
131
|
): Promise<number> {
|
|
132
|
+
// runProdApp/runDevApp install this at boot; the standalone CLI (the
|
|
133
|
+
// migrate-db initContainer, `bun kumiko.js schema apply`) never goes through
|
|
134
|
+
// that boot path, so a projection rebuild's tz/timestamp coercion throws
|
|
135
|
+
// "Temporal is not defined" — deterministically, on every rebuild-marker
|
|
136
|
+
// migration, since the crashed process still records the migration as
|
|
137
|
+
// applied and the rebuild is never retried.
|
|
138
|
+
await ensureTemporalPolyfill();
|
|
118
139
|
const sub = argv[0];
|
|
119
140
|
const schemaFile = resolvePath(appCwd, "kumiko/schema.ts");
|
|
120
141
|
const migrationsDir = resolvePath(appCwd, "kumiko/migrations");
|
|
@@ -132,7 +153,7 @@ export async function runSchemaCli(
|
|
|
132
153
|
out.err(" export const ENTITY_METAS: EntityTableMeta[] = [...]");
|
|
133
154
|
return 1;
|
|
134
155
|
}
|
|
135
|
-
const metas = await
|
|
156
|
+
const { entityMetas: metas } = await loadSchemaModFromApp(schemaFile);
|
|
136
157
|
const snapshotPath = join(migrationsDir, SNAPSHOT_FILENAME);
|
|
137
158
|
const prevSnapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
|
|
138
159
|
const result = generateMigration({
|
|
@@ -179,11 +200,18 @@ export async function runSchemaCli(
|
|
|
179
200
|
|
|
180
201
|
case "validate": {
|
|
181
202
|
// Static, DB-free boot-blocking checks for CI — catches "this won't boot"
|
|
182
|
-
// before deploy.
|
|
203
|
+
// before deploy. Three layers, no database:
|
|
183
204
|
// 1. schema drift: would `generate` write a migration? (= an entity was
|
|
184
205
|
// added/changed but never generated → missing table → prod 500)
|
|
185
206
|
// 2. boot validity: validateBoot over the composed FEATURES (QN/screen/
|
|
186
207
|
// nav/role refs). Runs only if kumiko/schema.ts exports FEATURES.
|
|
208
|
+
// 3. migration-content drift: replay every committed *.sql file and
|
|
209
|
+
// diff the result against .snapshot.json — catches a migration
|
|
210
|
+
// file whose SQL body doesn't match what its own snapshot entry
|
|
211
|
+
// claims (e.g. an accidental copy-paste from an earlier file).
|
|
212
|
+
// Layer 1 alone misses this: it only compares ENTITY_METAS to the
|
|
213
|
+
// snapshot, never the snapshot to the SQL that's supposed to have
|
|
214
|
+
// produced it.
|
|
187
215
|
// The DB-level gate (assertKumikoSchemaCurrent) stays at boot/deploy.
|
|
188
216
|
if (!existsSync(schemaFile)) {
|
|
189
217
|
out.err(` ${schemaFile} fehlt.`);
|
|
@@ -191,20 +219,18 @@ export async function runSchemaCli(
|
|
|
191
219
|
out.err(" export const ENTITY_METAS: EntityTableMeta[] = [...]");
|
|
192
220
|
return 1;
|
|
193
221
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
out.err(
|
|
200
|
-
` Schema file ${schemaFile} muss \`export const ENTITY_METAS: EntityTableMeta[]\` haben.`,
|
|
201
|
-
);
|
|
222
|
+
let schemaMod: SchemaMod;
|
|
223
|
+
try {
|
|
224
|
+
schemaMod = await loadSchemaModFromApp(schemaFile);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
out.err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
202
227
|
return 1;
|
|
203
228
|
}
|
|
229
|
+
const mod = { FEATURES: schemaMod.features };
|
|
204
230
|
let ok = true;
|
|
205
231
|
|
|
206
232
|
// 1. Schema drift — compute the diff, never write.
|
|
207
|
-
const metas =
|
|
233
|
+
const metas = schemaMod.entityMetas;
|
|
208
234
|
const snapshotPath = join(migrationsDir, SNAPSHOT_FILENAME);
|
|
209
235
|
const prevSnapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
|
|
210
236
|
const drift = generateMigration({
|
|
@@ -245,6 +271,40 @@ export async function runSchemaCli(
|
|
|
245
271
|
);
|
|
246
272
|
}
|
|
247
273
|
|
|
274
|
+
// 3. Migration-content drift — replay the committed *.sql files and
|
|
275
|
+
// diff the reconstructed schema against .snapshot.json.
|
|
276
|
+
const committedSnapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
|
|
277
|
+
if (existsSync(migrationsDir) && committedSnapshot !== null) {
|
|
278
|
+
try {
|
|
279
|
+
const replayed = replayMigrationsDir(migrationsDir);
|
|
280
|
+
const mismatches = diffReplayAgainstSnapshot(replayed, committedSnapshot);
|
|
281
|
+
if (mismatches.length === 0) {
|
|
282
|
+
out.log(" ✓ migrations: table/column names match .snapshot.json");
|
|
283
|
+
} else {
|
|
284
|
+
ok = false;
|
|
285
|
+
out.err(
|
|
286
|
+
" ✗ migration-content drift: committed *.sql files don't produce .snapshot.json.",
|
|
287
|
+
);
|
|
288
|
+
for (const m of mismatches) {
|
|
289
|
+
out.err(` ${m.tableName} (${m.kind}): ${m.detail}`);
|
|
290
|
+
}
|
|
291
|
+
out.err(
|
|
292
|
+
" Fix: a migration file's body doesn't match what it (or the snapshot) claims — hand-fix the file, or ship a corrective migration if it's already applied in prod.",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
// replayMigrationsDir fail-loud's on a table-DDL statement it can't
|
|
297
|
+
// parse — that must surface as a normal ✗ line like every other
|
|
298
|
+
// check here, not a raw stack trace that kills the process before
|
|
299
|
+
// any later check (or the Fix: help) ever runs (framework#1535).
|
|
300
|
+
ok = false;
|
|
301
|
+
out.err(` ✗ migration-content: ${e instanceof Error ? e.message : String(e)}`);
|
|
302
|
+
out.err(
|
|
303
|
+
" Fix: a statement in a migration file isn't recognizable to the replay parser — extend the parser's recognized patterns, or reword the migration to one it understands.",
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
248
308
|
return ok ? 0 : 1;
|
|
249
309
|
}
|
|
250
310
|
|
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import { generateId as uuid } from "@cosmicdrift/kumiko-framework/utils";
|
|
4
3
|
import { Meilisearch } from "meilisearch";
|
|
5
|
-
import { createMeilisearchAdapter } from "../meilisearch-adapter";
|
|
4
|
+
import { createMeilisearchAdapter, meilisearchTenantIndex } from "../meilisearch-adapter";
|
|
6
5
|
import type { SearchAdapter } from "../types";
|
|
7
6
|
|
|
8
7
|
const MEILI_URL = process.env["MEILI_URL"] ?? "http://localhost:17700";
|
|
9
8
|
const MEILI_KEY = process.env["MEILI_MASTER_KEY"] ?? "kumiko-dev-key";
|
|
10
9
|
|
|
10
|
+
async function meiliAvailable(): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
const health = await fetch(`${MEILI_URL}/health`, { signal: AbortSignal.timeout(2000) });
|
|
13
|
+
return health.ok;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// skipIf when compose Meili is down — unit tests still cover index/doc id shaping.
|
|
20
|
+
// In a dedicated integration job (REQUIRE_MEILI=1) a down Meili is a hard
|
|
21
|
+
// failure, not a silent skip — the compose service is expected to be up there.
|
|
22
|
+
const MEILI_UP = await meiliAvailable();
|
|
23
|
+
if (!MEILI_UP) {
|
|
24
|
+
if (process.env["REQUIRE_MEILI"] === "1") {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`REQUIRE_MEILI=1 but Meilisearch not reachable at ${MEILI_URL} — integration job misconfigured`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
console.warn(
|
|
30
|
+
`Meilisearch not reachable at ${MEILI_URL} — skipping live adapter tests ` +
|
|
31
|
+
`(docker compose up -d meilisearch, default port 17700)`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
11
35
|
// Use a fake tenantId to get a unique index name
|
|
12
36
|
const TENANT = uuid();
|
|
13
37
|
|
|
@@ -15,222 +39,220 @@ let adapter: SearchAdapter;
|
|
|
15
39
|
let client: Meilisearch;
|
|
16
40
|
let indexPrefix: string;
|
|
17
41
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
apiKey: MEILI_KEY,
|
|
28
|
-
indexPrefix,
|
|
29
|
-
});
|
|
42
|
+
describe.skipIf(!MEILI_UP)("meilisearch adapter (live)", () => {
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
client = new Meilisearch({ host: MEILI_URL, apiKey: MEILI_KEY });
|
|
45
|
+
indexPrefix = `test_${uuid().slice(-6)}_`;
|
|
46
|
+
adapter = createMeilisearchAdapter({
|
|
47
|
+
url: MEILI_URL,
|
|
48
|
+
apiKey: MEILI_KEY,
|
|
49
|
+
indexPrefix,
|
|
50
|
+
});
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
await adapter.configure(TENANT, {
|
|
53
|
+
searchableFields: ["email", "firstName", "lastName", "notes", "_roles"],
|
|
54
|
+
rankingFields: ["email", "firstName", "lastName", "notes", "_roles"],
|
|
55
|
+
});
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
57
|
+
// Seed data with different entity types and weights
|
|
58
|
+
await adapter.index(TENANT, {
|
|
59
|
+
entityType: "user",
|
|
60
|
+
entityId: 1,
|
|
61
|
+
weight: 10,
|
|
62
|
+
fields: {
|
|
63
|
+
email: "marc.weber@company.de",
|
|
64
|
+
firstName: "Marc",
|
|
65
|
+
lastName: "Weber",
|
|
66
|
+
notes: "Senior developer",
|
|
67
|
+
_roles: "Admin, Developer",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
await adapter.index(TENANT, {
|
|
71
|
+
entityType: "user",
|
|
72
|
+
entityId: 2,
|
|
73
|
+
weight: 10,
|
|
74
|
+
fields: {
|
|
75
|
+
email: "anna.schmidt@company.de",
|
|
76
|
+
firstName: "Anna",
|
|
77
|
+
lastName: "Schmidt",
|
|
78
|
+
notes: "Project manager",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
await adapter.index(TENANT, {
|
|
82
|
+
entityType: "user",
|
|
83
|
+
entityId: 3,
|
|
84
|
+
weight: 10,
|
|
85
|
+
fields: {
|
|
86
|
+
email: "admin@company.de",
|
|
87
|
+
firstName: "Admin",
|
|
88
|
+
lastName: "User",
|
|
89
|
+
notes: "System administrator",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
await adapter.index(TENANT, {
|
|
93
|
+
entityType: "role",
|
|
94
|
+
entityId: 1,
|
|
95
|
+
weight: 1,
|
|
96
|
+
fields: { firstName: "Admin" },
|
|
97
|
+
});
|
|
98
|
+
await adapter.index(TENANT, {
|
|
99
|
+
entityType: "role",
|
|
100
|
+
entityId: 2,
|
|
101
|
+
weight: 1,
|
|
102
|
+
fields: { firstName: "Developer" },
|
|
103
|
+
});
|
|
104
|
+
await adapter.index(TENANT, {
|
|
105
|
+
entityType: "department",
|
|
106
|
+
entityId: 1,
|
|
107
|
+
weight: 5,
|
|
108
|
+
fields: { firstName: "Engineering" },
|
|
109
|
+
});
|
|
88
110
|
});
|
|
89
|
-
});
|
|
90
111
|
|
|
91
|
-
afterAll(async () => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
afterAll(async () => {
|
|
113
|
+
// Clean up all test indices
|
|
114
|
+
const indices = await client.getIndexes();
|
|
115
|
+
for (const idx of indices.results) {
|
|
116
|
+
if (idx.uid.startsWith("test_")) {
|
|
117
|
+
try {
|
|
118
|
+
await client.index(idx.uid).delete().waitTask();
|
|
119
|
+
} catch {
|
|
120
|
+
/* ok */
|
|
121
|
+
}
|
|
100
122
|
}
|
|
101
123
|
}
|
|
102
|
-
}
|
|
103
|
-
});
|
|
124
|
+
});
|
|
104
125
|
|
|
105
|
-
// --- Basic search ---
|
|
126
|
+
// --- Basic search ---
|
|
106
127
|
|
|
107
|
-
describe("basic search", () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
describe("basic search", () => {
|
|
129
|
+
test("finds user by name", async () => {
|
|
130
|
+
const results = await adapter.search(TENANT, "anna");
|
|
131
|
+
expect(results.some((r) => r.entityId === 2 && r.entityType === "user")).toBe(true);
|
|
132
|
+
});
|
|
112
133
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
test("returns empty for no match", async () => {
|
|
135
|
+
const results = await adapter.search(TENANT, "zzzznonexistent99999");
|
|
136
|
+
expect(results).toEqual([]);
|
|
137
|
+
});
|
|
116
138
|
});
|
|
117
|
-
});
|
|
118
139
|
|
|
119
|
-
// --- Partial matching ---
|
|
140
|
+
// --- Partial matching ---
|
|
120
141
|
|
|
121
|
-
describe("partial matching", () => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
describe("partial matching", () => {
|
|
143
|
+
test("finds by prefix", async () => {
|
|
144
|
+
const results = await adapter.search(TENANT, "mar");
|
|
145
|
+
expect(results.some((r) => r.entityId === 1 && r.entityType === "user")).toBe(true);
|
|
146
|
+
});
|
|
125
147
|
});
|
|
126
|
-
});
|
|
127
148
|
|
|
128
|
-
// --- Typo tolerance ---
|
|
149
|
+
// --- Typo tolerance ---
|
|
129
150
|
|
|
130
|
-
describe("typo tolerance", () => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
describe("typo tolerance", () => {
|
|
152
|
+
test("finds despite typos", async () => {
|
|
153
|
+
const results = await adapter.search(TENANT, "schmit");
|
|
154
|
+
expect(results.some((r) => r.entityId === 2)).toBe(true);
|
|
155
|
+
});
|
|
134
156
|
});
|
|
135
|
-
});
|
|
136
157
|
|
|
137
|
-
// --- Filter by entity type (list search) ---
|
|
158
|
+
// --- Filter by entity type (list search) ---
|
|
138
159
|
|
|
139
|
-
describe("list search (filterType)", () => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
describe("list search (filterType)", () => {
|
|
161
|
+
test("only returns specified entity type", async () => {
|
|
162
|
+
const results = await adapter.search(TENANT, "admin", { filterType: "user" });
|
|
163
|
+
expect(results.every((r) => r.entityType === "user")).toBe(true);
|
|
164
|
+
expect(results.length).toBeGreaterThan(0);
|
|
165
|
+
});
|
|
145
166
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
test("role filter returns only roles", async () => {
|
|
168
|
+
const results = await adapter.search(TENANT, "admin", { filterType: "role" });
|
|
169
|
+
expect(results.every((r) => r.entityType === "role")).toBe(true);
|
|
170
|
+
});
|
|
149
171
|
});
|
|
150
|
-
});
|
|
151
172
|
|
|
152
|
-
// --- Global search with weight ---
|
|
173
|
+
// --- Global search with weight ---
|
|
153
174
|
|
|
154
|
-
describe("global search with searchWeight", () => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
describe("global search with searchWeight", () => {
|
|
176
|
+
test("user (weight 10) ranks before role (weight 1) for same query", async () => {
|
|
177
|
+
const results = await adapter.search(TENANT, "admin");
|
|
178
|
+
const userIdx = results.findIndex((r) => r.entityType === "user");
|
|
179
|
+
const roleIdx = results.findIndex((r) => r.entityType === "role");
|
|
180
|
+
// User should appear before Role due to _weight:desc sort
|
|
181
|
+
if (userIdx >= 0 && roleIdx >= 0) {
|
|
182
|
+
expect(userIdx).toBeLessThan(roleIdx);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
163
185
|
});
|
|
164
|
-
});
|
|
165
186
|
|
|
166
|
-
// --- Resolved relation data ---
|
|
187
|
+
// --- Resolved relation data ---
|
|
167
188
|
|
|
168
|
-
describe("relation data in search", () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
describe("relation data in search", () => {
|
|
190
|
+
test("finds user by role name in _roles field", async () => {
|
|
191
|
+
const results = await adapter.search(TENANT, "developer", { filterType: "user" });
|
|
192
|
+
expect(results.some((r) => r.entityId === 1)).toBe(true);
|
|
193
|
+
});
|
|
172
194
|
});
|
|
173
|
-
});
|
|
174
195
|
|
|
175
|
-
// --- Remove ---
|
|
196
|
+
// --- Remove ---
|
|
176
197
|
|
|
177
|
-
describe("remove", () => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
describe("remove", () => {
|
|
199
|
+
test("removed document not found", async () => {
|
|
200
|
+
// Create temp doc
|
|
201
|
+
await adapter.index(TENANT, {
|
|
202
|
+
entityType: "temp",
|
|
203
|
+
entityId: 999,
|
|
204
|
+
weight: 1,
|
|
205
|
+
fields: { firstName: "DeleteMe" },
|
|
206
|
+
});
|
|
207
|
+
let results = await adapter.search(TENANT, "deleteme");
|
|
208
|
+
expect(results.some((r) => r.entityId === 999)).toBe(true);
|
|
188
209
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
await adapter.remove(TENANT, "temp", 999);
|
|
211
|
+
results = await adapter.search(TENANT, "deleteme");
|
|
212
|
+
expect(results.some((r) => r.entityId === 999)).toBe(false);
|
|
213
|
+
});
|
|
192
214
|
});
|
|
193
|
-
});
|
|
194
215
|
|
|
195
|
-
// --- Batch variants ---
|
|
216
|
+
// --- Batch variants ---
|
|
196
217
|
|
|
197
|
-
describe("indexBatch / removeBatch", () => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
});
|
|
218
|
+
describe("indexBatch / removeBatch", () => {
|
|
219
|
+
test("indexBatch indexes multiple docs in a single task", async () => {
|
|
220
|
+
const docs = Array.from({ length: 5 }, (_, i) => ({
|
|
221
|
+
entityType: "batch" as const,
|
|
222
|
+
entityId: 1000 + i,
|
|
223
|
+
weight: 1,
|
|
224
|
+
fields: { firstName: `Bulk${i}`, notes: "batchtoken" },
|
|
225
|
+
}));
|
|
226
|
+
await adapter.indexBatch?.(TENANT, docs);
|
|
212
227
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
[1000, 1001, 1002, 1003, 1004]
|
|
217
|
-
);
|
|
218
|
-
const hits = await adapter.search(TENANT, "batchtoken", { limit: 20, filterType: "batch" });
|
|
219
|
-
expect(hits.length).toBe(0);
|
|
220
|
-
});
|
|
228
|
+
const hits = await adapter.search(TENANT, "batchtoken", { limit: 20, filterType: "batch" });
|
|
229
|
+
expect(hits.length).toBe(5);
|
|
230
|
+
const ids = hits.map((h) => h.entityId).sort();
|
|
231
|
+
expect(ids).toEqual([1000, 1001, 1002, 1003, 1004]);
|
|
232
|
+
});
|
|
221
233
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
test("removeBatch removes multiple docs in a single task", async () => {
|
|
235
|
+
await adapter.removeBatch?.(
|
|
236
|
+
TENANT,
|
|
237
|
+
[1000, 1001, 1002, 1003, 1004].map((id) => ({ entityType: "batch", entityId: id })),
|
|
238
|
+
);
|
|
239
|
+
const hits = await adapter.search(TENANT, "batchtoken", { limit: 20, filterType: "batch" });
|
|
240
|
+
expect(hits.length).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("indexBatch no-ops on empty array — no Meilisearch task created", async () => {
|
|
244
|
+
// We verify the "no-op" contract by peeking at Meilisearch's own
|
|
245
|
+
// IndexStats.numberOfDocuments + isIndexing directly. If the adapter had
|
|
246
|
+
// accidentally sent an addDocuments request (even with an empty body),
|
|
247
|
+
// isIndexing would flip to true or a task would land in the queue.
|
|
248
|
+
// numberOfDocuments must also stay unchanged — the empty batch must not
|
|
249
|
+
// replace, delete, or otherwise touch existing docs.
|
|
250
|
+
const index = client.index(meilisearchTenantIndex(indexPrefix, TENANT));
|
|
251
|
+
const before = await index.getStats();
|
|
252
|
+
await expect(adapter.indexBatch?.(TENANT, [])).resolves.toBeUndefined();
|
|
253
|
+
const after = await index.getStats();
|
|
254
|
+
expect(after.numberOfDocuments).toBe(before.numberOfDocuments);
|
|
255
|
+
expect(after.isIndexing).toBe(false);
|
|
256
|
+
});
|
|
235
257
|
});
|
|
236
258
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { meilisearchDocId, meilisearchTenantIndex } from "../meilisearch-adapter";
|
|
3
|
+
|
|
4
|
+
describe("meilisearchTenantIndex", () => {
|
|
5
|
+
test("prefixes tenant id with t after the index prefix", () => {
|
|
6
|
+
expect(meilisearchTenantIndex("kumiko_", "00000000-0000-4000-8000-000000000001")).toBe(
|
|
7
|
+
"kumiko_t00000000-0000-4000-8000-000000000001",
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("keeps custom prefixes intact", () => {
|
|
12
|
+
expect(meilisearchTenantIndex("test_abc_", 42 as never)).toBe("test_abc_t42");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("meilisearchDocId", () => {
|
|
17
|
+
test("joins entity type and id with underscore", () => {
|
|
18
|
+
expect(meilisearchDocId("user", 7)).toBe("user_7");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("preserves UUID dashes (legal Meilisearch primary-key chars)", () => {
|
|
22
|
+
expect(meilisearchDocId("order", "a1b2c3d4-e5f6-7890-abcd-ef1234567890" as never)).toBe(
|
|
23
|
+
"order_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("sanitizes illegal primary-key characters to underscore", () => {
|
|
28
|
+
expect(meilisearchDocId("note", "a:b/c.d" as never)).toBe("note_a_b_c_d");
|
|
29
|
+
});
|
|
30
|
+
});
|