@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
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
// Designer/AI know the call exists.
|
|
12
12
|
|
|
13
13
|
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import { resolve } from "node:path";
|
|
14
15
|
import { Project } from "ts-morph";
|
|
15
|
-
import {
|
|
16
|
+
import { isRawRefSentinel, readDataLiteralNode } from "../extractors/shared";
|
|
17
|
+
import { parseFeatureFile, parseSourceFile } from "../parse";
|
|
16
18
|
|
|
17
19
|
function createProject() {
|
|
18
20
|
return new Project({
|
|
@@ -122,6 +124,102 @@ defineFeature("isolated", (r) => {
|
|
|
122
124
|
expect(result.featureName).toBe("nameOnly");
|
|
123
125
|
expect(result.patterns).toEqual([]);
|
|
124
126
|
});
|
|
127
|
+
|
|
128
|
+
test("recurses into a locally declared registrar-wrapper function", () => {
|
|
129
|
+
const result = parseInline(`
|
|
130
|
+
function registerTaskScreens(registrar) {
|
|
131
|
+
registrar.entity("task", { fields: {} });
|
|
132
|
+
registrar.requires("auth");
|
|
133
|
+
}
|
|
134
|
+
defineFeature("wrapped", (r) => {
|
|
135
|
+
r.systemScope();
|
|
136
|
+
registerTaskScreens(r);
|
|
137
|
+
});
|
|
138
|
+
`);
|
|
139
|
+
|
|
140
|
+
expect(result.errors).toEqual([]);
|
|
141
|
+
expect(result.patterns.map((p) => p.kind)).toEqual(["systemScope", "entity", "requires"]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("recurses into a registrar-wrapper declared as a const arrow function", () => {
|
|
145
|
+
const result = parseInline(`
|
|
146
|
+
const registerTaskScreens = (registrar) => {
|
|
147
|
+
registrar.entity("task", { fields: {} });
|
|
148
|
+
};
|
|
149
|
+
defineFeature("wrapped", (r) => {
|
|
150
|
+
registerTaskScreens(r);
|
|
151
|
+
});
|
|
152
|
+
`);
|
|
153
|
+
|
|
154
|
+
expect(result.errors).toEqual([]);
|
|
155
|
+
expect(result.patterns).toMatchObject([{ kind: "entity", entityName: "task" }]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("ignores a bare call that does not receive the registrar as an argument", () => {
|
|
159
|
+
const result = parseInline(`
|
|
160
|
+
function unrelated() {
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
defineFeature("f", (r) => {
|
|
164
|
+
unrelated();
|
|
165
|
+
r.systemScope();
|
|
166
|
+
});
|
|
167
|
+
`);
|
|
168
|
+
|
|
169
|
+
expect(result.errors).toEqual([]);
|
|
170
|
+
expect(result.patterns).toMatchObject([{ kind: "systemScope" }]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("does not infinite-loop on a registrar-wrapper cycle", () => {
|
|
174
|
+
const result = parseInline(`
|
|
175
|
+
function a(registrar) {
|
|
176
|
+
registrar.systemScope();
|
|
177
|
+
b(registrar);
|
|
178
|
+
}
|
|
179
|
+
function b(registrar) {
|
|
180
|
+
a(registrar);
|
|
181
|
+
}
|
|
182
|
+
defineFeature("f", (r) => {
|
|
183
|
+
a(r);
|
|
184
|
+
});
|
|
185
|
+
`);
|
|
186
|
+
|
|
187
|
+
expect(result.errors).toEqual([]);
|
|
188
|
+
expect(result.patterns).toMatchObject([{ kind: "systemScope" }]);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("readDataLiteralNode — raw-ref sentinel", () => {
|
|
193
|
+
function readExpression(source: string) {
|
|
194
|
+
const project = createProject();
|
|
195
|
+
const sourceFile = project.createSourceFile("inline.ts", `const __probe__ = (${source});`);
|
|
196
|
+
const decl = sourceFile.getVariableDeclarationOrThrow("__probe__");
|
|
197
|
+
return readDataLiteralNode(decl.getInitializerOrThrow());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
test("Identifier reference resolves to a raw-ref sentinel with the exact source text", () => {
|
|
201
|
+
const value = readExpression("eventEntity");
|
|
202
|
+
expect(isRawRefSentinel(value)).toBe(true);
|
|
203
|
+
expect(value).toEqual({ __raw: "eventEntity" });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("zero-arg call expression resolves to a raw-ref sentinel with the exact source text", () => {
|
|
207
|
+
const value = readExpression("createInviteBrandingQuery()");
|
|
208
|
+
expect(isRawRefSentinel(value)).toBe(true);
|
|
209
|
+
expect(value).toEqual({ __raw: "createInviteBrandingQuery()" });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("member access resolves to a raw-ref sentinel", () => {
|
|
213
|
+
const value = readExpression("config.timeout");
|
|
214
|
+
expect(isRawRefSentinel(value)).toBe(true);
|
|
215
|
+
expect(value).toEqual({ __raw: "config.timeout" });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a single unresolvable property no longer bubbles the whole object to undefined", () => {
|
|
219
|
+
const value = readExpression("{ timeout: 30, query: createQuery() }");
|
|
220
|
+
expect(isRawRefSentinel(value)).toBe(false);
|
|
221
|
+
expect(value).toEqual({ timeout: 30, query: { __raw: "createQuery()" } });
|
|
222
|
+
});
|
|
125
223
|
});
|
|
126
224
|
|
|
127
225
|
// =============================================================================
|
|
@@ -143,12 +241,26 @@ defineFeature("f", (r) => {
|
|
|
143
241
|
expect(result.errors).toEqual([]);
|
|
144
242
|
});
|
|
145
243
|
|
|
146
|
-
test("
|
|
244
|
+
test("resolves a local const identifier arg via the raw-ref sentinel (#1009)", () => {
|
|
147
245
|
const result = parseInline(`
|
|
148
246
|
const dep = "auth";
|
|
149
247
|
defineFeature("f", (r) => {
|
|
150
248
|
r.requires(dep);
|
|
151
249
|
});
|
|
250
|
+
`);
|
|
251
|
+
|
|
252
|
+
expect(result.patterns[0]).toMatchObject({
|
|
253
|
+
kind: "requires",
|
|
254
|
+
featureNames: [{ __raw: "dep" }],
|
|
255
|
+
});
|
|
256
|
+
expect(result.errors).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("emits a ParseError when an argument cannot be resolved at all", () => {
|
|
260
|
+
const result = parseInline(`
|
|
261
|
+
defineFeature("f", (r) => {
|
|
262
|
+
r.requires(5);
|
|
263
|
+
});
|
|
152
264
|
`);
|
|
153
265
|
|
|
154
266
|
expect(result.patterns).toEqual([]);
|
|
@@ -170,6 +282,21 @@ defineFeature("f", (r) => {
|
|
|
170
282
|
featureNames: ["billing"],
|
|
171
283
|
});
|
|
172
284
|
});
|
|
285
|
+
|
|
286
|
+
test("keeps a computed feature-name arg as a raw-ref sentinel instead of failing (#1009)", () => {
|
|
287
|
+
const result = parseInline(`
|
|
288
|
+
const someFeature = { name: "billing" };
|
|
289
|
+
defineFeature("f", (r) => {
|
|
290
|
+
r.optionalRequires(someFeature.name);
|
|
291
|
+
});
|
|
292
|
+
`);
|
|
293
|
+
|
|
294
|
+
expect(result.patterns[0]).toMatchObject({
|
|
295
|
+
kind: "optionalRequires",
|
|
296
|
+
featureNames: [{ __raw: "someFeature.name" }],
|
|
297
|
+
});
|
|
298
|
+
expect(result.errors).toEqual([]);
|
|
299
|
+
});
|
|
173
300
|
});
|
|
174
301
|
|
|
175
302
|
describe("extractReadsConfig", () => {
|
|
@@ -185,6 +312,21 @@ defineFeature("f", (r) => {
|
|
|
185
312
|
qualifiedKeys: ["auth:config:jwt-ttl", "tenant:config:locale"],
|
|
186
313
|
});
|
|
187
314
|
});
|
|
315
|
+
|
|
316
|
+
test("keeps a computed key arg as a raw-ref sentinel instead of failing (#1009)", () => {
|
|
317
|
+
const result = parseInline(`
|
|
318
|
+
const cfg = { key: "auth:config:jwt-ttl" };
|
|
319
|
+
defineFeature("f", (r) => {
|
|
320
|
+
r.readsConfig(cfg.key, "tenant:config:locale");
|
|
321
|
+
});
|
|
322
|
+
`);
|
|
323
|
+
|
|
324
|
+
expect(result.patterns[0]).toMatchObject({
|
|
325
|
+
kind: "readsConfig",
|
|
326
|
+
qualifiedKeys: [{ __raw: "cfg.key" }, "tenant:config:locale"],
|
|
327
|
+
});
|
|
328
|
+
expect(result.errors).toEqual([]);
|
|
329
|
+
});
|
|
188
330
|
});
|
|
189
331
|
|
|
190
332
|
describe("extractSystemScope", () => {
|
|
@@ -269,6 +411,40 @@ defineFeature("f", (r) => {
|
|
|
269
411
|
expect(result.errors).toEqual([]);
|
|
270
412
|
});
|
|
271
413
|
|
|
414
|
+
test("keeps an unresolved const reference as a raw-ref sentinel instead of erroring", () => {
|
|
415
|
+
const result = parseInline(`
|
|
416
|
+
const eventEntity = { fields: { name: { type: "text" } } };
|
|
417
|
+
defineFeature("f", (r) => {
|
|
418
|
+
r.entity("event", eventEntity);
|
|
419
|
+
});
|
|
420
|
+
`);
|
|
421
|
+
|
|
422
|
+
expect(result.errors).toEqual([]);
|
|
423
|
+
expect(result.patterns[0]).toMatchObject({
|
|
424
|
+
kind: "entity",
|
|
425
|
+
entityName: "event",
|
|
426
|
+
definition: { __raw: "eventEntity" },
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("keeps a factory call reference inside a nested property as a raw-ref sentinel", () => {
|
|
431
|
+
const result = parseInline(`
|
|
432
|
+
function buildFields() {
|
|
433
|
+
return { name: { type: "text" } };
|
|
434
|
+
}
|
|
435
|
+
defineFeature("f", (r) => {
|
|
436
|
+
r.entity("task", { fields: buildFields() });
|
|
437
|
+
});
|
|
438
|
+
`);
|
|
439
|
+
|
|
440
|
+
expect(result.errors).toEqual([]);
|
|
441
|
+
expect(result.patterns[0]).toMatchObject({
|
|
442
|
+
kind: "entity",
|
|
443
|
+
entityName: "task",
|
|
444
|
+
definition: { fields: { __raw: "buildFields()" } },
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
272
448
|
test("walks through `as const` and `satisfies` wrappers", () => {
|
|
273
449
|
const result = parseInline(`
|
|
274
450
|
defineFeature("f", (r) => {
|
|
@@ -303,6 +479,60 @@ const ENTITY = "task";
|
|
|
303
479
|
defineFeature("f", (r) => {
|
|
304
480
|
r.entity(ENTITY, { fields: {} });
|
|
305
481
|
});
|
|
482
|
+
`);
|
|
483
|
+
|
|
484
|
+
expect(result.errors[0]?.methodName).toBe("entity");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("object form captures name + definition", () => {
|
|
488
|
+
const result = parseInline(`
|
|
489
|
+
defineFeature("f", (r) => {
|
|
490
|
+
r.entity({ name: "task", fields: { title: { type: "text" } } });
|
|
491
|
+
});
|
|
492
|
+
`);
|
|
493
|
+
|
|
494
|
+
expect(result.patterns[0]).toMatchObject({
|
|
495
|
+
kind: "entity",
|
|
496
|
+
entityName: "task",
|
|
497
|
+
definition: { fields: { title: { type: "text" } } },
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("emits ParseError when called with no arguments", () => {
|
|
502
|
+
const result = parseInline(`
|
|
503
|
+
defineFeature("f", (r) => {
|
|
504
|
+
r.entity();
|
|
505
|
+
});
|
|
506
|
+
`);
|
|
507
|
+
|
|
508
|
+
expect(result.errors[0]?.methodName).toBe("entity");
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("emits ParseError when object form is missing the name property", () => {
|
|
512
|
+
const result = parseInline(`
|
|
513
|
+
defineFeature("f", (r) => {
|
|
514
|
+
r.entity({ fields: {} });
|
|
515
|
+
});
|
|
516
|
+
`);
|
|
517
|
+
|
|
518
|
+
expect(result.errors[0]?.methodName).toBe("entity");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("emits ParseError when object form definition contains a function value", () => {
|
|
522
|
+
const result = parseInline(`
|
|
523
|
+
defineFeature("f", (r) => {
|
|
524
|
+
r.entity({ name: "task", fields: { title: { type: "text", default: () => "x" } } });
|
|
525
|
+
});
|
|
526
|
+
`);
|
|
527
|
+
|
|
528
|
+
expect(result.errors[0]?.methodName).toBe("entity");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("emits ParseError when the positional definition argument is missing", () => {
|
|
532
|
+
const result = parseInline(`
|
|
533
|
+
defineFeature("f", (r) => {
|
|
534
|
+
r.entity("task");
|
|
535
|
+
});
|
|
306
536
|
`);
|
|
307
537
|
|
|
308
538
|
expect(result.errors[0]?.methodName).toBe("entity");
|
|
@@ -345,6 +575,71 @@ const taskRef = { name: "task" };
|
|
|
345
575
|
defineFeature("f", (r) => {
|
|
346
576
|
r.relation(taskRef, "owner", { kind: "belongsTo", to: "user" });
|
|
347
577
|
});
|
|
578
|
+
`);
|
|
579
|
+
|
|
580
|
+
expect(result.errors[0]?.methodName).toBe("relation");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("object form captures entity, name and definition", () => {
|
|
584
|
+
const result = parseInline(`
|
|
585
|
+
defineFeature("f", (r) => {
|
|
586
|
+
r.relation({ entity: "task", name: "owner", kind: "belongsTo", to: "user" });
|
|
587
|
+
});
|
|
588
|
+
`);
|
|
589
|
+
|
|
590
|
+
expect(result.patterns[0]).toMatchObject({
|
|
591
|
+
kind: "relation",
|
|
592
|
+
entityName: "task",
|
|
593
|
+
relationName: "owner",
|
|
594
|
+
definition: { kind: "belongsTo", to: "user" },
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("emits ParseError when called with no arguments", () => {
|
|
599
|
+
const result = parseInline(`
|
|
600
|
+
defineFeature("f", (r) => {
|
|
601
|
+
r.relation();
|
|
602
|
+
});
|
|
603
|
+
`);
|
|
604
|
+
|
|
605
|
+
expect(result.errors[0]?.methodName).toBe("relation");
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("emits ParseError when object form is missing the entity property", () => {
|
|
609
|
+
const result = parseInline(`
|
|
610
|
+
defineFeature("f", (r) => {
|
|
611
|
+
r.relation({ name: "owner", kind: "belongsTo" });
|
|
612
|
+
});
|
|
613
|
+
`);
|
|
614
|
+
|
|
615
|
+
expect(result.errors[0]?.methodName).toBe("relation");
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("emits ParseError when object form is missing the name property", () => {
|
|
619
|
+
const result = parseInline(`
|
|
620
|
+
defineFeature("f", (r) => {
|
|
621
|
+
r.relation({ entity: "task", kind: "belongsTo" });
|
|
622
|
+
});
|
|
623
|
+
`);
|
|
624
|
+
|
|
625
|
+
expect(result.errors[0]?.methodName).toBe("relation");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("emits ParseError when the relation name is not a string literal", () => {
|
|
629
|
+
const result = parseInline(`
|
|
630
|
+
defineFeature("f", (r) => {
|
|
631
|
+
r.relation("task", REL, { kind: "belongsTo" });
|
|
632
|
+
});
|
|
633
|
+
`);
|
|
634
|
+
|
|
635
|
+
expect(result.errors[0]?.methodName).toBe("relation");
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("emits ParseError when the positional definition argument is missing", () => {
|
|
639
|
+
const result = parseInline(`
|
|
640
|
+
defineFeature("f", (r) => {
|
|
641
|
+
r.relation("task", "owner");
|
|
642
|
+
});
|
|
348
643
|
`);
|
|
349
644
|
|
|
350
645
|
expect(result.errors[0]?.methodName).toBe("relation");
|
|
@@ -364,6 +659,26 @@ defineFeature("f", (r) => {
|
|
|
364
659
|
definition: { id: "tasks", label: "Tasks", screen: "tasks:screen:list" },
|
|
365
660
|
});
|
|
366
661
|
});
|
|
662
|
+
|
|
663
|
+
test("emits ParseError when called with no arguments", () => {
|
|
664
|
+
const result = parseInline(`
|
|
665
|
+
defineFeature("f", (r) => {
|
|
666
|
+
r.nav();
|
|
667
|
+
});
|
|
668
|
+
`);
|
|
669
|
+
|
|
670
|
+
expect(result.errors[0]?.methodName).toBe("nav");
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("emits ParseError when the definition is not a plain object", () => {
|
|
674
|
+
const result = parseInline(`
|
|
675
|
+
defineFeature("f", (r) => {
|
|
676
|
+
r.nav(() => {});
|
|
677
|
+
});
|
|
678
|
+
`);
|
|
679
|
+
|
|
680
|
+
expect(result.errors[0]?.methodName).toBe("nav");
|
|
681
|
+
});
|
|
367
682
|
});
|
|
368
683
|
|
|
369
684
|
describe("extractWorkspace", () => {
|
|
@@ -379,6 +694,16 @@ defineFeature("f", (r) => {
|
|
|
379
694
|
definition: { id: "admin", label: "Admin" },
|
|
380
695
|
});
|
|
381
696
|
});
|
|
697
|
+
|
|
698
|
+
test("emits ParseError when called with no arguments", () => {
|
|
699
|
+
const result = parseInline(`
|
|
700
|
+
defineFeature("f", (r) => {
|
|
701
|
+
r.workspace();
|
|
702
|
+
});
|
|
703
|
+
`);
|
|
704
|
+
|
|
705
|
+
expect(result.errors[0]?.methodName).toBe("workspace");
|
|
706
|
+
});
|
|
382
707
|
});
|
|
383
708
|
|
|
384
709
|
// =============================================================================
|
|
@@ -412,6 +737,26 @@ defineFeature("f", (r) => {
|
|
|
412
737
|
defineFeature("f", (r) => {
|
|
413
738
|
r.config({});
|
|
414
739
|
});
|
|
740
|
+
`);
|
|
741
|
+
|
|
742
|
+
expect(result.errors[0]?.methodName).toBe("config");
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("emits ParseError when called with no arguments", () => {
|
|
746
|
+
const result = parseInline(`
|
|
747
|
+
defineFeature("f", (r) => {
|
|
748
|
+
r.config();
|
|
749
|
+
});
|
|
750
|
+
`);
|
|
751
|
+
|
|
752
|
+
expect(result.errors[0]?.methodName).toBe("config");
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test("emits ParseError when the argument is not a plain object", () => {
|
|
756
|
+
const result = parseInline(`
|
|
757
|
+
defineFeature("f", (r) => {
|
|
758
|
+
r.config(() => {});
|
|
759
|
+
});
|
|
415
760
|
`);
|
|
416
761
|
|
|
417
762
|
expect(result.errors[0]?.methodName).toBe("config");
|
|
@@ -439,6 +784,26 @@ defineFeature("f", (r) => {
|
|
|
439
784
|
},
|
|
440
785
|
});
|
|
441
786
|
});
|
|
787
|
+
|
|
788
|
+
test("emits ParseError when called with no arguments", () => {
|
|
789
|
+
const result = parseInline(`
|
|
790
|
+
defineFeature("f", (r) => {
|
|
791
|
+
r.translations();
|
|
792
|
+
});
|
|
793
|
+
`);
|
|
794
|
+
|
|
795
|
+
expect(result.errors[0]?.methodName).toBe("translations");
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("emits ParseError when keys property is missing", () => {
|
|
799
|
+
const result = parseInline(`
|
|
800
|
+
defineFeature("f", (r) => {
|
|
801
|
+
r.translations({});
|
|
802
|
+
});
|
|
803
|
+
`);
|
|
804
|
+
|
|
805
|
+
expect(result.errors[0]?.methodName).toBe("translations");
|
|
806
|
+
});
|
|
442
807
|
});
|
|
443
808
|
|
|
444
809
|
describe("extractMetric", () => {
|
|
@@ -455,336 +820,1372 @@ defineFeature("f", (r) => {
|
|
|
455
820
|
options: { type: "counter", description: "API requests" },
|
|
456
821
|
});
|
|
457
822
|
});
|
|
458
|
-
|
|
823
|
+
|
|
824
|
+
test("object form captures name + options", () => {
|
|
825
|
+
const result = parseInline(`
|
|
826
|
+
defineFeature("f", (r) => {
|
|
827
|
+
r.metric({ name: "requests", type: "counter", description: "API requests" });
|
|
828
|
+
});
|
|
829
|
+
`);
|
|
830
|
+
|
|
831
|
+
expect(result.patterns[0]).toMatchObject({
|
|
832
|
+
kind: "metric",
|
|
833
|
+
shortName: "requests",
|
|
834
|
+
options: { type: "counter", description: "API requests" },
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test("emits ParseError when called with no arguments", () => {
|
|
839
|
+
const result = parseInline(`
|
|
840
|
+
defineFeature("f", (r) => {
|
|
841
|
+
r.metric();
|
|
842
|
+
});
|
|
843
|
+
`);
|
|
844
|
+
|
|
845
|
+
expect(result.errors[0]?.methodName).toBe("metric");
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("emits ParseError when object form is missing the name property", () => {
|
|
849
|
+
const result = parseInline(`
|
|
850
|
+
defineFeature("f", (r) => {
|
|
851
|
+
r.metric({ type: "counter" });
|
|
852
|
+
});
|
|
853
|
+
`);
|
|
854
|
+
|
|
855
|
+
expect(result.errors[0]?.methodName).toBe("metric");
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("emits ParseError when the name is not a string literal", () => {
|
|
859
|
+
const result = parseInline(`
|
|
860
|
+
defineFeature("f", (r) => {
|
|
861
|
+
r.metric(NAME, { type: "counter" });
|
|
862
|
+
});
|
|
863
|
+
`);
|
|
864
|
+
|
|
865
|
+
expect(result.errors[0]?.methodName).toBe("metric");
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
test("emits ParseError when the options argument is missing", () => {
|
|
869
|
+
const result = parseInline(`
|
|
870
|
+
defineFeature("f", (r) => {
|
|
871
|
+
r.metric("requests");
|
|
872
|
+
});
|
|
873
|
+
`);
|
|
874
|
+
|
|
875
|
+
expect(result.errors[0]?.methodName).toBe("metric");
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("emits ParseError when options is not a plain object", () => {
|
|
879
|
+
const result = parseInline(`
|
|
880
|
+
defineFeature("f", (r) => {
|
|
881
|
+
r.metric("requests", () => {});
|
|
882
|
+
});
|
|
883
|
+
`);
|
|
884
|
+
|
|
885
|
+
expect(result.errors[0]?.methodName).toBe("metric");
|
|
886
|
+
});
|
|
887
|
+
});
|
|
459
888
|
|
|
460
889
|
describe("extractSecret", () => {
|
|
461
890
|
test("captures shortName + options", () => {
|
|
462
891
|
const result = parseInline(`
|
|
463
892
|
defineFeature("f", (r) => {
|
|
464
|
-
r.secret("apiKey", { description: "Stripe API key" });
|
|
893
|
+
r.secret("apiKey", { description: "Stripe API key" });
|
|
894
|
+
});
|
|
895
|
+
`);
|
|
896
|
+
|
|
897
|
+
expect(result.patterns[0]).toMatchObject({
|
|
898
|
+
kind: "secret",
|
|
899
|
+
shortName: "apiKey",
|
|
900
|
+
options: { description: "Stripe API key" },
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test("object form captures name + options", () => {
|
|
905
|
+
const result = parseInline(`
|
|
906
|
+
defineFeature("f", (r) => {
|
|
907
|
+
r.secret({ name: "apiKey", description: "Stripe API key" });
|
|
908
|
+
});
|
|
909
|
+
`);
|
|
910
|
+
|
|
911
|
+
expect(result.patterns[0]).toMatchObject({
|
|
912
|
+
kind: "secret",
|
|
913
|
+
shortName: "apiKey",
|
|
914
|
+
options: { description: "Stripe API key" },
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
test("emits ParseError when called with no arguments", () => {
|
|
919
|
+
const result = parseInline(`
|
|
920
|
+
defineFeature("f", (r) => {
|
|
921
|
+
r.secret();
|
|
922
|
+
});
|
|
923
|
+
`);
|
|
924
|
+
|
|
925
|
+
expect(result.errors[0]?.methodName).toBe("secret");
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
describe("extractClaimKey", () => {
|
|
930
|
+
test("captures shortName + claim type", () => {
|
|
931
|
+
const result = parseInline(`
|
|
932
|
+
defineFeature("f", (r) => {
|
|
933
|
+
r.claimKey("teamId", { type: "string" });
|
|
934
|
+
});
|
|
935
|
+
`);
|
|
936
|
+
|
|
937
|
+
expect(result.patterns[0]).toMatchObject({
|
|
938
|
+
kind: "claimKey",
|
|
939
|
+
shortName: "teamId",
|
|
940
|
+
claimType: "string",
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test("emits ParseError on invalid claim type", () => {
|
|
945
|
+
const result = parseInline(`
|
|
946
|
+
defineFeature("f", (r) => {
|
|
947
|
+
r.claimKey("teamId", { type: "bigint" });
|
|
948
|
+
});
|
|
949
|
+
`);
|
|
950
|
+
|
|
951
|
+
expect(result.errors[0]?.methodName).toBe("claimKey");
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test("object form captures name + type", () => {
|
|
955
|
+
const result = parseInline(`
|
|
956
|
+
defineFeature("f", (r) => {
|
|
957
|
+
r.claimKey({ name: "teamId", type: "string" });
|
|
958
|
+
});
|
|
959
|
+
`);
|
|
960
|
+
|
|
961
|
+
expect(result.patterns[0]).toMatchObject({
|
|
962
|
+
kind: "claimKey",
|
|
963
|
+
shortName: "teamId",
|
|
964
|
+
claimType: "string",
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test("emits ParseError when called with no arguments", () => {
|
|
969
|
+
const result = parseInline(`
|
|
970
|
+
defineFeature("f", (r) => {
|
|
971
|
+
r.claimKey();
|
|
972
|
+
});
|
|
973
|
+
`);
|
|
974
|
+
|
|
975
|
+
expect(result.errors[0]?.methodName).toBe("claimKey");
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
describe("extractReferenceData", () => {
|
|
980
|
+
test("captures entity name + data array", () => {
|
|
981
|
+
const result = parseInline(`
|
|
982
|
+
defineFeature("f", (r) => {
|
|
983
|
+
r.referenceData("status", [
|
|
984
|
+
{ id: "open", label: "Open" },
|
|
985
|
+
{ id: "closed", label: "Closed" },
|
|
986
|
+
]);
|
|
987
|
+
});
|
|
988
|
+
`);
|
|
989
|
+
|
|
990
|
+
expect(result.patterns[0]).toMatchObject({
|
|
991
|
+
kind: "referenceData",
|
|
992
|
+
entityName: "status",
|
|
993
|
+
data: [
|
|
994
|
+
{ id: "open", label: "Open" },
|
|
995
|
+
{ id: "closed", label: "Closed" },
|
|
996
|
+
],
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test("captures the optional upsertKey", () => {
|
|
1001
|
+
const result = parseInline(`
|
|
1002
|
+
defineFeature("f", (r) => {
|
|
1003
|
+
r.referenceData("status", [{ id: "open" }], { upsertKey: "id" });
|
|
1004
|
+
});
|
|
1005
|
+
`);
|
|
1006
|
+
|
|
1007
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1008
|
+
kind: "referenceData",
|
|
1009
|
+
entityName: "status",
|
|
1010
|
+
upsertKey: "id",
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
test("object form captures entity, data and upsertKey", () => {
|
|
1015
|
+
const result = parseInline(`
|
|
1016
|
+
defineFeature("f", (r) => {
|
|
1017
|
+
r.referenceData({
|
|
1018
|
+
entity: "status",
|
|
1019
|
+
data: [{ id: "open" }, { id: "closed" }],
|
|
1020
|
+
upsertKey: "id",
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
`);
|
|
1024
|
+
|
|
1025
|
+
expect(result.errors).toEqual([]);
|
|
1026
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1027
|
+
kind: "referenceData",
|
|
1028
|
+
entityName: "status",
|
|
1029
|
+
data: [{ id: "open" }, { id: "closed" }],
|
|
1030
|
+
upsertKey: "id",
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
test("accepts an inline { name: '...' } entity ref, both forms", () => {
|
|
1035
|
+
const positional = parseInline(`
|
|
1036
|
+
defineFeature("f", (r) => {
|
|
1037
|
+
r.referenceData({ name: "status" }, [{ id: "open" }]);
|
|
1038
|
+
});
|
|
1039
|
+
`);
|
|
1040
|
+
expect(positional.patterns[0]).toMatchObject({ kind: "referenceData", entityName: "status" });
|
|
1041
|
+
|
|
1042
|
+
const objectForm = parseInline(`
|
|
1043
|
+
defineFeature("f", (r) => {
|
|
1044
|
+
r.referenceData({ entity: { name: "status" }, data: [{ id: "open" }] });
|
|
1045
|
+
});
|
|
1046
|
+
`);
|
|
1047
|
+
expect(objectForm.patterns[0]).toMatchObject({ kind: "referenceData", entityName: "status" });
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
test("emits ParseError when called with no arguments", () => {
|
|
1051
|
+
const result = parseInline(`
|
|
1052
|
+
defineFeature("f", (r) => {
|
|
1053
|
+
r.referenceData();
|
|
1054
|
+
});
|
|
1055
|
+
`);
|
|
1056
|
+
|
|
1057
|
+
expect(result.patterns).toEqual([]);
|
|
1058
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test("emits ParseError when object form is missing the data property", () => {
|
|
1062
|
+
const result = parseInline(`
|
|
1063
|
+
defineFeature("f", (r) => {
|
|
1064
|
+
r.referenceData({ entity: "status" });
|
|
1065
|
+
});
|
|
1066
|
+
`);
|
|
1067
|
+
|
|
1068
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
test("emits ParseError when object form is missing the entity property", () => {
|
|
1072
|
+
const result = parseInline(`
|
|
1073
|
+
defineFeature("f", (r) => {
|
|
1074
|
+
r.referenceData({ data: [{ id: "open" }] });
|
|
1075
|
+
});
|
|
1076
|
+
`);
|
|
1077
|
+
|
|
1078
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
test("emits ParseError when object form's data is not an array of plain objects", () => {
|
|
1082
|
+
const result = parseInline(`
|
|
1083
|
+
defineFeature("f", (r) => {
|
|
1084
|
+
r.referenceData({ entity: "status", data: "not-an-array" });
|
|
1085
|
+
});
|
|
1086
|
+
`);
|
|
1087
|
+
|
|
1088
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
test("emits ParseError when the positional data argument is missing", () => {
|
|
1092
|
+
const result = parseInline(`
|
|
1093
|
+
defineFeature("f", (r) => {
|
|
1094
|
+
r.referenceData("status");
|
|
1095
|
+
});
|
|
1096
|
+
`);
|
|
1097
|
+
|
|
1098
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
test("emits ParseError when positional upsertKey option is not a string", () => {
|
|
1102
|
+
const result = parseInline(`
|
|
1103
|
+
defineFeature("f", (r) => {
|
|
1104
|
+
r.referenceData("status", [{ id: "open" }], { upsertKey: 123 });
|
|
1105
|
+
});
|
|
1106
|
+
`);
|
|
1107
|
+
|
|
1108
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
test("emits ParseError when object form entity ref is unresolvable", () => {
|
|
1112
|
+
const result = parseInline(`
|
|
1113
|
+
const ent = { name: "status" };
|
|
1114
|
+
defineFeature("f", (r) => {
|
|
1115
|
+
r.referenceData({ entity: ent, data: [{ id: "open" }] });
|
|
1116
|
+
});
|
|
1117
|
+
`);
|
|
1118
|
+
|
|
1119
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
test("emits ParseError when positional data is not an array of plain objects", () => {
|
|
1123
|
+
const result = parseInline(`
|
|
1124
|
+
defineFeature("f", (r) => {
|
|
1125
|
+
r.referenceData("status", "not-an-array");
|
|
1126
|
+
});
|
|
1127
|
+
`);
|
|
1128
|
+
|
|
1129
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
test("emits ParseError when positional options is not a plain object", () => {
|
|
1133
|
+
const result = parseInline(`
|
|
1134
|
+
defineFeature("f", (r) => {
|
|
1135
|
+
r.referenceData("status", [{ id: "open" }], () => {});
|
|
1136
|
+
});
|
|
1137
|
+
`);
|
|
1138
|
+
|
|
1139
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
describe("extractUseExtension", () => {
|
|
1144
|
+
test("captures extension name + entity ref", () => {
|
|
1145
|
+
const result = parseInline(`
|
|
1146
|
+
defineFeature("f", (r) => {
|
|
1147
|
+
r.useExtension("audit", "task");
|
|
1148
|
+
});
|
|
1149
|
+
`);
|
|
1150
|
+
|
|
1151
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1152
|
+
kind: "useExtension",
|
|
1153
|
+
extensionName: "audit",
|
|
1154
|
+
entityName: "task",
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test("captures optional options", () => {
|
|
1159
|
+
const result = parseInline(`
|
|
1160
|
+
defineFeature("f", (r) => {
|
|
1161
|
+
r.useExtension("audit", "task", { mode: "verbose" });
|
|
1162
|
+
});
|
|
1163
|
+
`);
|
|
1164
|
+
|
|
1165
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1166
|
+
kind: "useExtension",
|
|
1167
|
+
extensionName: "audit",
|
|
1168
|
+
entityName: "task",
|
|
1169
|
+
options: { mode: "verbose" },
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
test("object form captures name, entity ref and options", () => {
|
|
1174
|
+
const result = parseInline(`
|
|
1175
|
+
defineFeature("f", (r) => {
|
|
1176
|
+
r.useExtension({ name: "audit", entity: "task", options: { mode: "verbose" } });
|
|
1177
|
+
});
|
|
1178
|
+
`);
|
|
1179
|
+
|
|
1180
|
+
expect(result.errors).toEqual([]);
|
|
1181
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1182
|
+
kind: "useExtension",
|
|
1183
|
+
extensionName: "audit",
|
|
1184
|
+
entityName: "task",
|
|
1185
|
+
options: { mode: "verbose" },
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
test("accepts an inline { name: '...' } entity ref, both forms", () => {
|
|
1190
|
+
const positional = parseInline(`
|
|
1191
|
+
defineFeature("f", (r) => {
|
|
1192
|
+
r.useExtension("audit", { name: "task" });
|
|
1193
|
+
});
|
|
1194
|
+
`);
|
|
1195
|
+
expect(positional.patterns[0]).toMatchObject({ kind: "useExtension", entityName: "task" });
|
|
1196
|
+
|
|
1197
|
+
const objectForm = parseInline(`
|
|
1198
|
+
defineFeature("f", (r) => {
|
|
1199
|
+
r.useExtension({ name: "audit", entity: { name: "task" } });
|
|
1200
|
+
});
|
|
1201
|
+
`);
|
|
1202
|
+
expect(objectForm.patterns[0]).toMatchObject({ kind: "useExtension", entityName: "task" });
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
test("emits ParseError when called with no arguments", () => {
|
|
1206
|
+
const result = parseInline(`
|
|
1207
|
+
defineFeature("f", (r) => {
|
|
1208
|
+
r.useExtension();
|
|
1209
|
+
});
|
|
1210
|
+
`);
|
|
1211
|
+
|
|
1212
|
+
expect(result.patterns).toEqual([]);
|
|
1213
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
test("emits ParseError when object form is missing the entity property", () => {
|
|
1217
|
+
const result = parseInline(`
|
|
1218
|
+
defineFeature("f", (r) => {
|
|
1219
|
+
r.useExtension({ name: "audit" });
|
|
1220
|
+
});
|
|
1221
|
+
`);
|
|
1222
|
+
|
|
1223
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
test("emits ParseError when object form's options is not a plain object", () => {
|
|
1227
|
+
const result = parseInline(`
|
|
1228
|
+
defineFeature("f", (r) => {
|
|
1229
|
+
r.useExtension({ name: "audit", entity: "task", options: "not-an-object" });
|
|
1230
|
+
});
|
|
1231
|
+
`);
|
|
1232
|
+
|
|
1233
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
test("emits ParseError when the positional entity argument is missing", () => {
|
|
1237
|
+
const result = parseInline(`
|
|
1238
|
+
defineFeature("f", (r) => {
|
|
1239
|
+
r.useExtension("audit");
|
|
1240
|
+
});
|
|
1241
|
+
`);
|
|
1242
|
+
|
|
1243
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
test("emits ParseError when positional options argument is not a plain object", () => {
|
|
1247
|
+
const result = parseInline(`
|
|
1248
|
+
defineFeature("f", (r) => {
|
|
1249
|
+
r.useExtension("audit", "task", "not-an-object");
|
|
1250
|
+
});
|
|
1251
|
+
`);
|
|
1252
|
+
|
|
1253
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
test("emits ParseError when the extension name is not a string literal", () => {
|
|
1257
|
+
const result = parseInline(`
|
|
1258
|
+
defineFeature("f", (r) => {
|
|
1259
|
+
r.useExtension(EXT, "task");
|
|
1260
|
+
});
|
|
1261
|
+
`);
|
|
1262
|
+
|
|
1263
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
test("emits ParseError when the positional entity ref is unresolvable", () => {
|
|
1267
|
+
const result = parseInline(`
|
|
1268
|
+
const ent = { name: "task" };
|
|
1269
|
+
defineFeature("f", (r) => {
|
|
1270
|
+
r.useExtension("audit", ent);
|
|
1271
|
+
});
|
|
1272
|
+
`);
|
|
1273
|
+
|
|
1274
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
test("emits ParseError when object form is missing the name property", () => {
|
|
1278
|
+
const result = parseInline(`
|
|
1279
|
+
defineFeature("f", (r) => {
|
|
1280
|
+
r.useExtension({ entity: "task" });
|
|
1281
|
+
});
|
|
1282
|
+
`);
|
|
1283
|
+
|
|
1284
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
1285
|
+
});
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// =============================================================================
|
|
1289
|
+
// Round 4 extractors — mixed patterns (header data + opaque body source)
|
|
1290
|
+
// =============================================================================
|
|
1291
|
+
|
|
1292
|
+
describe("extractHook", () => {
|
|
1293
|
+
test("captures hookType, target and the function body", () => {
|
|
1294
|
+
const result = parseInline(`
|
|
1295
|
+
defineFeature("f", (r) => {
|
|
1296
|
+
r.hook("postSave", "task", (event, ctx) => { console.log(event); });
|
|
1297
|
+
});
|
|
1298
|
+
`);
|
|
1299
|
+
|
|
1300
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1301
|
+
kind: "hook",
|
|
1302
|
+
hookType: "postSave",
|
|
1303
|
+
target: "task",
|
|
1304
|
+
});
|
|
1305
|
+
const fnBody = (result.patterns[0] as { fnBody?: { raw: string } } | undefined)?.fnBody;
|
|
1306
|
+
expect(fnBody?.raw).toContain("(event, ctx)");
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
test("captures the optional phase from the options object", () => {
|
|
1310
|
+
const result = parseInline(`
|
|
1311
|
+
defineFeature("f", (r) => {
|
|
1312
|
+
r.hook("postSave", "task", (event, ctx) => {}, { phase: "afterCommit" });
|
|
1313
|
+
});
|
|
1314
|
+
`);
|
|
1315
|
+
|
|
1316
|
+
expect(result.patterns[0]).toMatchObject({ kind: "hook", phase: "afterCommit" });
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
test("rejects an unknown hook type", () => {
|
|
1320
|
+
const result = parseInline(`
|
|
1321
|
+
defineFeature("f", (r) => {
|
|
1322
|
+
r.hook("postCommit", "task", () => {});
|
|
1323
|
+
});
|
|
1324
|
+
`);
|
|
1325
|
+
|
|
1326
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
test("object form captures type, target and handler", () => {
|
|
1330
|
+
const result = parseInline(`
|
|
1331
|
+
defineFeature("f", (r) => {
|
|
1332
|
+
r.hook({ type: "preSave", target: "task", handler: (event, ctx) => {} });
|
|
1333
|
+
});
|
|
1334
|
+
`);
|
|
1335
|
+
|
|
1336
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1337
|
+
kind: "hook",
|
|
1338
|
+
hookType: "preSave",
|
|
1339
|
+
target: "task",
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
test("emits ParseError when object form has an unknown hook type", () => {
|
|
1344
|
+
const result = parseInline(`
|
|
1345
|
+
defineFeature("f", (r) => {
|
|
1346
|
+
r.hook({ type: "postCommit", target: "task", handler: () => {} });
|
|
1347
|
+
});
|
|
1348
|
+
`);
|
|
1349
|
+
|
|
1350
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
test("emits ParseError when object form is missing the handler property", () => {
|
|
1354
|
+
const result = parseInline(`
|
|
1355
|
+
defineFeature("f", (r) => {
|
|
1356
|
+
r.hook({ type: "postSave", target: "task" });
|
|
1357
|
+
});
|
|
1358
|
+
`);
|
|
1359
|
+
|
|
1360
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
test("emits ParseError when object form handler is not an inline function", () => {
|
|
1364
|
+
const result = parseInline(`
|
|
1365
|
+
defineFeature("f", (r) => {
|
|
1366
|
+
r.hook({ type: "postSave", target: "task", handler: handlerRef });
|
|
1367
|
+
});
|
|
1368
|
+
`);
|
|
1369
|
+
|
|
1370
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
test("captures a multi-entity target array", () => {
|
|
1374
|
+
const result = parseInline(`
|
|
1375
|
+
defineFeature("f", (r) => {
|
|
1376
|
+
r.hook("postSave", ["task", "note"], () => {});
|
|
1377
|
+
});
|
|
1378
|
+
`);
|
|
1379
|
+
|
|
1380
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1381
|
+
kind: "hook",
|
|
1382
|
+
target: ["task", "note"],
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
test("emits ParseError when called with no arguments", () => {
|
|
1387
|
+
const result = parseInline(`
|
|
1388
|
+
defineFeature("f", (r) => {
|
|
1389
|
+
r.hook();
|
|
1390
|
+
});
|
|
1391
|
+
`);
|
|
1392
|
+
|
|
1393
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
test("emits ParseError when object form is missing the type property", () => {
|
|
1397
|
+
const result = parseInline(`
|
|
1398
|
+
defineFeature("f", (r) => {
|
|
1399
|
+
r.hook({ target: "task", handler: () => {} });
|
|
1400
|
+
});
|
|
1401
|
+
`);
|
|
1402
|
+
|
|
1403
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
test("emits ParseError when object form is missing the target property", () => {
|
|
1407
|
+
const result = parseInline(`
|
|
1408
|
+
defineFeature("f", (r) => {
|
|
1409
|
+
r.hook({ type: "postSave", handler: () => {} });
|
|
1410
|
+
});
|
|
1411
|
+
`);
|
|
1412
|
+
|
|
1413
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
test("emits ParseError when the handler is not an inline function", () => {
|
|
1417
|
+
const result = parseInline(`
|
|
1418
|
+
defineFeature("f", (r) => {
|
|
1419
|
+
r.hook("postSave", "task", handlerRef);
|
|
1420
|
+
});
|
|
1421
|
+
`);
|
|
1422
|
+
|
|
1423
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
test("emits ParseError when the hook type is not a string literal", () => {
|
|
1427
|
+
const result = parseInline(`
|
|
1428
|
+
defineFeature("f", (r) => {
|
|
1429
|
+
r.hook(TYPE, "task", () => {});
|
|
1430
|
+
});
|
|
1431
|
+
`);
|
|
1432
|
+
|
|
1433
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
test("emits ParseError when the target argument is missing", () => {
|
|
1437
|
+
const result = parseInline(`
|
|
1438
|
+
defineFeature("f", (r) => {
|
|
1439
|
+
r.hook("postSave");
|
|
1440
|
+
});
|
|
1441
|
+
`);
|
|
1442
|
+
|
|
1443
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
test("emits ParseError when object form target is not a valid ref", () => {
|
|
1447
|
+
const result = parseInline(`
|
|
1448
|
+
defineFeature("f", (r) => {
|
|
1449
|
+
r.hook({ type: "postSave", target: 123, handler: () => {} });
|
|
1450
|
+
});
|
|
1451
|
+
`);
|
|
1452
|
+
|
|
1453
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
test("emits ParseError when the hook function argument is missing", () => {
|
|
1457
|
+
const result = parseInline(`
|
|
1458
|
+
defineFeature("f", (r) => {
|
|
1459
|
+
r.hook("postSave", "task");
|
|
1460
|
+
});
|
|
1461
|
+
`);
|
|
1462
|
+
|
|
1463
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
describe("extractHook — entity-wide { allOf } target", () => {
|
|
1468
|
+
test("captures hookType and the allOf entity from positional form", () => {
|
|
1469
|
+
const result = parseInline(`
|
|
1470
|
+
defineFeature("f", (r) => {
|
|
1471
|
+
r.hook("postSave", { allOf: "task" }, (event, ctx) => {});
|
|
1472
|
+
});
|
|
1473
|
+
`);
|
|
1474
|
+
|
|
1475
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1476
|
+
kind: "hook",
|
|
1477
|
+
hookType: "postSave",
|
|
1478
|
+
target: { allOf: "task" },
|
|
1479
|
+
});
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
test("captures hookType and the allOf entity from object form", () => {
|
|
1483
|
+
const result = parseInline(`
|
|
1484
|
+
defineFeature("f", (r) => {
|
|
1485
|
+
r.hook({ type: "postDelete", target: { allOf: "task" }, handler: () => {} });
|
|
1486
|
+
});
|
|
1487
|
+
`);
|
|
1488
|
+
|
|
1489
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1490
|
+
kind: "hook",
|
|
1491
|
+
hookType: "postDelete",
|
|
1492
|
+
target: { allOf: "task" },
|
|
1493
|
+
});
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
test("rejects a malformed allOf value (not a string/ref)", () => {
|
|
1497
|
+
const result = parseInline(`
|
|
1498
|
+
defineFeature("f", (r) => {
|
|
1499
|
+
r.hook("postSave", { allOf: 123 }, () => {});
|
|
1500
|
+
});
|
|
1501
|
+
`);
|
|
1502
|
+
|
|
1503
|
+
expect(result.errors[0]?.methodName).toBe("hook");
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
describe("extractAuthClaims", () => {
|
|
1508
|
+
test("captures the function body as a SourceLocation", () => {
|
|
1509
|
+
const result = parseInline(`
|
|
1510
|
+
defineFeature("f", (r) => {
|
|
1511
|
+
r.authClaims(async (user, ctx) => ({ teamId: "t1" }));
|
|
1512
|
+
});
|
|
1513
|
+
`);
|
|
1514
|
+
|
|
1515
|
+
expect(result.patterns[0]).toMatchObject({ kind: "authClaims" });
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
test("emits ParseError when called with no arguments", () => {
|
|
1519
|
+
const result = parseInline(`
|
|
1520
|
+
defineFeature("f", (r) => {
|
|
1521
|
+
r.authClaims();
|
|
1522
|
+
});
|
|
1523
|
+
`);
|
|
1524
|
+
|
|
1525
|
+
expect(result.errors[0]?.methodName).toBe("authClaims");
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
test("emits ParseError when the argument is not an inline function", () => {
|
|
1529
|
+
const result = parseInline(`
|
|
1530
|
+
defineFeature("f", (r) => {
|
|
1531
|
+
r.authClaims(handlerRef);
|
|
1532
|
+
});
|
|
1533
|
+
`);
|
|
1534
|
+
|
|
1535
|
+
expect(result.errors[0]?.methodName).toBe("authClaims");
|
|
1536
|
+
});
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
describe("extractWriteHandler", () => {
|
|
1540
|
+
test("inline form: name, schema, handler, options", () => {
|
|
1541
|
+
const result = parseInline(`
|
|
1542
|
+
defineFeature("f", (r) => {
|
|
1543
|
+
r.writeHandler("task:create", z.object({ title: z.string() }), async (event, ctx) => {
|
|
1544
|
+
return { isSuccess: true, data: {} };
|
|
1545
|
+
}, { access: { roles: ["admin"] } });
|
|
1546
|
+
});
|
|
1547
|
+
`);
|
|
1548
|
+
|
|
1549
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1550
|
+
kind: "writeHandler",
|
|
1551
|
+
handlerName: "task:create",
|
|
1552
|
+
access: { roles: ["admin"] },
|
|
1553
|
+
});
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
test("object form: defineWriteHandler shape", () => {
|
|
1557
|
+
const result = parseInline(`
|
|
1558
|
+
defineFeature("f", (r) => {
|
|
1559
|
+
r.writeHandler({
|
|
1560
|
+
name: "task:approve",
|
|
1561
|
+
schema: z.object({ id: z.string() }),
|
|
1562
|
+
handler: async (event, ctx) => ({ isSuccess: true, data: {} }),
|
|
1563
|
+
access: { openToAll: true },
|
|
1564
|
+
});
|
|
1565
|
+
});
|
|
1566
|
+
`);
|
|
1567
|
+
|
|
1568
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1569
|
+
kind: "writeHandler",
|
|
1570
|
+
handlerName: "task:approve",
|
|
1571
|
+
access: { openToAll: true },
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
test("resolves a same-file identifier arg to its object-literal form (#1007)", () => {
|
|
1576
|
+
const result = parseInline(`
|
|
1577
|
+
const h = {
|
|
1578
|
+
name: "task:archive",
|
|
1579
|
+
schema: z.object({ id: z.string() }),
|
|
1580
|
+
handler: async (event, ctx) => ({ isSuccess: true, data: {} }),
|
|
1581
|
+
};
|
|
1582
|
+
defineFeature("f", (r) => {
|
|
1583
|
+
r.writeHandler(h);
|
|
1584
|
+
});
|
|
1585
|
+
`);
|
|
1586
|
+
|
|
1587
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1588
|
+
kind: "writeHandler",
|
|
1589
|
+
handlerName: "task:archive",
|
|
1590
|
+
});
|
|
1591
|
+
expect(result.errors).toEqual([]);
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
test("keeps an unresolvable ref (import, factory call) as an opaque pattern instead of failing (#1007)", () => {
|
|
1595
|
+
const result = parseInline(`
|
|
1596
|
+
defineFeature("f", (r) => {
|
|
1597
|
+
r.writeHandler(eventCreateHandler);
|
|
1598
|
+
});
|
|
1599
|
+
`);
|
|
1600
|
+
|
|
1601
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1602
|
+
kind: "writeHandler",
|
|
1603
|
+
handlerName: undefined,
|
|
1604
|
+
source: { raw: "r.writeHandler(eventCreateHandler)" },
|
|
1605
|
+
});
|
|
1606
|
+
expect(result.errors).toEqual([]);
|
|
1607
|
+
});
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
describe("extractQueryHandler", () => {
|
|
1611
|
+
test("inline form returns kind=queryHandler", () => {
|
|
1612
|
+
const result = parseInline(`
|
|
1613
|
+
defineFeature("f", (r) => {
|
|
1614
|
+
r.queryHandler("task:list", z.object({}), async (q, ctx) => []);
|
|
1615
|
+
});
|
|
1616
|
+
`);
|
|
1617
|
+
|
|
1618
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1619
|
+
kind: "queryHandler",
|
|
1620
|
+
handlerName: "task:list",
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
describe("extractStreamHandler", () => {
|
|
1626
|
+
test("inline form: name, schema, handler, options", () => {
|
|
1627
|
+
const result = parseInline(`
|
|
1628
|
+
defineFeature("f", (r) => {
|
|
1629
|
+
r.streamHandler("chat:complete", z.object({ prompt: z.string() }), async function* (input, ctx) {
|
|
1630
|
+
yield "token";
|
|
1631
|
+
}, { access: { roles: ["User"] } });
|
|
1632
|
+
});
|
|
1633
|
+
`);
|
|
1634
|
+
|
|
1635
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1636
|
+
kind: "streamHandler",
|
|
1637
|
+
handlerName: "chat:complete",
|
|
1638
|
+
access: { roles: ["User"] },
|
|
1639
|
+
});
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
test("object form: name, schema, handler", () => {
|
|
1643
|
+
const result = parseInline(`
|
|
1644
|
+
defineFeature("f", (r) => {
|
|
1645
|
+
r.streamHandler({
|
|
1646
|
+
name: "chat:complete",
|
|
1647
|
+
schema: z.object({ prompt: z.string() }),
|
|
1648
|
+
handler: async function* (input, ctx) { yield "token"; },
|
|
1649
|
+
access: { openToAll: true },
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
`);
|
|
1653
|
+
|
|
1654
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1655
|
+
kind: "streamHandler",
|
|
1656
|
+
handlerName: "chat:complete",
|
|
1657
|
+
access: { openToAll: true },
|
|
1658
|
+
});
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
test("keeps an unresolvable ref as an opaque pattern instead of failing", () => {
|
|
1662
|
+
const result = parseInline(`
|
|
1663
|
+
defineFeature("f", (r) => {
|
|
1664
|
+
r.streamHandler(chatCompleteHandler);
|
|
1665
|
+
});
|
|
1666
|
+
`);
|
|
1667
|
+
|
|
1668
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1669
|
+
kind: "streamHandler",
|
|
1670
|
+
handlerName: undefined,
|
|
1671
|
+
source: { raw: "r.streamHandler(chatCompleteHandler)" },
|
|
1672
|
+
});
|
|
1673
|
+
expect(result.errors).toEqual([]);
|
|
1674
|
+
});
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
describe("extractJob", () => {
|
|
1678
|
+
test("captures jobName, options and handler body", () => {
|
|
1679
|
+
const result = parseInline(`
|
|
1680
|
+
defineFeature("f", (r) => {
|
|
1681
|
+
r.job("daily-cleanup", { trigger: { cron: "0 3 * * *" } }, async (ctx) => {});
|
|
1682
|
+
});
|
|
1683
|
+
`);
|
|
1684
|
+
|
|
1685
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1686
|
+
kind: "job",
|
|
1687
|
+
jobName: "daily-cleanup",
|
|
1688
|
+
});
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
test("object form captures name, handler and extra options", () => {
|
|
1692
|
+
const result = parseInline(`
|
|
1693
|
+
defineFeature("f", (r) => {
|
|
1694
|
+
r.job({
|
|
1695
|
+
name: "daily-cleanup",
|
|
1696
|
+
trigger: { cron: "0 3 * * *" },
|
|
1697
|
+
handler: async (ctx) => {},
|
|
1698
|
+
});
|
|
1699
|
+
});
|
|
1700
|
+
`);
|
|
1701
|
+
|
|
1702
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1703
|
+
kind: "job",
|
|
1704
|
+
jobName: "daily-cleanup",
|
|
1705
|
+
options: { trigger: { cron: "0 3 * * *" } },
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
test("emits ParseError when called with no arguments", () => {
|
|
1710
|
+
const result = parseInline(`
|
|
1711
|
+
defineFeature("f", (r) => {
|
|
1712
|
+
r.job();
|
|
1713
|
+
});
|
|
1714
|
+
`);
|
|
1715
|
+
|
|
1716
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
test("emits ParseError when object form is missing the name property", () => {
|
|
1720
|
+
const result = parseInline(`
|
|
1721
|
+
defineFeature("f", (r) => {
|
|
1722
|
+
r.job({ handler: async () => {} });
|
|
1723
|
+
});
|
|
1724
|
+
`);
|
|
1725
|
+
|
|
1726
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
test("emits ParseError when object form is missing the handler property", () => {
|
|
1730
|
+
const result = parseInline(`
|
|
1731
|
+
defineFeature("f", (r) => {
|
|
1732
|
+
r.job({ name: "daily-cleanup", trigger: { cron: "0 3 * * *" } });
|
|
1733
|
+
});
|
|
1734
|
+
`);
|
|
1735
|
+
|
|
1736
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
test("emits ParseError when object form option value is not readable", () => {
|
|
1740
|
+
const result = parseInline(`
|
|
1741
|
+
defineFeature("f", (r) => {
|
|
1742
|
+
r.job({ name: "daily-cleanup", handler: async () => {}, onComplete: () => {} });
|
|
1743
|
+
});
|
|
1744
|
+
`);
|
|
1745
|
+
|
|
1746
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
test("emits ParseError when object form handler is not an inline function", () => {
|
|
1750
|
+
const result = parseInline(`
|
|
1751
|
+
defineFeature("f", (r) => {
|
|
1752
|
+
r.job({ name: "daily-cleanup", handler: handlerRef });
|
|
1753
|
+
});
|
|
1754
|
+
`);
|
|
1755
|
+
|
|
1756
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
test("emits ParseError when the job name is not a string literal", () => {
|
|
1760
|
+
const result = parseInline(`
|
|
1761
|
+
defineFeature("f", (r) => {
|
|
1762
|
+
r.job(NAME, {}, async () => {});
|
|
1763
|
+
});
|
|
1764
|
+
`);
|
|
1765
|
+
|
|
1766
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
test("emits ParseError when the options argument is missing", () => {
|
|
1770
|
+
const result = parseInline(`
|
|
1771
|
+
defineFeature("f", (r) => {
|
|
1772
|
+
r.job("daily-cleanup");
|
|
1773
|
+
});
|
|
1774
|
+
`);
|
|
1775
|
+
|
|
1776
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
test("emits ParseError when the handler argument is missing", () => {
|
|
1780
|
+
const result = parseInline(`
|
|
1781
|
+
defineFeature("f", (r) => {
|
|
1782
|
+
r.job("daily-cleanup", { trigger: { cron: "0 3 * * *" } });
|
|
1783
|
+
});
|
|
1784
|
+
`);
|
|
1785
|
+
|
|
1786
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
test("emits ParseError when options is not a plain object", () => {
|
|
1790
|
+
const result = parseInline(`
|
|
1791
|
+
defineFeature("f", (r) => {
|
|
1792
|
+
r.job("daily-cleanup", () => {}, async () => {});
|
|
1793
|
+
});
|
|
1794
|
+
`);
|
|
1795
|
+
|
|
1796
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
test("emits ParseError when the positional handler is not an inline function", () => {
|
|
1800
|
+
const result = parseInline(`
|
|
1801
|
+
defineFeature("f", (r) => {
|
|
1802
|
+
r.job("daily-cleanup", { trigger: { cron: "0 3 * * *" } }, handlerRef);
|
|
1803
|
+
});
|
|
1804
|
+
`);
|
|
1805
|
+
|
|
1806
|
+
expect(result.errors[0]?.methodName).toBe("job");
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
describe("extractHttpRoute", () => {
|
|
1811
|
+
test("captures method, path, anonymous, handler", () => {
|
|
1812
|
+
const result = parseInline(`
|
|
1813
|
+
defineFeature("f", (r) => {
|
|
1814
|
+
r.httpRoute({
|
|
1815
|
+
method: "GET",
|
|
1816
|
+
path: "/feed.xml",
|
|
1817
|
+
anonymous: true,
|
|
1818
|
+
handler: async (c) => new Response("ok"),
|
|
1819
|
+
});
|
|
1820
|
+
});
|
|
1821
|
+
`);
|
|
1822
|
+
|
|
1823
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1824
|
+
kind: "httpRoute",
|
|
1825
|
+
method: "GET",
|
|
1826
|
+
path: "/feed.xml",
|
|
1827
|
+
anonymous: true,
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
test("emits ParseError when the argument is not an inline object", () => {
|
|
1832
|
+
const result = parseInline(`
|
|
1833
|
+
defineFeature("f", (r) => {
|
|
1834
|
+
r.httpRoute("GET", "/feed.xml");
|
|
1835
|
+
});
|
|
1836
|
+
`);
|
|
1837
|
+
|
|
1838
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
test("emits ParseError when method is missing", () => {
|
|
1842
|
+
const result = parseInline(`
|
|
1843
|
+
defineFeature("f", (r) => {
|
|
1844
|
+
r.httpRoute({ path: "/feed.xml", handler: async () => {} });
|
|
465
1845
|
});
|
|
466
1846
|
`);
|
|
467
1847
|
|
|
468
|
-
expect(result.
|
|
469
|
-
kind: "secret",
|
|
470
|
-
shortName: "apiKey",
|
|
471
|
-
options: { description: "Stripe API key" },
|
|
472
|
-
});
|
|
1848
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
473
1849
|
});
|
|
474
|
-
});
|
|
475
1850
|
|
|
476
|
-
|
|
477
|
-
test("captures shortName + claim type", () => {
|
|
1851
|
+
test("emits ParseError when method is not a valid HTTP verb", () => {
|
|
478
1852
|
const result = parseInline(`
|
|
479
1853
|
defineFeature("f", (r) => {
|
|
480
|
-
r.
|
|
1854
|
+
r.httpRoute({ method: "FETCH", path: "/feed.xml", handler: async () => {} });
|
|
481
1855
|
});
|
|
482
1856
|
`);
|
|
483
1857
|
|
|
484
|
-
expect(result.
|
|
485
|
-
kind: "claimKey",
|
|
486
|
-
shortName: "teamId",
|
|
487
|
-
claimType: "string",
|
|
488
|
-
});
|
|
1858
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
489
1859
|
});
|
|
490
1860
|
|
|
491
|
-
test("emits ParseError
|
|
1861
|
+
test("emits ParseError when path is missing", () => {
|
|
492
1862
|
const result = parseInline(`
|
|
493
1863
|
defineFeature("f", (r) => {
|
|
494
|
-
r.
|
|
1864
|
+
r.httpRoute({ method: "GET", handler: async () => {} });
|
|
495
1865
|
});
|
|
496
1866
|
`);
|
|
497
1867
|
|
|
498
|
-
expect(result.errors[0]?.methodName).toBe("
|
|
1868
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
499
1869
|
});
|
|
500
|
-
});
|
|
501
1870
|
|
|
502
|
-
|
|
503
|
-
test("captures entity name + data array", () => {
|
|
1871
|
+
test("emits ParseError when handler is missing", () => {
|
|
504
1872
|
const result = parseInline(`
|
|
505
1873
|
defineFeature("f", (r) => {
|
|
506
|
-
r.
|
|
507
|
-
{ id: "open", label: "Open" },
|
|
508
|
-
{ id: "closed", label: "Closed" },
|
|
509
|
-
]);
|
|
1874
|
+
r.httpRoute({ method: "GET", path: "/feed.xml" });
|
|
510
1875
|
});
|
|
511
1876
|
`);
|
|
512
1877
|
|
|
513
|
-
expect(result.
|
|
514
|
-
kind: "referenceData",
|
|
515
|
-
entityName: "status",
|
|
516
|
-
data: [
|
|
517
|
-
{ id: "open", label: "Open" },
|
|
518
|
-
{ id: "closed", label: "Closed" },
|
|
519
|
-
],
|
|
520
|
-
});
|
|
1878
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
521
1879
|
});
|
|
522
1880
|
|
|
523
|
-
test("
|
|
1881
|
+
test("emits ParseError when handler is not an inline function", () => {
|
|
524
1882
|
const result = parseInline(`
|
|
525
1883
|
defineFeature("f", (r) => {
|
|
526
|
-
r.
|
|
1884
|
+
r.httpRoute({ method: "GET", path: "/feed.xml", handler: handlerRef });
|
|
527
1885
|
});
|
|
528
1886
|
`);
|
|
529
1887
|
|
|
530
|
-
expect(result.
|
|
531
|
-
kind: "referenceData",
|
|
532
|
-
entityName: "status",
|
|
533
|
-
upsertKey: "id",
|
|
534
|
-
});
|
|
1888
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
535
1889
|
});
|
|
536
1890
|
});
|
|
537
1891
|
|
|
538
|
-
describe("
|
|
539
|
-
test("captures
|
|
1892
|
+
describe("extractDefineEvent", () => {
|
|
1893
|
+
test("captures eventName + version", () => {
|
|
540
1894
|
const result = parseInline(`
|
|
541
1895
|
defineFeature("f", (r) => {
|
|
542
|
-
r.
|
|
1896
|
+
r.defineEvent("incidentOpened", z.object({ id: z.string() }), { version: 2 });
|
|
543
1897
|
});
|
|
544
1898
|
`);
|
|
545
1899
|
|
|
546
1900
|
expect(result.patterns[0]).toMatchObject({
|
|
547
|
-
kind: "
|
|
548
|
-
|
|
549
|
-
|
|
1901
|
+
kind: "defineEvent",
|
|
1902
|
+
eventName: "incidentOpened",
|
|
1903
|
+
version: 2,
|
|
550
1904
|
});
|
|
551
1905
|
});
|
|
552
1906
|
|
|
553
|
-
test("captures
|
|
1907
|
+
test("object form captures name, schema, version and migrations", () => {
|
|
554
1908
|
const result = parseInline(`
|
|
555
1909
|
defineFeature("f", (r) => {
|
|
556
|
-
r.
|
|
1910
|
+
r.defineEvent({
|
|
1911
|
+
name: "incidentOpened",
|
|
1912
|
+
schema: z.object({ id: z.string() }),
|
|
1913
|
+
version: 2,
|
|
1914
|
+
migrations: { "1": (payload) => payload },
|
|
1915
|
+
});
|
|
557
1916
|
});
|
|
558
1917
|
`);
|
|
559
1918
|
|
|
560
1919
|
expect(result.patterns[0]).toMatchObject({
|
|
561
|
-
kind: "
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
1920
|
+
kind: "defineEvent",
|
|
1921
|
+
eventName: "incidentOpened",
|
|
1922
|
+
version: 2,
|
|
1923
|
+
migrations: { "1": expect.anything() },
|
|
565
1924
|
});
|
|
566
1925
|
});
|
|
567
|
-
});
|
|
568
1926
|
|
|
569
|
-
|
|
570
|
-
// Round 4 extractors — mixed patterns (header data + opaque body source)
|
|
571
|
-
// =============================================================================
|
|
572
|
-
|
|
573
|
-
describe("extractHook", () => {
|
|
574
|
-
test("captures hookType, target and the function body", () => {
|
|
1927
|
+
test("emits ParseError when called with no arguments", () => {
|
|
575
1928
|
const result = parseInline(`
|
|
576
1929
|
defineFeature("f", (r) => {
|
|
577
|
-
r.
|
|
1930
|
+
r.defineEvent();
|
|
578
1931
|
});
|
|
579
1932
|
`);
|
|
580
1933
|
|
|
581
|
-
expect(result.
|
|
582
|
-
kind: "hook",
|
|
583
|
-
hookType: "postSave",
|
|
584
|
-
target: "task",
|
|
585
|
-
});
|
|
586
|
-
const fnBody = (result.patterns[0] as { fnBody?: { raw: string } } | undefined)?.fnBody;
|
|
587
|
-
expect(fnBody?.raw).toContain("(event, ctx)");
|
|
1934
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
588
1935
|
});
|
|
589
1936
|
|
|
590
|
-
test("
|
|
1937
|
+
test("emits ParseError when object form is missing the name property", () => {
|
|
591
1938
|
const result = parseInline(`
|
|
592
1939
|
defineFeature("f", (r) => {
|
|
593
|
-
r.
|
|
1940
|
+
r.defineEvent({ schema: z.object({ id: z.string() }) });
|
|
594
1941
|
});
|
|
595
1942
|
`);
|
|
596
1943
|
|
|
597
|
-
expect(result.
|
|
1944
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
598
1945
|
});
|
|
599
1946
|
|
|
600
|
-
test("
|
|
1947
|
+
test("emits ParseError when object form is missing the schema property", () => {
|
|
601
1948
|
const result = parseInline(`
|
|
602
1949
|
defineFeature("f", (r) => {
|
|
603
|
-
r.
|
|
1950
|
+
r.defineEvent({ name: "incidentOpened" });
|
|
604
1951
|
});
|
|
605
1952
|
`);
|
|
606
1953
|
|
|
607
|
-
expect(result.errors[0]?.methodName).toBe("
|
|
1954
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
608
1955
|
});
|
|
609
|
-
});
|
|
610
1956
|
|
|
611
|
-
|
|
612
|
-
test("captures hookType, entity and the function body", () => {
|
|
1957
|
+
test("emits ParseError when the event name is not a string literal", () => {
|
|
613
1958
|
const result = parseInline(`
|
|
614
1959
|
defineFeature("f", (r) => {
|
|
615
|
-
r.
|
|
1960
|
+
r.defineEvent(EVENT, z.object({ id: z.string() }));
|
|
616
1961
|
});
|
|
617
1962
|
`);
|
|
618
1963
|
|
|
619
|
-
expect(result.
|
|
620
|
-
kind: "entityHook",
|
|
621
|
-
hookType: "postSave",
|
|
622
|
-
entityName: "task",
|
|
623
|
-
});
|
|
1964
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
624
1965
|
});
|
|
625
1966
|
|
|
626
|
-
test("
|
|
1967
|
+
test("emits ParseError when the schema argument is missing", () => {
|
|
627
1968
|
const result = parseInline(`
|
|
628
1969
|
defineFeature("f", (r) => {
|
|
629
|
-
r.
|
|
1970
|
+
r.defineEvent("incidentOpened");
|
|
630
1971
|
});
|
|
631
1972
|
`);
|
|
632
1973
|
|
|
633
|
-
expect(result.errors[0]?.methodName).toBe("
|
|
1974
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
634
1975
|
});
|
|
635
1976
|
});
|
|
636
1977
|
|
|
637
|
-
describe("
|
|
638
|
-
test("
|
|
1978
|
+
describe("extractDefineEvent — migrations (formerly extractEventMigration)", () => {
|
|
1979
|
+
test("positional-form migrations array captures the fromVersion→transform step", () => {
|
|
639
1980
|
const result = parseInline(`
|
|
640
1981
|
defineFeature("f", (r) => {
|
|
641
|
-
r.
|
|
1982
|
+
r.defineEvent("incidentOpened", z.object({ id: z.string() }), {
|
|
1983
|
+
version: 2,
|
|
1984
|
+
migrations: [
|
|
1985
|
+
{ fromVersion: 1, toVersion: 2, transform: (payload) => ({ ...payload, severity: "low" }) },
|
|
1986
|
+
],
|
|
1987
|
+
});
|
|
642
1988
|
});
|
|
643
1989
|
`);
|
|
644
1990
|
|
|
645
|
-
expect(result.patterns[0]).toMatchObject({
|
|
1991
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1992
|
+
kind: "defineEvent",
|
|
1993
|
+
eventName: "incidentOpened",
|
|
1994
|
+
version: 2,
|
|
1995
|
+
migrations: { "1": expect.anything() },
|
|
1996
|
+
});
|
|
646
1997
|
});
|
|
647
|
-
});
|
|
648
1998
|
|
|
649
|
-
|
|
650
|
-
test("inline form: name, schema, handler, options", () => {
|
|
1999
|
+
test("object-form migrations map captures the fromVersion→transform step", () => {
|
|
651
2000
|
const result = parseInline(`
|
|
652
2001
|
defineFeature("f", (r) => {
|
|
653
|
-
r.
|
|
654
|
-
|
|
655
|
-
|
|
2002
|
+
r.defineEvent({
|
|
2003
|
+
name: "incidentOpened",
|
|
2004
|
+
schema: z.object({ id: z.string() }),
|
|
2005
|
+
version: 2,
|
|
2006
|
+
migrations: {
|
|
2007
|
+
"1": (payload) => ({ ...payload, severity: "low" }),
|
|
2008
|
+
},
|
|
2009
|
+
});
|
|
656
2010
|
});
|
|
657
2011
|
`);
|
|
658
2012
|
|
|
659
2013
|
expect(result.patterns[0]).toMatchObject({
|
|
660
|
-
kind: "
|
|
661
|
-
|
|
662
|
-
|
|
2014
|
+
kind: "defineEvent",
|
|
2015
|
+
eventName: "incidentOpened",
|
|
2016
|
+
version: 2,
|
|
2017
|
+
migrations: { "1": expect.anything() },
|
|
663
2018
|
});
|
|
664
2019
|
});
|
|
665
2020
|
|
|
666
|
-
test("
|
|
2021
|
+
test("skips malformed migration array entries instead of failing the whole extract", () => {
|
|
667
2022
|
const result = parseInline(`
|
|
668
2023
|
defineFeature("f", (r) => {
|
|
669
|
-
r.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
2024
|
+
r.defineEvent("incidentOpened", z.object({ id: z.string() }), {
|
|
2025
|
+
migrations: [
|
|
2026
|
+
"skip-me",
|
|
2027
|
+
{ fromVersion: "bad", transform: (payload) => payload },
|
|
2028
|
+
{ fromVersion: 1, transform: (payload) => ({ ...payload, ok: true }) },
|
|
2029
|
+
],
|
|
674
2030
|
});
|
|
675
2031
|
});
|
|
676
2032
|
`);
|
|
677
2033
|
|
|
2034
|
+
expect(result.errors).toEqual([]);
|
|
678
2035
|
expect(result.patterns[0]).toMatchObject({
|
|
679
|
-
kind: "
|
|
680
|
-
|
|
681
|
-
access: { openToAll: true },
|
|
2036
|
+
kind: "defineEvent",
|
|
2037
|
+
migrations: { "1": expect.anything() },
|
|
682
2038
|
});
|
|
683
2039
|
});
|
|
684
2040
|
});
|
|
685
2041
|
|
|
686
|
-
describe("
|
|
687
|
-
test("
|
|
2042
|
+
describe("extractNotification", () => {
|
|
2043
|
+
test("captures trigger + recipient + data bodies", () => {
|
|
688
2044
|
const result = parseInline(`
|
|
689
2045
|
defineFeature("f", (r) => {
|
|
690
|
-
r.
|
|
2046
|
+
r.notification("incidentOpened", {
|
|
2047
|
+
trigger: { on: "incident:create" },
|
|
2048
|
+
recipient: (event) => ({ tenant: event.tenantId }),
|
|
2049
|
+
data: (event) => ({ id: event.id }),
|
|
2050
|
+
});
|
|
691
2051
|
});
|
|
692
2052
|
`);
|
|
693
2053
|
|
|
694
2054
|
expect(result.patterns[0]).toMatchObject({
|
|
695
|
-
kind: "
|
|
696
|
-
|
|
2055
|
+
kind: "notification",
|
|
2056
|
+
notificationName: "incidentOpened",
|
|
2057
|
+
trigger: { on: "incident:create" },
|
|
697
2058
|
});
|
|
698
2059
|
});
|
|
699
|
-
});
|
|
700
2060
|
|
|
701
|
-
|
|
702
|
-
test("captures jobName, options and handler body", () => {
|
|
2061
|
+
test("object form captures name + trigger/recipient/data", () => {
|
|
703
2062
|
const result = parseInline(`
|
|
704
2063
|
defineFeature("f", (r) => {
|
|
705
|
-
r.
|
|
2064
|
+
r.notification({
|
|
2065
|
+
name: "incidentOpened",
|
|
2066
|
+
trigger: { on: "incident:create" },
|
|
2067
|
+
recipient: (event) => ({ tenant: event.tenantId }),
|
|
2068
|
+
data: (event) => ({ id: event.id }),
|
|
2069
|
+
});
|
|
706
2070
|
});
|
|
707
2071
|
`);
|
|
708
2072
|
|
|
709
2073
|
expect(result.patterns[0]).toMatchObject({
|
|
710
|
-
kind: "
|
|
711
|
-
|
|
2074
|
+
kind: "notification",
|
|
2075
|
+
notificationName: "incidentOpened",
|
|
712
2076
|
});
|
|
713
2077
|
});
|
|
2078
|
+
|
|
2079
|
+
test("captures optional template bodies", () => {
|
|
2080
|
+
const result = parseInline(`
|
|
2081
|
+
defineFeature("f", (r) => {
|
|
2082
|
+
r.notification("incidentOpened", {
|
|
2083
|
+
trigger: { on: "incident:create" },
|
|
2084
|
+
recipient: (event) => ({}),
|
|
2085
|
+
data: (event) => ({}),
|
|
2086
|
+
templates: {
|
|
2087
|
+
email: (payload) => payload.subject,
|
|
2088
|
+
inApp: (payload) => payload.title,
|
|
2089
|
+
},
|
|
2090
|
+
});
|
|
714
2091
|
});
|
|
2092
|
+
`);
|
|
715
2093
|
|
|
716
|
-
|
|
717
|
-
|
|
2094
|
+
const templates = (result.patterns[0] as { templates?: Record<string, unknown> } | undefined)
|
|
2095
|
+
?.templates;
|
|
2096
|
+
expect(Object.keys(templates ?? {})).toEqual(["email", "inApp"]);
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
test("emits ParseError when called with no arguments", () => {
|
|
718
2100
|
const result = parseInline(`
|
|
719
2101
|
defineFeature("f", (r) => {
|
|
720
|
-
r.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
2102
|
+
r.notification();
|
|
2103
|
+
});
|
|
2104
|
+
`);
|
|
2105
|
+
|
|
2106
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
725
2107
|
});
|
|
2108
|
+
|
|
2109
|
+
test("emits ParseError when object form is missing the name property", () => {
|
|
2110
|
+
const result = parseInline(`
|
|
2111
|
+
defineFeature("f", (r) => {
|
|
2112
|
+
r.notification({ trigger: { on: "x" }, recipient: () => ({}), data: () => ({}) });
|
|
726
2113
|
});
|
|
727
2114
|
`);
|
|
728
2115
|
|
|
729
|
-
expect(result.
|
|
730
|
-
kind: "httpRoute",
|
|
731
|
-
method: "GET",
|
|
732
|
-
path: "/feed.xml",
|
|
733
|
-
anonymous: true,
|
|
734
|
-
});
|
|
2116
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
735
2117
|
});
|
|
2118
|
+
|
|
2119
|
+
test("emits ParseError when the notification name is not a string literal", () => {
|
|
2120
|
+
const result = parseInline(`
|
|
2121
|
+
defineFeature("f", (r) => {
|
|
2122
|
+
r.notification(NAME, { trigger: { on: "x" }, recipient: () => ({}), data: () => ({}) });
|
|
736
2123
|
});
|
|
2124
|
+
`);
|
|
737
2125
|
|
|
738
|
-
|
|
739
|
-
|
|
2126
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
test("emits ParseError when the definition object is missing", () => {
|
|
740
2130
|
const result = parseInline(`
|
|
741
2131
|
defineFeature("f", (r) => {
|
|
742
|
-
r.
|
|
2132
|
+
r.notification("incidentOpened");
|
|
743
2133
|
});
|
|
744
2134
|
`);
|
|
745
2135
|
|
|
746
|
-
expect(result.
|
|
747
|
-
kind: "defineEvent",
|
|
748
|
-
eventName: "incidentOpened",
|
|
749
|
-
version: 2,
|
|
750
|
-
});
|
|
2136
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
751
2137
|
});
|
|
2138
|
+
|
|
2139
|
+
test("emits ParseError when trigger is missing", () => {
|
|
2140
|
+
const result = parseInline(`
|
|
2141
|
+
defineFeature("f", (r) => {
|
|
2142
|
+
r.notification("incidentOpened", { recipient: () => ({}), data: () => ({}) });
|
|
752
2143
|
});
|
|
2144
|
+
`);
|
|
2145
|
+
|
|
2146
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
2147
|
+
});
|
|
753
2148
|
|
|
754
|
-
|
|
755
|
-
test("captures fromVersion / toVersion / transform body", () => {
|
|
2149
|
+
test("emits ParseError when trigger.on is not a string/ref", () => {
|
|
756
2150
|
const result = parseInline(`
|
|
757
2151
|
defineFeature("f", (r) => {
|
|
758
|
-
r.
|
|
2152
|
+
r.notification("incidentOpened", {
|
|
2153
|
+
trigger: { on: 123 },
|
|
2154
|
+
recipient: () => ({}),
|
|
2155
|
+
data: () => ({}),
|
|
2156
|
+
});
|
|
759
2157
|
});
|
|
760
2158
|
`);
|
|
761
2159
|
|
|
762
|
-
expect(result.
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
2160
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
test("emits ParseError when recipient is not an inline function", () => {
|
|
2164
|
+
const result = parseInline(`
|
|
2165
|
+
defineFeature("f", (r) => {
|
|
2166
|
+
r.notification("incidentOpened", {
|
|
2167
|
+
trigger: { on: "incident:create" },
|
|
2168
|
+
recipient: recipientRef,
|
|
2169
|
+
data: () => ({}),
|
|
768
2170
|
});
|
|
769
2171
|
});
|
|
2172
|
+
`);
|
|
770
2173
|
|
|
771
|
-
|
|
772
|
-
|
|
2174
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
test("emits ParseError when data is not an inline function", () => {
|
|
773
2178
|
const result = parseInline(`
|
|
774
2179
|
defineFeature("f", (r) => {
|
|
775
2180
|
r.notification("incidentOpened", {
|
|
776
2181
|
trigger: { on: "incident:create" },
|
|
777
|
-
recipient: (
|
|
778
|
-
data:
|
|
2182
|
+
recipient: () => ({}),
|
|
2183
|
+
data: dataRef,
|
|
779
2184
|
});
|
|
780
2185
|
});
|
|
781
2186
|
`);
|
|
782
2187
|
|
|
783
|
-
expect(result.
|
|
784
|
-
kind: "notification",
|
|
785
|
-
notificationName: "incidentOpened",
|
|
786
|
-
trigger: { on: "incident:create" },
|
|
787
|
-
});
|
|
2188
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
788
2189
|
});
|
|
789
2190
|
});
|
|
790
2191
|
|
|
@@ -886,3 +2287,82 @@ defineFeature("f", (r) => {
|
|
|
886
2287
|
});
|
|
887
2288
|
});
|
|
888
2289
|
});
|
|
2290
|
+
|
|
2291
|
+
// =============================================================================
|
|
2292
|
+
// Regression — show-pony-shaped feature (#998)
|
|
2293
|
+
// =============================================================================
|
|
2294
|
+
// Mirrors the real-world idioms from show-pony/src/features/show-pony/feature.ts
|
|
2295
|
+
// that motivated #998 — inline here (not the live external repo path) so this
|
|
2296
|
+
// test doesn't depend on the show-pony checkout being present in this repo's CI.
|
|
2297
|
+
|
|
2298
|
+
describe("show-pony-shaped feature (regression, #998)", () => {
|
|
2299
|
+
const result = parseInline(`
|
|
2300
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
2301
|
+
import { eventEntity } from "./schema";
|
|
2302
|
+
import { eventCreateHandler } from "./handlers/event-create.write";
|
|
2303
|
+
import { createInviteBrandingQuery } from "./handlers/invite-branding.query";
|
|
2304
|
+
import { registerShowPonyScreens } from "./register/screens";
|
|
2305
|
+
|
|
2306
|
+
const someFeature = { name: "mail-foundation" };
|
|
2307
|
+
|
|
2308
|
+
defineFeature("showpony", (r) => {
|
|
2309
|
+
r.requires(someFeature.name, "config");
|
|
2310
|
+
r.entity("event", eventEntity);
|
|
2311
|
+
r.writeHandler(eventCreateHandler);
|
|
2312
|
+
r.queryHandler(createInviteBrandingQuery());
|
|
2313
|
+
registerShowPonyScreens(r);
|
|
2314
|
+
});
|
|
2315
|
+
`);
|
|
2316
|
+
|
|
2317
|
+
test("requires()'s computed name and entity's imported const resolve via the raw-ref sentinel (#998/#1009 fix)", () => {
|
|
2318
|
+
expect(result.patterns[0]).toMatchObject({
|
|
2319
|
+
kind: "requires",
|
|
2320
|
+
featureNames: [{ __raw: "someFeature.name" }, "config"],
|
|
2321
|
+
});
|
|
2322
|
+
expect(result.patterns[1]).toMatchObject({
|
|
2323
|
+
kind: "entity",
|
|
2324
|
+
entityName: "event",
|
|
2325
|
+
definition: { __raw: "eventEntity" },
|
|
2326
|
+
});
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
test("writeHandler/queryHandler(handlerRef) resolve as opaque patterns (#998/#1007 fix)", () => {
|
|
2330
|
+
expect(result.patterns).toMatchObject([
|
|
2331
|
+
{ kind: "requires" },
|
|
2332
|
+
{ kind: "entity" },
|
|
2333
|
+
{ kind: "writeHandler", handlerName: undefined },
|
|
2334
|
+
{ kind: "queryHandler", handlerName: undefined },
|
|
2335
|
+
]);
|
|
2336
|
+
expect(result.errors).toEqual([]);
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
test("an imported registrar-wrapper stays invisible when its file can't be resolved (in-memory project, #1008)", () => {
|
|
2340
|
+
// parseInline uses an in-memory ts-morph Project — "./register/screens"
|
|
2341
|
+
// isn't a real file, so getModuleSpecifierSourceFile() returns
|
|
2342
|
+
// undefined and the wrapper is skipped like any other unresolvable
|
|
2343
|
+
// call. Real cross-file resolution against on-disk files is covered by
|
|
2344
|
+
// the parseFeatureFile fixture tests below.
|
|
2345
|
+
expect(result.patterns.some((p) => p.kind === "screen" || p.kind === "nav")).toBe(false);
|
|
2346
|
+
});
|
|
2347
|
+
});
|
|
2348
|
+
|
|
2349
|
+
describe("cross-file registrar-wrapper resolution against a real filesystem Project (#1008)", () => {
|
|
2350
|
+
const fixture = resolve(__dirname, "fixtures/cross-file-registrar/feature.ts");
|
|
2351
|
+
const result = parseFeatureFile(fixture);
|
|
2352
|
+
|
|
2353
|
+
test("resolves the imported wrapper and recognises the nav pattern it registers", () => {
|
|
2354
|
+
expect(result.errors).toEqual([]);
|
|
2355
|
+
expect(result.patterns).toMatchObject([
|
|
2356
|
+
{ kind: "requires", featureNames: ["config"] },
|
|
2357
|
+
{ kind: "nav", definition: { id: "foo", label: "Foo" } },
|
|
2358
|
+
]);
|
|
2359
|
+
});
|
|
2360
|
+
|
|
2361
|
+
test("the resolved pattern's source points at the imported file, not the entry file", () => {
|
|
2362
|
+
const navPattern = result.patterns.find((p) => p.kind === "nav");
|
|
2363
|
+
expect(navPattern?.source.file).toBe(
|
|
2364
|
+
resolve(__dirname, "fixtures/cross-file-registrar/screens.ts"),
|
|
2365
|
+
);
|
|
2366
|
+
expect(navPattern?.source.file).not.toBe(fixture);
|
|
2367
|
+
});
|
|
2368
|
+
});
|