@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
|
@@ -47,6 +47,38 @@ describe("buildAppSchema", () => {
|
|
|
47
47
|
expect(fleet?.entities["vehicle"]).toBeDefined();
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
// #1059: r.translations({keys}) must flow into FeatureSchema.translations
|
|
51
|
+
// BYTE-IDENTICAL — nav/screen labels resolve these keys verbatim via
|
|
52
|
+
// t(label) client-side, so any re-prefixing (like the registry's internal
|
|
53
|
+
// mergedTranslations does for features whose keys already carry a colon)
|
|
54
|
+
// would break the lookup. Mirrors cap-counter's real shape exactly: nav
|
|
55
|
+
// label references its own already-qualified translation key.
|
|
56
|
+
test("r.translations landet verbatim in FeatureSchema.translations, ohne Re-Prefixing", () => {
|
|
57
|
+
const capCounterLikeFeature = defineFeature("cap-counter", (r) => {
|
|
58
|
+
r.nav({ id: "cap-list", label: "cap-counter:nav.cap-list" });
|
|
59
|
+
r.translations({
|
|
60
|
+
keys: {
|
|
61
|
+
"cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const app = buildAppSchema(createRegistry([capCounterLikeFeature]));
|
|
67
|
+
const feature = app.features.find((f) => f.featureName === "cap-counter");
|
|
68
|
+
|
|
69
|
+
expect(feature?.translations).toEqual({
|
|
70
|
+
"cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("Feature ohne r.translations lässt das Feld weg (omit-undefined-Pattern)", () => {
|
|
75
|
+
const f = defineFeature("bare", (r) => {
|
|
76
|
+
r.nav({ id: "x", label: "X" });
|
|
77
|
+
});
|
|
78
|
+
const app = buildAppSchema(createRegistry([f]));
|
|
79
|
+
expect(app.features[0]?.translations).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
50
82
|
test("Workspaces — definition + aufgelöste navMembers landen auf AppSchema-Ebene", () => {
|
|
51
83
|
const ordersFeature = defineFeature("orders", (r) => {
|
|
52
84
|
r.nav({ id: "list", label: "List" });
|
|
@@ -111,6 +143,35 @@ describe("buildAppSchema", () => {
|
|
|
111
143
|
expect(idField?.["type"]).toBe("text"); // type kommt durch
|
|
112
144
|
});
|
|
113
145
|
|
|
146
|
+
test("Reference-Field: entity + labelField + multiple überleben die Projection", () => {
|
|
147
|
+
// Regression: ohne diese Properties im Client-Schema baut der ReferenceInput
|
|
148
|
+
// die Options-Query als `<feature>:query::list` (leeres refEntity) → 404 →
|
|
149
|
+
// Dropdown zeigt „Keine Treffer" obwohl die referenzierte Entity Rows hat.
|
|
150
|
+
const entity = {
|
|
151
|
+
fields: {
|
|
152
|
+
name: { type: "text" },
|
|
153
|
+
parentId: { type: "reference", entity: "component", labelField: "name" },
|
|
154
|
+
tags: { type: "reference", entity: "tag", labelField: "label", multiple: true },
|
|
155
|
+
},
|
|
156
|
+
} as unknown as EntityDefinition;
|
|
157
|
+
|
|
158
|
+
const f = defineFeature("ent", (r) => {
|
|
159
|
+
r.entity("thing", entity);
|
|
160
|
+
});
|
|
161
|
+
const app = buildAppSchema(createRegistry([f]));
|
|
162
|
+
const fields = (
|
|
163
|
+
app.features[0]?.entities["thing"] as unknown as {
|
|
164
|
+
fields: Record<string, Record<string, unknown>>;
|
|
165
|
+
}
|
|
166
|
+
).fields;
|
|
167
|
+
|
|
168
|
+
expect(fields["parentId"]?.["type"]).toBe("reference");
|
|
169
|
+
expect(fields["parentId"]?.["entity"]).toBe("component");
|
|
170
|
+
expect(fields["parentId"]?.["labelField"]).toBe("name");
|
|
171
|
+
expect(fields["tags"]?.["entity"]).toBe("tag");
|
|
172
|
+
expect(fields["tags"]?.["multiple"]).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
114
175
|
test("JSON-Safety: literal Defaults bleiben erhalten", () => {
|
|
115
176
|
const entity = {
|
|
116
177
|
fields: {
|
|
@@ -155,6 +216,27 @@ describe("buildAppSchema", () => {
|
|
|
155
216
|
expect(fields["name"]?.["filterable"]).toBeUndefined();
|
|
156
217
|
});
|
|
157
218
|
|
|
219
|
+
test("searchable kommt ins Client-Schema (steuert den EntityList-Default, #1194)", () => {
|
|
220
|
+
const entity = {
|
|
221
|
+
fields: {
|
|
222
|
+
title: { type: "text", searchable: true },
|
|
223
|
+
name: { type: "text" },
|
|
224
|
+
},
|
|
225
|
+
} as unknown as EntityDefinition;
|
|
226
|
+
const f = defineFeature("ent", (r) => {
|
|
227
|
+
r.entity("thing", entity);
|
|
228
|
+
});
|
|
229
|
+
const app = buildAppSchema(createRegistry([f]));
|
|
230
|
+
const fields = (
|
|
231
|
+
app.features[0]?.entities["thing"] as unknown as {
|
|
232
|
+
fields: Record<string, Record<string, unknown>>;
|
|
233
|
+
}
|
|
234
|
+
).fields;
|
|
235
|
+
expect(fields["title"]?.["searchable"]).toBe(true);
|
|
236
|
+
// Fields without searchable don't carry the key (no false-litter).
|
|
237
|
+
expect(fields["name"]?.["searchable"]).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
|
|
158
240
|
test("AppSchema ist via JSON.stringify roundtrip-sicher", () => {
|
|
159
241
|
// Echter Smoke-Test des Vertrags — wenn jemand in den project-
|
|
160
242
|
// Helper eine Function reinschmuggelt, würde das hier brennen.
|
|
@@ -90,6 +90,9 @@ describe("buildConfigFeatureSchema — structure", () => {
|
|
|
90
90
|
expect(sys?.screen).toBeUndefined();
|
|
91
91
|
expect(sys?.parent).toBeUndefined();
|
|
92
92
|
expect(ten?.label).toBe("config.settings.tenant");
|
|
93
|
+
expect(sys?.icon).toBe("shield");
|
|
94
|
+
expect(ten?.icon).toBe("building");
|
|
95
|
+
expect(usr?.icon).toBe("user");
|
|
93
96
|
expect((sys?.order ?? 0) < (ten?.order ?? 0) && (ten?.order ?? 0) < (usr?.order ?? 0)).toBe(
|
|
94
97
|
true,
|
|
95
98
|
);
|
|
@@ -256,6 +259,27 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
|
|
|
256
259
|
expect(out.navs.find((n) => n.id === "audience-tenant")?.access).toEqual({ openToAll: true });
|
|
257
260
|
});
|
|
258
261
|
|
|
262
|
+
test("openToAll collapse holds regardless of key declaration order (516/2)", () => {
|
|
263
|
+
// The test above only ever puts the openToAll key first, so
|
|
264
|
+
// unionAccessRules's short-circuit (`if ("openToAll" in rule) return`)
|
|
265
|
+
// never actually has to walk past a role-restricted rule to prove it —
|
|
266
|
+
// `gated` here comes FIRST so a hypothetical short-circuit-on-first-only
|
|
267
|
+
// implementation would fail this one even though it passed the other.
|
|
268
|
+
const mixedWriteReversed = defineFeature("mixedwritereversed", (r) => {
|
|
269
|
+
r.config({
|
|
270
|
+
keys: {
|
|
271
|
+
gated: createTenantConfig("text", { mask: { title: "mwr.gated" } }), // default admin write
|
|
272
|
+
open: createTenantConfig("text", { write: access.all, mask: { title: "mwr.open" } }),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
const out = buildConfigFeatureSchema(createRegistry([mixedWriteReversed]));
|
|
277
|
+
const screen = out.screens.find((s) => s.id === "mixedwritereversed-tenant");
|
|
278
|
+
if (screen?.type !== "configEdit")
|
|
279
|
+
throw new Error('expected configEdit screen "mixedwritereversed-tenant"');
|
|
280
|
+
expect(screen.access).toEqual({ openToAll: true });
|
|
281
|
+
});
|
|
282
|
+
|
|
259
283
|
test("returns empty (no workspace) when no key opts into the hub via mask", () => {
|
|
260
284
|
const plain = defineFeature("plain", (r) => {
|
|
261
285
|
r.config({ keys: { secret: createSystemConfig("text", {}) } });
|
|
@@ -327,3 +351,104 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
|
|
|
327
351
|
expect([...roles].sort()).toEqual(["Admin", "SystemAdmin", "TenantAdmin"]);
|
|
328
352
|
});
|
|
329
353
|
});
|
|
354
|
+
|
|
355
|
+
describe("buildConfigFeatureSchema — cross-feature group", () => {
|
|
356
|
+
test("two features sharing a group bucket under one screen/nav per scope, not two", () => {
|
|
357
|
+
const timeTracking = defineFeature("time-tracking", (r) => {
|
|
358
|
+
r.config({
|
|
359
|
+
keys: {
|
|
360
|
+
enabled: createTenantConfig("boolean", {
|
|
361
|
+
group: "bmc-settings",
|
|
362
|
+
mask: { title: "bmc.time-tracking-enabled" },
|
|
363
|
+
}),
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
const autoProtocol = defineFeature("auto-protocol", (r) => {
|
|
368
|
+
r.config({
|
|
369
|
+
keys: {
|
|
370
|
+
enabled: createTenantConfig("boolean", {
|
|
371
|
+
group: "bmc-settings",
|
|
372
|
+
mask: { title: "bmc.auto-protocol-enabled" },
|
|
373
|
+
}),
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const out = buildConfigFeatureSchema(createRegistry([timeTracking, autoProtocol]));
|
|
379
|
+
expect(out.screens.filter((s) => s.id === "bmc-settings-tenant")).toHaveLength(1);
|
|
380
|
+
expect(out.navs.filter((n) => n.id === "bmc-settings-tenant")).toHaveLength(1);
|
|
381
|
+
|
|
382
|
+
const screen = out.screens.find((s) => s.id === "bmc-settings-tenant");
|
|
383
|
+
if (screen?.type !== "configEdit") throw new Error("expected configEdit screen");
|
|
384
|
+
// Neither feature owns the group name, so field ids are prefixed by the
|
|
385
|
+
// owning feature to stay unique — the whole point of the collision guard.
|
|
386
|
+
expect(screen.configKeys).toEqual({
|
|
387
|
+
"time-tracking-enabled": "time-tracking:config:enabled",
|
|
388
|
+
"auto-protocol-enabled": "auto-protocol:config:enabled",
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("same group + colliding owner-prefixed field id throws instead of silently overwriting", () => {
|
|
393
|
+
// "a-b" owner + shortKey "x" and "a" owner + shortKey "b-x" both produce
|
|
394
|
+
// the prefixed field id "a-b-x" — the exact clobbering hazard the
|
|
395
|
+
// owner-prefix scheme must catch, not just avoid in the common case.
|
|
396
|
+
const ab = defineFeature("a-b", (r) => {
|
|
397
|
+
r.config({
|
|
398
|
+
keys: { x: createTenantConfig("boolean", { group: "shared", mask: { title: "ab.x" } }) },
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
const a = defineFeature("a", (r) => {
|
|
402
|
+
r.config({
|
|
403
|
+
keys: {
|
|
404
|
+
bX: createTenantConfig("boolean", { group: "shared", mask: { title: "a.b-x" } }),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
expect(() => buildConfigFeatureSchema(createRegistry([ab, a]))).toThrow(
|
|
409
|
+
/both resolve to field id "a-b-x"/,
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("group unset → behavior unchanged (buckets under the owning feature, plain shortKey)", () => {
|
|
414
|
+
const feature = defineFeature("ungrouped", (r) => {
|
|
415
|
+
r.config({
|
|
416
|
+
keys: { flag: createTenantConfig("boolean", { mask: { title: "u.flag" } }) },
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
const out = buildConfigFeatureSchema(createRegistry([feature]));
|
|
420
|
+
const screen = out.screens.find((s) => s.id === "ungrouped-tenant");
|
|
421
|
+
if (screen?.type !== "configEdit") throw new Error("expected configEdit screen");
|
|
422
|
+
expect(screen.configKeys).toEqual({ flag: "ungrouped:config:flag" });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("invalid (non-kebab) group value throws at schema-build time", () => {
|
|
426
|
+
const feature = defineFeature("badgroup", (r) => {
|
|
427
|
+
r.config({
|
|
428
|
+
keys: {
|
|
429
|
+
flag: createTenantConfig("boolean", {
|
|
430
|
+
group: "Not Kebab!",
|
|
431
|
+
mask: { title: "bg.flag" },
|
|
432
|
+
}),
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
expect(() => buildConfigFeatureSchema(createRegistry([feature]))).toThrow(/kebab-case/);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("invalid (non-kebab) group value throws even on a maskless key", () => {
|
|
440
|
+
// Maskless keys never appear in the hub — group is a no-op for them at
|
|
441
|
+
// runtime — but the type contract still promises kebab-case is
|
|
442
|
+
// boot-validated regardless of `mask`. collectMaskedKeys used to check
|
|
443
|
+
// the mask-gate before the group check, so a maskless key with a bad
|
|
444
|
+
// group silently passed boot; the check now runs before the mask-gate.
|
|
445
|
+
const feature = defineFeature("badgroupmaskless", (r) => {
|
|
446
|
+
r.config({
|
|
447
|
+
keys: {
|
|
448
|
+
flag: createTenantConfig("boolean", { group: "Not Kebab!" }),
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
expect(() => buildConfigFeatureSchema(createRegistry([feature]))).toThrow(/kebab-case/);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
@@ -95,11 +95,7 @@ describe("buildTarget — Compile-Time-Safety (verified via @ts-expect-error)",
|
|
|
95
95
|
|
|
96
96
|
test("falsche args-shape wird vom Compiler abgelehnt", () => {
|
|
97
97
|
// @ts-expect-error — slug muss string sein, nicht number
|
|
98
|
-
buildTarget({
|
|
99
|
-
target: textContentStub,
|
|
100
|
-
action: "edit",
|
|
101
|
-
args: { slug: 42 },
|
|
102
|
-
});
|
|
98
|
+
buildTarget({ target: textContentStub, action: "edit", args: { slug: 42 } });
|
|
103
99
|
// Runtime: korrekt-typed args liefern valid TargetRef
|
|
104
100
|
const ref = buildTarget({
|
|
105
101
|
target: textContentStub,
|
|
@@ -110,12 +106,8 @@ describe("buildTarget — Compile-Time-Safety (verified via @ts-expect-error)",
|
|
|
110
106
|
});
|
|
111
107
|
|
|
112
108
|
test("args bei NoArgs-Action wird vom Compiler abgelehnt", () => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
action: "list",
|
|
116
|
-
// @ts-expect-error — list hat keine args, args-Feld nicht erlaubt
|
|
117
|
-
args: { x: 1 },
|
|
118
|
-
});
|
|
109
|
+
// @ts-expect-error — list hat keine args, args-Feld nicht erlaubt
|
|
110
|
+
buildTarget({ target: textContentStub, action: "list", args: { x: 1 } });
|
|
119
111
|
// Runtime: NoArgs-Action ohne args-Feld liefert valid TargetRef
|
|
120
112
|
const ref = buildTarget({ target: textContentStub, action: "list" });
|
|
121
113
|
expect("args" in ref).toBe(false);
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, it, mock } from "bun:test";
|
|
2
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
-
// Bun.Glob is only used by scanForCandidates / runCodemod
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
// Bun.Glob is only used by scanForCandidates / runCodemod. Emulate
|
|
6
|
+
// **/*.write.ts via node:fs so scan/convert paths stay testable.
|
|
7
|
+
function collectWriteTsRelPaths(rootDir: string, dir = rootDir, prefix = ""): string[] {
|
|
8
|
+
const out: string[] = [];
|
|
9
|
+
for (const name of readdirSync(dir)) {
|
|
10
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
11
|
+
const full = join(dir, name);
|
|
12
|
+
if (statSync(full).isDirectory()) {
|
|
13
|
+
out.push(...collectWriteTsRelPaths(rootDir, full, rel));
|
|
14
|
+
} else if (name.endsWith(".write.ts")) {
|
|
15
|
+
out.push(rel);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
mock.module("bun", () => {
|
|
11
|
-
class
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
class FsGlob {
|
|
23
|
+
constructor(private readonly pattern: string) {}
|
|
24
|
+
scanSync(rootDir: string): string[] {
|
|
25
|
+
if (this.pattern !== "**/*.write.ts") return [];
|
|
26
|
+
return collectWriteTsRelPaths(rootDir).sort();
|
|
14
27
|
}
|
|
15
28
|
}
|
|
16
|
-
return { Glob:
|
|
29
|
+
return { Glob: FsGlob };
|
|
17
30
|
});
|
|
18
31
|
|
|
19
32
|
import {
|
|
@@ -21,6 +34,8 @@ import {
|
|
|
21
34
|
analyzeHandlerArrow,
|
|
22
35
|
convertFile,
|
|
23
36
|
generatePerformBlock,
|
|
37
|
+
runCodemod,
|
|
38
|
+
scanForCandidates,
|
|
24
39
|
} from "../codemod/index";
|
|
25
40
|
|
|
26
41
|
const tmpDir = join(__dirname, "__codemod_fixtures__");
|
|
@@ -31,6 +46,20 @@ function writeFixture(name: string, content: string): string {
|
|
|
31
46
|
return p;
|
|
32
47
|
}
|
|
33
48
|
|
|
49
|
+
function writeFixtureIn(dir: string, name: string, content: string): string {
|
|
50
|
+
const p = join(dir, name);
|
|
51
|
+
mkdirSync(join(p, ".."), { recursive: true });
|
|
52
|
+
writeFileSync(p, content, "utf8");
|
|
53
|
+
return p;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function freshScanRoot(name: string): string {
|
|
57
|
+
const dir = join(tmpDir, name);
|
|
58
|
+
rmSync(dir, { recursive: true, force: true });
|
|
59
|
+
mkdirSync(dir, { recursive: true });
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
beforeAll(() => {
|
|
35
64
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
36
65
|
mkdirSync(tmpDir, { recursive: true });
|
|
@@ -103,14 +132,14 @@ export const myHandler = defineWriteHandler({
|
|
|
103
132
|
`;
|
|
104
133
|
|
|
105
134
|
const alreadyPipelineContent = `\
|
|
106
|
-
import { access, defineWriteHandler,
|
|
135
|
+
import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
|
|
107
136
|
import { z } from "zod";
|
|
108
137
|
|
|
109
138
|
export const myHandler = defineWriteHandler({
|
|
110
139
|
name: "test:pipeline",
|
|
111
140
|
schema: z.object({}),
|
|
112
141
|
access: { roles: access.authenticated },
|
|
113
|
-
perform:
|
|
142
|
+
perform: stepsPipeline(({ event, r }) => [
|
|
114
143
|
r.step.return((ctx) => ({ isSuccess: true, data: { ok: true } })),
|
|
115
144
|
]),
|
|
116
145
|
});
|
|
@@ -173,14 +202,14 @@ describe("analyzeFile", () => {
|
|
|
173
202
|
|
|
174
203
|
describe("convertFile", () => {
|
|
175
204
|
describe("static return handler", () => {
|
|
176
|
-
it("converts handler to perform:
|
|
205
|
+
it("converts handler to perform: stepsPipeline(...)", async () => {
|
|
177
206
|
const p = writeFixture("static-convert.write.ts", staticReturnContent);
|
|
178
207
|
const result = await convertFile(p);
|
|
179
208
|
expect(result.status).toBe("converted");
|
|
180
209
|
const content = readFileSync(p, "utf8");
|
|
181
|
-
expect(content).toContain("perform:
|
|
210
|
+
expect(content).toContain("perform: stepsPipeline");
|
|
182
211
|
expect(content).toContain("r.step.return");
|
|
183
|
-
expect(content).toContain("import { access, defineWriteHandler,
|
|
212
|
+
expect(content).toContain("import { access, defineWriteHandler, stepsPipeline }");
|
|
184
213
|
expect(content).not.toContain("handler:");
|
|
185
214
|
});
|
|
186
215
|
|
|
@@ -198,7 +227,7 @@ export const h = defineWriteHandler({
|
|
|
198
227
|
const result = await convertFile(p);
|
|
199
228
|
expect(result.status).toBe("converted");
|
|
200
229
|
const converted = readFileSync(p, "utf8");
|
|
201
|
-
expect(converted).toContain("
|
|
230
|
+
expect(converted).toContain("stepsPipeline<typeof MySchema, unknown>");
|
|
202
231
|
});
|
|
203
232
|
});
|
|
204
233
|
|
|
@@ -271,12 +300,12 @@ export const h = defineWriteHandler({
|
|
|
271
300
|
const result = await convertFile(p);
|
|
272
301
|
expect(result.status).toBe("converted");
|
|
273
302
|
const content = readFileSync(p, "utf8");
|
|
274
|
-
expect(content).toContain("import { access, defineWriteHandler,
|
|
303
|
+
expect(content).toContain("import { access, defineWriteHandler, stepsPipeline }");
|
|
275
304
|
});
|
|
276
305
|
|
|
277
306
|
it("does not duplicate pipeline import when already present", async () => {
|
|
278
307
|
const content = `\
|
|
279
|
-
import { access, defineWriteHandler,
|
|
308
|
+
import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
|
|
280
309
|
import { z } from "zod";
|
|
281
310
|
|
|
282
311
|
export const h = defineWriteHandler({
|
|
@@ -288,7 +317,9 @@ export const h = defineWriteHandler({
|
|
|
288
317
|
const result = await convertFile(p);
|
|
289
318
|
expect(result.status).toBe("converted");
|
|
290
319
|
const final = readFileSync(p, "utf8").split("\n");
|
|
291
|
-
const pipelineImports = final.filter(
|
|
320
|
+
const pipelineImports = final.filter(
|
|
321
|
+
(l) => l.includes("import") && l.includes("stepsPipeline"),
|
|
322
|
+
);
|
|
292
323
|
// Only one import line should contain "pipeline"
|
|
293
324
|
expect(pipelineImports.length).toBe(1);
|
|
294
325
|
});
|
|
@@ -397,6 +428,31 @@ export const h = defineWriteHandler({
|
|
|
397
428
|
// ts-morph should handle this gracefully (parse error → no call found)
|
|
398
429
|
expect(result.status).toBe("skipped");
|
|
399
430
|
});
|
|
431
|
+
|
|
432
|
+
it("returns error when the path is not a readable file", async () => {
|
|
433
|
+
const dirPath = join(tmpDir, "dir-not-file.write.ts");
|
|
434
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
435
|
+
mkdirSync(dirPath, { recursive: true });
|
|
436
|
+
const result = await convertFile(dirPath);
|
|
437
|
+
expect(result.status).toBe("error");
|
|
438
|
+
expect(result.reason).toBeDefined();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("verbose logs when skipping a non-trivial handler", async () => {
|
|
442
|
+
const p = writeFixture("verbose-skip-file.write.ts", complexContent);
|
|
443
|
+
const logs: string[] = [];
|
|
444
|
+
const origLog = console.log;
|
|
445
|
+
console.log = (...args: unknown[]) => {
|
|
446
|
+
logs.push(args.map(String).join(" "));
|
|
447
|
+
};
|
|
448
|
+
try {
|
|
449
|
+
const result = await convertFile(p, undefined, { verbose: true });
|
|
450
|
+
expect(result.status).toBe("skipped");
|
|
451
|
+
expect(logs.some((l) => l.includes("non-trivial handler"))).toBe(true);
|
|
452
|
+
} finally {
|
|
453
|
+
console.log = origLog;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
400
456
|
});
|
|
401
457
|
});
|
|
402
458
|
|
|
@@ -507,20 +563,95 @@ describe("analyzeHandlerArrow", () => {
|
|
|
507
563
|
expect(result.isGuardedCreate).toBe(true);
|
|
508
564
|
expect(result.isSimpleExecutorCreate).toBe(false);
|
|
509
565
|
});
|
|
566
|
+
|
|
567
|
+
it("returns non-convertible defaults for async ( without =>", () => {
|
|
568
|
+
const result = analyzeHandlerArrow("async (event, ctx) { return { isSuccess: true } }");
|
|
569
|
+
expect(result.isStaticReturn).toBe(false);
|
|
570
|
+
expect(result.isSimpleExecutorCreate).toBe(false);
|
|
571
|
+
expect(result.isSimpleExecutorUpdate).toBe(false);
|
|
572
|
+
expect(result.hasConditionalLogic).toBe(false);
|
|
573
|
+
expect(result.isExpressionBodyCreate).toBe(false);
|
|
574
|
+
expect(result.isGuardedCreate).toBe(false);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
describe("scanForCandidates", () => {
|
|
579
|
+
it("returns empty list when no *.write.ts files exist", () => {
|
|
580
|
+
const root = freshScanRoot("scan-empty");
|
|
581
|
+
expect(scanForCandidates(root)).toEqual([]);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("finds and classifies *.write.ts fixtures under rootDir", () => {
|
|
585
|
+
const root = freshScanRoot("scan-mixed");
|
|
586
|
+
writeFixtureIn(root, "freeform.write.ts", staticReturnContent);
|
|
587
|
+
writeFixtureIn(root, "already-pipeline.write.ts", alreadyPipelineContent);
|
|
588
|
+
writeFixtureIn(root, "nested/deep.write.ts", queryHandlerContent);
|
|
589
|
+
|
|
590
|
+
const results = scanForCandidates(root);
|
|
591
|
+
expect(results.map((r) => r.filePath).sort()).toEqual(
|
|
592
|
+
[
|
|
593
|
+
join(root, "already-pipeline.write.ts"),
|
|
594
|
+
join(root, "freeform.write.ts"),
|
|
595
|
+
join(root, "nested/deep.write.ts"),
|
|
596
|
+
].sort(),
|
|
597
|
+
);
|
|
598
|
+
expect(results.find((r) => r.pattern === "free-form-write")?.convertible).toBe(true);
|
|
599
|
+
expect(results.find((r) => r.pattern === "pipeline-write")?.convertible).toBe(false);
|
|
600
|
+
expect(results.find((r) => r.pattern === "query-handler")?.convertible).toBe(false);
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
describe("runCodemod", () => {
|
|
605
|
+
it("returns empty report when scan finds no convertible handlers", async () => {
|
|
606
|
+
const root = freshScanRoot("run-empty");
|
|
607
|
+
const report = await runCodemod(root);
|
|
608
|
+
expect(report).toEqual({ results: [], converted: 0, skipped: 0, errors: 0 });
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("converts free-form write handlers discovered by scan", async () => {
|
|
612
|
+
const root = freshScanRoot("run-convert");
|
|
613
|
+
const p = writeFixtureIn(root, "to-convert.write.ts", staticReturnContent);
|
|
614
|
+
const report = await runCodemod(root);
|
|
615
|
+
expect(report.converted).toBe(1);
|
|
616
|
+
expect(report.skipped).toBe(0);
|
|
617
|
+
expect(report.errors).toBe(0);
|
|
618
|
+
expect(readFileSync(p, "utf8")).toContain("perform: stepsPipeline");
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("verbose logs per-file outcomes", async () => {
|
|
622
|
+
const root = freshScanRoot("run-verbose");
|
|
623
|
+
writeFixtureIn(root, "verbose-convert.write.ts", staticReturnContent);
|
|
624
|
+
writeFixtureIn(root, "verbose-skip.write.ts", complexContent);
|
|
625
|
+
|
|
626
|
+
const logs: string[] = [];
|
|
627
|
+
const origLog = console.log;
|
|
628
|
+
console.log = (...args: unknown[]) => {
|
|
629
|
+
logs.push(args.map(String).join(" "));
|
|
630
|
+
};
|
|
631
|
+
try {
|
|
632
|
+
const report = await runCodemod(root, { verbose: true });
|
|
633
|
+
expect(report.converted).toBe(1);
|
|
634
|
+
expect(report.skipped).toBe(1);
|
|
635
|
+
expect(logs.some((l) => l.includes("verbose-convert.write.ts"))).toBe(true);
|
|
636
|
+
expect(logs.some((l) => l.includes("verbose-skip.write.ts"))).toBe(true);
|
|
637
|
+
} finally {
|
|
638
|
+
console.log = origLog;
|
|
639
|
+
}
|
|
640
|
+
});
|
|
510
641
|
});
|
|
511
642
|
|
|
512
643
|
describe("generatePerformBlock", () => {
|
|
513
644
|
it("generates pipeline block for static return", () => {
|
|
514
645
|
const analysis = analyzeHandlerArrow("async () => ({ isSuccess: true, data: { ok: true } })");
|
|
515
646
|
const block = generatePerformBlock(analysis, "", " ");
|
|
516
|
-
expect(block).toContain("perform:
|
|
647
|
+
expect(block).toContain("perform: stepsPipeline(");
|
|
517
648
|
expect(block).toContain("r.step.return((ctx) => ({ isSuccess: true, data: { ok: true } })");
|
|
518
649
|
});
|
|
519
650
|
|
|
520
651
|
it("generates pipeline block with schema type parameter", () => {
|
|
521
652
|
const analysis = analyzeHandlerArrow("async () => ({ isSuccess: true, data: { ok: true } })");
|
|
522
653
|
const block = generatePerformBlock(analysis, "typeof InvoiceSchema", " ");
|
|
523
|
-
expect(block).toContain("
|
|
654
|
+
expect(block).toContain("stepsPipeline<typeof InvoiceSchema, unknown>");
|
|
524
655
|
});
|
|
525
656
|
|
|
526
657
|
it("returns null for non-convertible analysis", () => {
|
|
@@ -257,6 +257,22 @@ describe("config helpers — allowPerRequest opt-in", () => {
|
|
|
257
257
|
});
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
+
describe("config helpers — group (Settings-Hub namespace override)", () => {
|
|
261
|
+
test("group is carried when set", () => {
|
|
262
|
+
const key = createTenantConfig("boolean", { group: "tenant-settings" });
|
|
263
|
+
expect(key.group).toBe("tenant-settings");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("no group → field absent (defaults to the owning feature)", () => {
|
|
267
|
+
expect(createTenantConfig("boolean").group).toBeUndefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("group is available on every scope factory", () => {
|
|
271
|
+
expect(createSystemConfig("text", { group: "shared" }).group).toBe("shared");
|
|
272
|
+
expect(createUserConfig("text", { group: "shared" }).group).toBe("shared");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
260
276
|
describe("config helpers — computed (plan-based / derived values)", () => {
|
|
261
277
|
test("computed function attaches to the definition and returns the typed value", async () => {
|
|
262
278
|
const key = createTenantConfig("number", {
|
|
@@ -29,8 +29,14 @@ describe("defineFeature — bare CRUD verb → entity mapping", () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
test("feature-name match wins (the fallback is the secondary path)", () => {
|
|
32
|
+
// Two entities (516/1): with only "note" registered, this would also
|
|
33
|
+
// pass via the single-entity fallback, proving nothing about the
|
|
34
|
+
// name-match branch specifically. A second entity ("tag") disables the
|
|
35
|
+
// fallback (entityKeys.length !== 1), so success here can only come
|
|
36
|
+
// from entities[name] matching — the branch this test claims to cover.
|
|
32
37
|
const f = defineFeature("note", (r) => {
|
|
33
38
|
r.entity("note", noteEntity);
|
|
39
|
+
r.entity("tag", tagEntity);
|
|
34
40
|
r.writeHandler(bareCreate);
|
|
35
41
|
});
|
|
36
42
|
expect(f.handlerEntityMappings?.["create"]).toBe("note");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { defineRoles } from "../define-roles";
|
|
3
|
+
|
|
4
|
+
describe("defineRoles", () => {
|
|
5
|
+
test("maps each role name to itself", () => {
|
|
6
|
+
const roles = defineRoles(["Admin", "SystemAdmin", "Driver"] as const);
|
|
7
|
+
expect(roles.Admin).toBe("Admin");
|
|
8
|
+
expect(roles.SystemAdmin).toBe("SystemAdmin");
|
|
9
|
+
expect(roles.Driver).toBe("Driver");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("returns an object with exactly the given keys", () => {
|
|
13
|
+
const roles = defineRoles(["A", "B"] as const);
|
|
14
|
+
expect(Object.keys(roles).sort()).toEqual(["A", "B"]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("an empty role list returns an empty object", () => {
|
|
18
|
+
const roles = defineRoles([] as const);
|
|
19
|
+
expect(roles).toEqual({});
|
|
20
|
+
});
|
|
21
|
+
});
|