@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
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createTextField,
|
|
15
15
|
} from "../factories";
|
|
16
16
|
import { createApp, createRegistry, defineFeature } from "../index";
|
|
17
|
+
import { buildMinimalCtx } from "./_pipeline-test-utils";
|
|
17
18
|
|
|
18
19
|
// --- defineFeature ---
|
|
19
20
|
|
|
@@ -41,7 +42,9 @@ describe("defineFeature", () => {
|
|
|
41
42
|
r.describe("first");
|
|
42
43
|
r.describe("second");
|
|
43
44
|
}),
|
|
44
|
-
).toThrow(
|
|
45
|
+
).toThrow(
|
|
46
|
+
"[Feature test] r.describe() called twice — a feature's description is declared once",
|
|
47
|
+
);
|
|
45
48
|
});
|
|
46
49
|
|
|
47
50
|
test("r.describe() throws on empty or whitespace-only text", () => {
|
|
@@ -116,6 +119,33 @@ describe("defineFeature", () => {
|
|
|
116
119
|
expect(ref?.name).toBe("order:list");
|
|
117
120
|
});
|
|
118
121
|
|
|
122
|
+
test("streamHandler registers on feature.streamHandlers with schema/access/handler intact", async () => {
|
|
123
|
+
let ref: { name: string } | undefined;
|
|
124
|
+
const feature = defineFeature("test", (r) => {
|
|
125
|
+
ref = r.streamHandler(
|
|
126
|
+
"chat:complete",
|
|
127
|
+
z.object({ prompt: z.string() }),
|
|
128
|
+
async function* (query) {
|
|
129
|
+
yield `echo:${query.payload.prompt}`;
|
|
130
|
+
},
|
|
131
|
+
{ access: { openToAll: true } },
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
expect(ref?.name).toBe("chat:complete");
|
|
135
|
+
|
|
136
|
+
const stored = feature.streamHandlers["chat:complete"];
|
|
137
|
+
expect(stored?.access).toEqual({ openToAll: true });
|
|
138
|
+
|
|
139
|
+
const chunks: string[] = [];
|
|
140
|
+
for await (const chunk of stored!.handler(
|
|
141
|
+
{ type: "chat:complete", payload: { prompt: "hi" }, user: createTestUser() },
|
|
142
|
+
buildMinimalCtx(),
|
|
143
|
+
)) {
|
|
144
|
+
chunks.push(chunk as string);
|
|
145
|
+
}
|
|
146
|
+
expect(chunks).toEqual(["echo:hi"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
119
149
|
test("r.defineEvent returns typed EventDef and registers on feature", () => {
|
|
120
150
|
let eventRef: { name: string } | undefined;
|
|
121
151
|
const feature = defineFeature("orders", (r) => {
|
|
@@ -274,6 +304,24 @@ describe("defineFeature", () => {
|
|
|
274
304
|
}),
|
|
275
305
|
).toThrow(/r\.uiHints\(\) called twice/);
|
|
276
306
|
});
|
|
307
|
+
|
|
308
|
+
test("r.toggleable() throws when called twice", () => {
|
|
309
|
+
expect(() =>
|
|
310
|
+
defineFeature("test", (r) => {
|
|
311
|
+
r.toggleable({ default: true });
|
|
312
|
+
r.toggleable({ default: false });
|
|
313
|
+
}),
|
|
314
|
+
).toThrow(/r\.toggleable\(\) called twice/);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("r.envSchema() throws when called twice", () => {
|
|
318
|
+
expect(() =>
|
|
319
|
+
defineFeature("test", (r) => {
|
|
320
|
+
r.envSchema(z.object({ FOO: z.string() }));
|
|
321
|
+
r.envSchema(z.object({ BAR: z.string() }));
|
|
322
|
+
}),
|
|
323
|
+
).toThrow(/r\.envSchema\(\) called twice/);
|
|
324
|
+
});
|
|
277
325
|
});
|
|
278
326
|
|
|
279
327
|
// --- Field Factories ---
|
|
@@ -419,6 +467,22 @@ describe("createRegistry", () => {
|
|
|
419
467
|
expect(all["profile:nav.title"]).toEqual({ de: "Profil", en: "Profile" });
|
|
420
468
|
});
|
|
421
469
|
|
|
470
|
+
// #1105: a feature that already qualifies its own translation keys (nav
|
|
471
|
+
// labels referencing "featureName:..." verbatim, see cap-counter) must not
|
|
472
|
+
// be double-prefixed — else server-side t() can never resolve them.
|
|
473
|
+
test("does not double-prefix translation keys that already carry the feature's own namespace", () => {
|
|
474
|
+
const f = defineFeature("cap-counter", (r) => {
|
|
475
|
+
r.translations({
|
|
476
|
+
keys: { "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" } },
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const registry = createRegistry([f]);
|
|
481
|
+
const all = registry.getAllTranslations();
|
|
482
|
+
expect(all["cap-counter:nav.cap-list"]).toEqual({ de: "Limits", en: "Caps" });
|
|
483
|
+
expect(all["cap-counter:cap-counter:nav.cap-list"]).toBeUndefined();
|
|
484
|
+
});
|
|
485
|
+
|
|
422
486
|
test("throws when write handler is not entity-mapped in feature with field-access", () => {
|
|
423
487
|
const feature = defineFeature("hr", (r) => {
|
|
424
488
|
r.entity(
|
|
@@ -675,6 +739,36 @@ describe("createApp", () => {
|
|
|
675
739
|
).not.toThrow();
|
|
676
740
|
});
|
|
677
741
|
|
|
742
|
+
test("validates stream handler roles against app-defined roles (#1442)", () => {
|
|
743
|
+
const feature = defineFeature("admin", (r) => {
|
|
744
|
+
r.streamHandler("admin.tail", z.object({}), async function* () {}, {
|
|
745
|
+
access: { roles: ["SuperAdmin"] },
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
expect(() =>
|
|
750
|
+
createApp({
|
|
751
|
+
roles: ["Admin", "User"] as const,
|
|
752
|
+
features: [feature],
|
|
753
|
+
}),
|
|
754
|
+
).toThrow(/unknown role.*SuperAdmin/i);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test("passes when stream handler roles are valid (#1442)", () => {
|
|
758
|
+
const feature = defineFeature("admin", (r) => {
|
|
759
|
+
r.streamHandler("admin.tail", z.object({}), async function* () {}, {
|
|
760
|
+
access: { roles: ["Admin"] },
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
expect(() =>
|
|
765
|
+
createApp({
|
|
766
|
+
roles: ["Admin", "User"] as const,
|
|
767
|
+
features: [feature],
|
|
768
|
+
}),
|
|
769
|
+
).not.toThrow();
|
|
770
|
+
});
|
|
771
|
+
|
|
678
772
|
test("createApp returns registry", () => {
|
|
679
773
|
const feature = defineFeature("test", () => {});
|
|
680
774
|
const app = createApp({ roles: ["Admin"] as const, features: [feature] });
|
|
@@ -1188,6 +1282,50 @@ describe("r.config()", () => {
|
|
|
1188
1282
|
});
|
|
1189
1283
|
});
|
|
1190
1284
|
|
|
1285
|
+
// --- r.config() single-key overload ---
|
|
1286
|
+
|
|
1287
|
+
describe("r.config() single-key overload", () => {
|
|
1288
|
+
test("returns a bare handle with the same qualified name the multi-key form would produce", () => {
|
|
1289
|
+
let viaSingleKey!: { readonly name: string; readonly type: "boolean" };
|
|
1290
|
+
let viaMultiKey!: { readonly defaultVat: { readonly name: string; readonly type: "number" } };
|
|
1291
|
+
defineFeature("invoicing", (r) => {
|
|
1292
|
+
viaSingleKey = r.config("enabled", createTenantConfig("boolean", { default: false }));
|
|
1293
|
+
});
|
|
1294
|
+
defineFeature("invoicing2", (r) => {
|
|
1295
|
+
viaMultiKey = r.config({
|
|
1296
|
+
keys: { defaultVat: createTenantConfig("number", { default: 19 }) },
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
expect(viaSingleKey.name).toBe("invoicing:config:enabled");
|
|
1301
|
+
expect(viaSingleKey.type).toBe("boolean");
|
|
1302
|
+
// Structural check: the single-key form's handle is exactly what
|
|
1303
|
+
// {keys:{one}} would have produced for that single key — same shape,
|
|
1304
|
+
// same qualification scheme.
|
|
1305
|
+
expect(viaMultiKey.defaultVat.name).toBe("invoicing2:config:default-vat");
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
test("registers the key on the feature exactly like the multi-key form would", () => {
|
|
1309
|
+
const feature = defineFeature("shop", (r) => {
|
|
1310
|
+
r.config("maxItems", createTenantConfig("number", { default: 10 }));
|
|
1311
|
+
});
|
|
1312
|
+
expect(feature.configKeys["maxItems"]).toBeDefined();
|
|
1313
|
+
expect(feature.configKeys["maxItems"]?.type).toBe("number");
|
|
1314
|
+
expect(feature.configKeys["maxItems"]?.scope).toBe("tenant");
|
|
1315
|
+
|
|
1316
|
+
const registry = createRegistry([feature]);
|
|
1317
|
+
expect(registry.getConfigKey("shop:config:max-items")).toBeDefined();
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
test("camelCase feature + key are kebab-cased in the handle name", () => {
|
|
1321
|
+
let handle!: { readonly name: string };
|
|
1322
|
+
defineFeature("billingCore", (r) => {
|
|
1323
|
+
handle = r.config("monthlyTotalCents", createSystemConfig("number", { default: 0 }));
|
|
1324
|
+
});
|
|
1325
|
+
expect(handle.name).toBe("billing-core:config:monthly-total-cents");
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1191
1329
|
// --- Relations ---
|
|
1192
1330
|
|
|
1193
1331
|
describe("r.relation()", () => {
|
|
@@ -1325,6 +1463,30 @@ describe("registry relations", () => {
|
|
|
1325
1463
|
// Last write wins
|
|
1326
1464
|
expect((rels["roles"] as { through: { table: string } }).through.table).toBe("UR2");
|
|
1327
1465
|
});
|
|
1466
|
+
|
|
1467
|
+
test("getRelations aggregates relations on the same entity from two different features", () => {
|
|
1468
|
+
const core = defineFeature("core", (r) => {
|
|
1469
|
+
r.entity("user", createEntity({ table: "Users", fields: {} }));
|
|
1470
|
+
r.entity("role", createEntity({ table: "Roles", fields: {} }));
|
|
1471
|
+
r.relation("user", "roles", {
|
|
1472
|
+
type: "manyToMany",
|
|
1473
|
+
target: "role",
|
|
1474
|
+
through: { table: "UserRoles", sourceKey: "userId", targetKey: "roleId" },
|
|
1475
|
+
});
|
|
1476
|
+
});
|
|
1477
|
+
const billing = defineFeature("billing", (r) => {
|
|
1478
|
+
r.entity("invoice", createEntity({ table: "Invoices", fields: {} }));
|
|
1479
|
+
r.relation("user", "invoices", {
|
|
1480
|
+
type: "hasMany",
|
|
1481
|
+
target: "invoice",
|
|
1482
|
+
foreignKey: "userId",
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
const registry = createRegistry([core, billing]);
|
|
1487
|
+
const rels = registry.getRelations("user");
|
|
1488
|
+
expect(Object.keys(rels).sort()).toEqual(["invoices", "roles"]);
|
|
1489
|
+
});
|
|
1328
1490
|
});
|
|
1329
1491
|
|
|
1330
1492
|
// --- Global Search (new tenant-based API) ---
|
|
@@ -10,8 +10,11 @@ import {
|
|
|
10
10
|
defineEntityUpdateHandler,
|
|
11
11
|
defineEntityWriteHandler,
|
|
12
12
|
defineProjectionQueryHandler,
|
|
13
|
+
type EntityCrudRegistrar,
|
|
14
|
+
registerEntityCrud,
|
|
13
15
|
} from "../entity-handlers";
|
|
14
16
|
import { createEntity, createTextField } from "../factories";
|
|
17
|
+
import type { QueryHandlerDef, WriteHandlerDef } from "../types";
|
|
15
18
|
|
|
16
19
|
const VALID_UUID = "00000000-0000-4000-8000-000000000001";
|
|
17
20
|
|
|
@@ -272,3 +275,80 @@ describe("Verb-specific entity-handler factories", () => {
|
|
|
272
275
|
expect(typeof legacyApi.handler).toBe("function");
|
|
273
276
|
});
|
|
274
277
|
});
|
|
278
|
+
|
|
279
|
+
function createCrudRegistrarMock(): {
|
|
280
|
+
readonly r: EntityCrudRegistrar;
|
|
281
|
+
readonly entities: Record<string, unknown>;
|
|
282
|
+
readonly writes: WriteHandlerDef[];
|
|
283
|
+
readonly queries: QueryHandlerDef[];
|
|
284
|
+
} {
|
|
285
|
+
const entities: Record<string, unknown> = {};
|
|
286
|
+
const writes: WriteHandlerDef[] = [];
|
|
287
|
+
const queries: QueryHandlerDef[] = [];
|
|
288
|
+
const r: EntityCrudRegistrar = {
|
|
289
|
+
entity(name, definition) {
|
|
290
|
+
entities[name] = definition;
|
|
291
|
+
},
|
|
292
|
+
writeHandler(def) {
|
|
293
|
+
writes.push(def);
|
|
294
|
+
},
|
|
295
|
+
queryHandler(def) {
|
|
296
|
+
queries.push(def);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
return { r, entities, writes, queries };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
describe("registerEntityCrud", () => {
|
|
303
|
+
test("registers entity + standard handlers (restore only when softDelete)", () => {
|
|
304
|
+
const { r, entities, writes, queries } = createCrudRegistrarMock();
|
|
305
|
+
registerEntityCrud(r, "note", noteEntitySoftDelete, {
|
|
306
|
+
write: { access: { roles: ["Admin"] } },
|
|
307
|
+
read: { access: { openToAll: true } },
|
|
308
|
+
});
|
|
309
|
+
expect(entities["note"]).toBe(noteEntitySoftDelete);
|
|
310
|
+
expect(writes.map((w) => w.name)).toEqual([
|
|
311
|
+
"note:create",
|
|
312
|
+
"note:update",
|
|
313
|
+
"note:delete",
|
|
314
|
+
"note:restore",
|
|
315
|
+
]);
|
|
316
|
+
expect(queries.map((q) => q.name)).toEqual(["note:list", "note:detail"]);
|
|
317
|
+
expect(writes[0]?.access).toEqual({ roles: ["Admin"] });
|
|
318
|
+
expect(queries[0]?.access).toEqual({ openToAll: true });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("verbs.delete: false skips delete handler", () => {
|
|
322
|
+
const { r, writes } = createCrudRegistrarMock();
|
|
323
|
+
registerEntityCrud(r, "note", noteEntity, {
|
|
324
|
+
verbs: { delete: false },
|
|
325
|
+
});
|
|
326
|
+
expect(writes.map((w) => w.name)).toEqual(["note:create", "note:update"]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("no options → handlers have no access (no implicit openToAll)", () => {
|
|
330
|
+
const { r, writes, queries } = createCrudRegistrarMock();
|
|
331
|
+
registerEntityCrud(r, "note", noteEntity);
|
|
332
|
+
for (const def of [...writes, ...queries]) {
|
|
333
|
+
expect(def.access).toBeUndefined();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("restore verb on entity without softDelete throws", () => {
|
|
338
|
+
const { r } = createCrudRegistrarMock();
|
|
339
|
+
expect(() => registerEntityCrud(r, "note", noteEntity, { verbs: { restore: true } })).toThrow(
|
|
340
|
+
/softDelete/,
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("registerEntity: false skips r.entity when already registered", () => {
|
|
345
|
+
const { r, entities, writes } = createCrudRegistrarMock();
|
|
346
|
+
r.entity("note", noteEntity);
|
|
347
|
+
registerEntityCrud(r, "note", noteEntity, {
|
|
348
|
+
registerEntity: false,
|
|
349
|
+
verbs: { update: false, delete: false, restore: false, list: false, detail: false },
|
|
350
|
+
});
|
|
351
|
+
expect(Object.keys(entities)).toEqual(["note"]);
|
|
352
|
+
expect(writes.map((w) => w.name)).toEqual(["note:create"]);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { defineFeature } from "../define-feature";
|
|
4
|
+
import type { DeclarativeEventMigration, EventUpcastCtx } from "../types";
|
|
5
|
+
|
|
6
|
+
// Transforms under test are pure — ctx is never touched.
|
|
7
|
+
const upcastCtx = {} as EventUpcastCtx;
|
|
8
|
+
|
|
9
|
+
function compile(spec: DeclarativeEventMigration) {
|
|
10
|
+
const feature = defineFeature("billing", (r) => {
|
|
11
|
+
r.defineEvent("invoicePaid", z.unknown(), {
|
|
12
|
+
version: 2,
|
|
13
|
+
migrations: [{ fromVersion: 1, toVersion: 2, transform: spec }],
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
const def = feature.eventMigrations["invoicePaid"]?.[0];
|
|
17
|
+
if (!def) throw new Error("migration not registered");
|
|
18
|
+
return (payload: unknown) => def.transform(payload, upcastCtx);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("declarative eventMigration", () => {
|
|
22
|
+
test("rename moves the value and drops the old key; missing source is a no-op", async () => {
|
|
23
|
+
const run = compile({ rename: { amount: "amountCents", missing: "other" } });
|
|
24
|
+
expect(await run({ amount: 5, currency: "EUR" })).toEqual({
|
|
25
|
+
amountCents: 5,
|
|
26
|
+
currency: "EUR",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("default fills absent keys only — existing values win", async () => {
|
|
31
|
+
const run = compile({ default: { currency: "EUR", status: "open" } });
|
|
32
|
+
expect(await run({ currency: "CHF" })).toEqual({ currency: "CHF", status: "open" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("map runs after rename on the new key; absent keys are skipped", async () => {
|
|
36
|
+
const run = compile({
|
|
37
|
+
rename: { amount: "amountCents" },
|
|
38
|
+
map: { amountCents: (v) => Math.round(Number(v) * 100), missing: () => "never" },
|
|
39
|
+
});
|
|
40
|
+
expect(await run({ amount: 19.99 })).toEqual({ amountCents: 1999 });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("non-object payload fails loud", () => {
|
|
44
|
+
const run = compile({ default: { a: 1 } });
|
|
45
|
+
expect(() => run("not-an-object")).toThrow(/object payload/);
|
|
46
|
+
expect(() => run([1, 2])).toThrow(/object payload/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("two rename sources mapping to the same target fail loud at registration", () => {
|
|
50
|
+
expect(() => compile({ rename: { amount: "total", subtotal: "total" } })).toThrow(
|
|
51
|
+
/rename collision.*"total"/,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("imperative function variant is stored untouched", () => {
|
|
56
|
+
const fn = (payload: unknown) => payload;
|
|
57
|
+
const feature = defineFeature("billing", (r) => {
|
|
58
|
+
r.defineEvent("invoicePaid", z.unknown(), {
|
|
59
|
+
version: 2,
|
|
60
|
+
migrations: [{ fromVersion: 1, toVersion: 2, transform: fn }],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
expect(feature.eventMigrations["invoicePaid"]?.[0]?.transform).toBe(fn);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Type-only fixture for the KumikoEventTypeMap declaration-merging channel
|
|
2
|
+
// (#1394). event-type-map.ts moved the marker interfaces behind a re-export
|
|
3
|
+
// chain (kumiko-types -> engine/types/event-type-map.ts shim -> engine
|
|
4
|
+
// barrel), while the codegen augmentation channel
|
|
5
|
+
// (dev-server/src/codegen/render.ts) still targets
|
|
6
|
+
// `declare module "@cosmicdrift/kumiko-framework/engine"`. If a future
|
|
7
|
+
// change breaks that re-export chain, augmentation silently stops merging
|
|
8
|
+
// and every event payload falls back to `unknown` with no compile error —
|
|
9
|
+
// this test only catches it because tsc checks the `declare module` below
|
|
10
|
+
// against the SAME specifier the codegen emits.
|
|
11
|
+
import { expectTypeOf, test } from "bun:test";
|
|
12
|
+
import type { KumikoEventTypeMap } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
|
+
|
|
14
|
+
declare module "@cosmicdrift/kumiko-framework/engine" {
|
|
15
|
+
interface KumikoEventTypeMap {
|
|
16
|
+
"event-type-map-fixture:probe.created": { readonly probe: true };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("augmenting KumikoEventTypeMap via the engine specifier merges into the relocated interface", () => {
|
|
21
|
+
expectTypeOf<KumikoEventTypeMap["event-type-map-fixture:probe.created"]>().toEqualTypeOf<{
|
|
22
|
+
readonly probe: true;
|
|
23
|
+
}>();
|
|
24
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// r.extendEntityProjection — registrar/registry contracts (#759). The
|
|
2
|
+
// runtime rebuild-replay behaviour is proven in
|
|
3
|
+
// custom-fields/__tests__/custom-fields.integration.test.ts.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { defineFeature } from "../define-feature";
|
|
8
|
+
import { createEntity, createTextField } from "../factories";
|
|
9
|
+
import { createRegistry } from "../registry";
|
|
10
|
+
|
|
11
|
+
function exampleEntity(name = "unit") {
|
|
12
|
+
return createEntity({
|
|
13
|
+
table: name,
|
|
14
|
+
fields: { name: createTextField() },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const noopApply = async (): Promise<void> => {};
|
|
19
|
+
|
|
20
|
+
describe("r.extendEntityProjection — registration", () => {
|
|
21
|
+
test("merges apply keys + extraSources into the implicit projection", () => {
|
|
22
|
+
const feature = defineFeature("test", (r) => {
|
|
23
|
+
r.entity("unit", exampleEntity());
|
|
24
|
+
const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
|
|
25
|
+
r.extendEntityProjection("unit", {
|
|
26
|
+
sources: ["field-definition"],
|
|
27
|
+
apply: { [setEvent.name]: noopApply },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
const registry = createRegistry([feature]);
|
|
31
|
+
const projection = registry.getAllProjections().get("test:projection:unit-entity");
|
|
32
|
+
expect(projection).toBeDefined();
|
|
33
|
+
expect(projection?.apply["test:event:custom-field-set"]).toBe(noopApply);
|
|
34
|
+
expect(projection?.apply["unit.created"]).toBeDefined();
|
|
35
|
+
expect(projection?.extraSources).toEqual(["field-definition"]);
|
|
36
|
+
expect(projection?.source).toBe("unit");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("extension source equal to the entity name is not duplicated into extraSources", () => {
|
|
40
|
+
const feature = defineFeature("test", (r) => {
|
|
41
|
+
r.entity("unit", exampleEntity());
|
|
42
|
+
const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
|
|
43
|
+
r.extendEntityProjection("unit", {
|
|
44
|
+
sources: ["unit"],
|
|
45
|
+
apply: { [setEvent.name]: noopApply },
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
const registry = createRegistry([feature]);
|
|
49
|
+
const projection = registry.getAllProjections().get("test:projection:unit-entity");
|
|
50
|
+
expect(projection?.extraSources).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("auto-verb of a registered extraSources entity is a valid apply-key", () => {
|
|
54
|
+
const feature = defineFeature("test", (r) => {
|
|
55
|
+
r.entity("unit", exampleEntity());
|
|
56
|
+
r.entity("field-definition", exampleEntity("field_definitions"));
|
|
57
|
+
r.extendEntityProjection("unit", {
|
|
58
|
+
sources: ["field-definition"],
|
|
59
|
+
apply: { "field-definition.deleted": noopApply },
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
const registry = createRegistry([feature]);
|
|
63
|
+
const projection = registry.getAllProjections().get("test:projection:unit-entity");
|
|
64
|
+
expect(projection?.apply["field-definition.deleted"]).toBe(noopApply);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("registration order is free: extension before r.entity in the same feature", () => {
|
|
68
|
+
const feature = defineFeature("test", (r) => {
|
|
69
|
+
const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
|
|
70
|
+
r.extendEntityProjection("unit", { apply: { [setEvent.name]: noopApply } });
|
|
71
|
+
r.entity("unit", exampleEntity());
|
|
72
|
+
});
|
|
73
|
+
const registry = createRegistry([feature]);
|
|
74
|
+
const projection = registry.getAllProjections().get("test:projection:unit-entity");
|
|
75
|
+
expect(projection?.apply["test:event:custom-field-set"]).toBe(noopApply);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("empty apply throws at registration time", () => {
|
|
79
|
+
expect(() =>
|
|
80
|
+
defineFeature("test", (r) => {
|
|
81
|
+
r.entity("unit", exampleEntity());
|
|
82
|
+
r.extendEntityProjection("unit", { apply: {} });
|
|
83
|
+
}),
|
|
84
|
+
).toThrow(/no apply handlers/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("r.extendEntityProjection — registry-build validation", () => {
|
|
89
|
+
test("unknown entity fails at registry build", () => {
|
|
90
|
+
const feature = defineFeature("test", (r) => {
|
|
91
|
+
r.entity("unit", exampleEntity());
|
|
92
|
+
const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
|
|
93
|
+
r.extendEntityProjection("typo-entity", { apply: { [setEvent.name]: noopApply } });
|
|
94
|
+
});
|
|
95
|
+
expect(() => createRegistry([feature])).toThrow(/no r\.entity/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("apply-key collision with a lifecycle apply fails at registry build", () => {
|
|
99
|
+
const feature = defineFeature("test", (r) => {
|
|
100
|
+
r.entity("unit", exampleEntity());
|
|
101
|
+
r.extendEntityProjection("unit", { apply: { "unit.created": noopApply } });
|
|
102
|
+
});
|
|
103
|
+
expect(() => createRegistry([feature])).toThrow(/collides/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("apply-key collision between two extensions fails at registry build", () => {
|
|
107
|
+
const feature = defineFeature("test", (r) => {
|
|
108
|
+
r.entity("unit", exampleEntity());
|
|
109
|
+
const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
|
|
110
|
+
r.extendEntityProjection("unit", { apply: { [setEvent.name]: noopApply } });
|
|
111
|
+
r.extendEntityProjection("unit", { apply: { [setEvent.name]: noopApply } });
|
|
112
|
+
});
|
|
113
|
+
expect(() => createRegistry([feature])).toThrow(/collides/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("unknown apply-key (neither auto-verb nor domain event) fails at registry build", () => {
|
|
117
|
+
const feature = defineFeature("test", (r) => {
|
|
118
|
+
r.entity("unit", exampleEntity());
|
|
119
|
+
r.extendEntityProjection("unit", { apply: { "customField.set": noopApply } });
|
|
120
|
+
});
|
|
121
|
+
expect(() => createRegistry([feature])).toThrow(/no such event/);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
// Unit-Tests für die neuen Time-Field-Factories
|
|
2
|
-
// (createTimestampField, createTzField,
|
|
2
|
+
// (createTimestampField, createTzField, createLocatedTimestampField).
|
|
3
3
|
//
|
|
4
4
|
// Test-Fokus: korrektes Field-Shape + locatedBy-Marker-Verdrahtung. Die
|
|
5
5
|
// echte TZ-Konvertierung (Wall-Clock ↔ UTC) testen wir später beim
|
|
6
6
|
// DB-Wrapper-Schritt.
|
|
7
7
|
|
|
8
8
|
import { describe, expect, test } from "bun:test";
|
|
9
|
-
import {
|
|
10
|
-
createLocatedTimestampField,
|
|
11
|
-
createTimestampField,
|
|
12
|
-
createTzField,
|
|
13
|
-
locatedTimestamp,
|
|
14
|
-
} from "../factories";
|
|
9
|
+
import { createLocatedTimestampField, createTimestampField, createTzField } from "../factories";
|
|
15
10
|
|
|
16
11
|
describe("createTimestampField", () => {
|
|
17
12
|
test("default-Form ist nicht-required UTC-Instant ohne locatedBy", () => {
|
|
@@ -57,65 +52,6 @@ describe("createTzField", () => {
|
|
|
57
52
|
});
|
|
58
53
|
});
|
|
59
54
|
|
|
60
|
-
describe("locatedTimestamp(name) Helper", () => {
|
|
61
|
-
test("erzeugt korrektes Pair aus <name>At + <name>Tz mit locatedBy-Verdrahtung", () => {
|
|
62
|
-
const fields = locatedTimestamp("pickup");
|
|
63
|
-
expect(fields).toEqual({
|
|
64
|
-
pickupAt: { type: "timestamp", locatedBy: "pickupTz" },
|
|
65
|
-
pickupTz: { type: "tz" },
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("required-Override propagiert auf BEIDE Felder", () => {
|
|
70
|
-
const fields = locatedTimestamp("delivery", { required: true });
|
|
71
|
-
expect(fields).toEqual({
|
|
72
|
-
deliveryAt: { type: "timestamp", locatedBy: "deliveryTz", required: true },
|
|
73
|
-
deliveryTz: { type: "tz", required: true },
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("access-Override propagiert auf BEIDE Felder (Field-Level Read-Access)", () => {
|
|
78
|
-
const fields = locatedTimestamp("internal", {
|
|
79
|
-
access: { read: ["Dispatcher"] },
|
|
80
|
-
});
|
|
81
|
-
expect(fields).toEqual({
|
|
82
|
-
internalAt: {
|
|
83
|
-
type: "timestamp",
|
|
84
|
-
locatedBy: "internalTz",
|
|
85
|
-
access: { read: ["Dispatcher"] },
|
|
86
|
-
},
|
|
87
|
-
internalTz: { type: "tz", access: { read: ["Dispatcher"] } },
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("locatedBy-Marker zeigt immer auf das EIGENE Tz-Feld (nicht auf einen anderen Namen)", () => {
|
|
92
|
-
// Das ist der Kern des Patterns — wenn die zwei Felder nicht
|
|
93
|
-
// konsistent verdrahtet sind, fliegt der Boot-Validator (kommt in
|
|
94
|
-
// späterer Iteration). Hier prüfen wir die Helper-Garantie.
|
|
95
|
-
for (const name of ["a", "x_y", "long_field_name"]) {
|
|
96
|
-
const fields = locatedTimestamp(name);
|
|
97
|
-
const at = fields[`${name}At`];
|
|
98
|
-
if (at?.type !== "timestamp") throw new Error("at field missing");
|
|
99
|
-
expect(at.locatedBy).toBe(`${name}Tz`);
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("Spread in createEntity-fields Kompositions-tauglich", () => {
|
|
104
|
-
// Realer Use-Case: pickup + delivery in einer Entity, plus normale Felder.
|
|
105
|
-
const entityFields = {
|
|
106
|
-
...locatedTimestamp("pickup"),
|
|
107
|
-
...locatedTimestamp("delivery"),
|
|
108
|
-
// Kein Konflikt zwischen den beiden Pairs.
|
|
109
|
-
};
|
|
110
|
-
expect(Object.keys(entityFields).sort()).toEqual([
|
|
111
|
-
"deliveryAt",
|
|
112
|
-
"deliveryTz",
|
|
113
|
-
"pickupAt",
|
|
114
|
-
"pickupTz",
|
|
115
|
-
]);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
55
|
describe("createLocatedTimestampField (Phase A — atomarer Field-Type)", () => {
|
|
120
56
|
test("default-Form ist nicht-required mit type 'locatedTimestamp'", () => {
|
|
121
57
|
expect(createLocatedTimestampField()).toEqual({
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { defineFeature } from "../define-feature";
|
|
3
|
+
import { createEntity, createTextField } from "../factories";
|
|
4
|
+
|
|
5
|
+
const taskEntity = createEntity({
|
|
6
|
+
table: "crud_shorthand_tasks",
|
|
7
|
+
fields: { title: createTextField({ required: true }) },
|
|
8
|
+
softDelete: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("r.crud", () => {
|
|
12
|
+
test("registers the entity + full CRUD handler set, same as registerEntityCrud", () => {
|
|
13
|
+
const write = { access: { roles: ["Admin"] } } as const;
|
|
14
|
+
const read = { access: { openToAll: true } } as const;
|
|
15
|
+
|
|
16
|
+
const feature = defineFeature("via-crud", (r) => {
|
|
17
|
+
r.crud("task", taskEntity, { write, read });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(Object.keys(feature.entities ?? {})).toEqual(["task"]);
|
|
21
|
+
expect(Object.keys(feature.writeHandlers ?? {}).sort()).toEqual(
|
|
22
|
+
["task:create", "task:delete", "task:restore", "task:update"].sort(),
|
|
23
|
+
);
|
|
24
|
+
expect(Object.keys(feature.queryHandlers ?? {}).sort()).toEqual(
|
|
25
|
+
["task:detail", "task:list"].sort(),
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { ConfigScopes } from "../constants";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildManifestFromRegistry,
|
|
5
|
+
createRegistry,
|
|
6
|
+
createSystemConfig,
|
|
7
|
+
defineFeature,
|
|
8
|
+
} from "../index";
|
|
4
9
|
|
|
5
10
|
const boolKey = {
|
|
6
11
|
type: "boolean",
|
|
@@ -104,4 +109,29 @@ describe("buildManifestFromRegistry — deterministic codepoint sort (#330)", ()
|
|
|
104
109
|
});
|
|
105
110
|
expect(noHints && "uiHints" in noHints).toBe(false);
|
|
106
111
|
});
|
|
112
|
+
|
|
113
|
+
// #1039: backing:"secrets" routes the value through the envelope-encrypted
|
|
114
|
+
// secrets store — the manifest must report encrypted:true even without an
|
|
115
|
+
// explicit `encrypted` flag, so generated docs don't mislabel Stripe-style
|
|
116
|
+
// secret-backed config keys as plaintext. (createConfigKey never emits an
|
|
117
|
+
// explicit `encrypted: false` — falsy opts.encrypted just omits the field —
|
|
118
|
+
// so there's no "explicit false wins" case to pin here.)
|
|
119
|
+
test("encrypted flag — backing:secrets implies encrypted:true without an explicit flag", () => {
|
|
120
|
+
const feature = defineFeature("demo", (r) => {
|
|
121
|
+
r.config({
|
|
122
|
+
keys: {
|
|
123
|
+
"api-key": createSystemConfig("text", { backing: "secrets" }),
|
|
124
|
+
"plain-flag": createSystemConfig("boolean", {}),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
const registry = createRegistry([feature]);
|
|
129
|
+
|
|
130
|
+
const manifest = buildManifestFromRegistry(registry, { source: "test" });
|
|
131
|
+
const demo = manifest.features.find((f) => f.name === "demo");
|
|
132
|
+
const byKey = (key: string) => demo?.configKeys.find((k) => k.key === key);
|
|
133
|
+
|
|
134
|
+
expect(byKey("api-key")?.encrypted).toBe(true);
|
|
135
|
+
expect(byKey("plain-flag")?.encrypted).toBe(false);
|
|
136
|
+
});
|
|
107
137
|
});
|