@cosmicdrift/kumiko-framework 0.158.2 → 0.160.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/package.json +7 -2
- package/src/__tests__/consumer-cli.integration.test.ts +110 -0
- package/src/api/__tests__/api.test.ts +65 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +237 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +1 -0
- 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 +150 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +41 -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/api-constants.ts +5 -0
- package/src/api/auth-middleware.ts +48 -59
- package/src/api/auth-routes.ts +51 -17
- package/src/api/index.ts +3 -3
- package/src/api/jwt.ts +148 -7
- package/src/api/pii-leak-guard.ts +5 -2
- package/src/api/routes.ts +57 -0
- package/src/api/server.ts +19 -5
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/query.ts +46 -27
- package/src/consumer-cli.ts +87 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +69 -13
- package/src/crypto/blind-index.ts +8 -4
- package/src/crypto/event-pii.ts +1 -0
- package/src/crypto/kms-adapter.ts +2 -118
- package/src/crypto/pii-field-encryption.ts +49 -15
- package/src/db/__tests__/build-filter-where.test.ts +34 -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 +396 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +5 -5
- package/src/db/blind-index-cleanup.ts +3 -1
- package/src/db/connection.ts +3 -11
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +8 -19
- package/src/db/encryption.ts +2 -3
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +16 -90
- package/src/db/event-store-executor.ts +4 -96
- package/src/db/queries/backfill-pii.ts +1 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/table-builder.ts +2 -19
- package/src/db/tenant-db.ts +6 -55
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -233
- package/src/engine/__tests__/boot-validator.test.ts +46 -0
- package/src/engine/__tests__/codemod-pipeline.test.ts +139 -10
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +28 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/registry-facade-sweep.test.ts +80 -0
- package/src/engine/__tests__/registry.test.ts +40 -0
- package/src/engine/__tests__/store-table.test.ts +12 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/boot-validator/action-wiring.ts +1 -1
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/entity-handler.ts +10 -1
- package/src/engine/boot-validator/entity-list-screens.ts +1 -1
- package/src/engine/boot-validator/gdpr-storage.ts +0 -112
- package/src/engine/boot-validator/index.ts +3 -9
- package/src/engine/boot-validator/screens.ts +1 -1
- package/src/engine/define-feature.ts +2 -0
- package/src/engine/define-handler.ts +11 -91
- package/src/engine/entity-handlers.ts +15 -27
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +11 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +983 -3
- package/src/engine/feature-ast/__tests__/patch.test.ts +168 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +7 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +9 -0
- package/src/engine/feature-ast/extractors/handlers.ts +19 -2
- package/src/engine/feature-ast/extractors/index.ts +1 -0
- package/src/engine/feature-ast/index.ts +2 -0
- package/src/engine/feature-ast/parse.ts +3 -0
- package/src/engine/feature-ast/patch.ts +2 -0
- package/src/engine/feature-ast/patcher.ts +21 -0
- package/src/engine/feature-ast/patterns.ts +16 -0
- package/src/engine/feature-ast/render.ts +15 -0
- package/src/engine/feature-builder-state.ts +6 -0
- package/src/engine/feature-config-events-jobs.ts +1 -1
- package/src/engine/feature-entity-handlers.ts +36 -2
- package/src/engine/feature-ui-extensions.ts +5 -1
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +14 -0
- package/src/engine/index.ts +5 -2
- package/src/engine/ownership.ts +22 -76
- package/src/engine/pattern-library/__tests__/library.test.ts +9 -0
- package/src/engine/pattern-library/library.ts +2 -0
- package/src/engine/pattern-library/mixed-schemas.ts +37 -0
- package/src/engine/registry-facade.ts +9 -0
- package/src/engine/registry-ingest.ts +10 -0
- package/src/engine/registry-state.ts +3 -0
- package/src/engine/registry-validate.ts +1 -1
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/config.ts +2 -497
- 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 -976
- package/src/engine/types/fields.ts +2 -697
- package/src/engine/types/handlers.ts +2 -839
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -72
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +66 -33
- package/src/engine/types/nav.ts +2 -67
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -165
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -793
- 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 -129
- package/src/engine/types/workspace.ts +2 -42
- package/src/entrypoint/index.ts +2 -2
- package/src/errors/write-error-info.ts +6 -22
- package/src/event-store/__tests__/event-store.integration.test.ts +31 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +43 -0
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +28 -51
- package/src/event-store/events-schema.ts +1 -10
- package/src/event-store/index.ts +3 -2
- package/src/event-store/snapshot.ts +11 -35
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/provider-resolver.ts +3 -5
- package/src/files/types.ts +5 -54
- package/src/i18n/required-surface-keys.ts +1 -1
- package/src/jobs/__tests__/jobs.integration.test.ts +102 -1
- package/src/logging/types.ts +1 -7
- 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 -58
- package/src/pipeline/__tests__/dispatcher.test.ts +134 -1
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +180 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +208 -0
- package/src/pipeline/dispatch-shared.ts +51 -3
- package/src/pipeline/dispatch-stream.ts +74 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +7 -0
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +28 -3
- package/src/pipeline/event-dispatcher-admin.ts +4 -0
- package/src/pipeline/event-dispatcher-delivery.ts +29 -3
- package/src/pipeline/event-dispatcher.ts +27 -1
- package/src/pipeline/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/system-hooks.ts +7 -0
- package/src/rate-limit/resolver.ts +10 -30
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/envelope-cipher.test.ts +2 -30
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/envelope-cipher.ts +17 -45
- package/src/secrets/types.ts +2 -177
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/testing/__tests__/late-bound.test.ts +25 -0
- package/src/testing/__tests__/wait-for.test.ts +53 -0
- package/src/testing/boot-validator-fixture.ts +1 -1
- package/src/testing/file-provider-contract.ts +84 -0
- package/src/testing/handler-context.ts +1 -1
- package/src/testing/index.ts +1 -0
- package/src/time/geo-tz.ts +1 -32
- package/src/time/tz-context.ts +9 -56
- package/src/ui-types/index.ts +7 -7
|
@@ -479,6 +479,60 @@ const ENTITY = "task";
|
|
|
479
479
|
defineFeature("f", (r) => {
|
|
480
480
|
r.entity(ENTITY, { fields: {} });
|
|
481
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
|
+
});
|
|
482
536
|
`);
|
|
483
537
|
|
|
484
538
|
expect(result.errors[0]?.methodName).toBe("entity");
|
|
@@ -521,6 +575,71 @@ const taskRef = { name: "task" };
|
|
|
521
575
|
defineFeature("f", (r) => {
|
|
522
576
|
r.relation(taskRef, "owner", { kind: "belongsTo", to: "user" });
|
|
523
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
|
+
});
|
|
524
643
|
`);
|
|
525
644
|
|
|
526
645
|
expect(result.errors[0]?.methodName).toBe("relation");
|
|
@@ -540,6 +659,26 @@ defineFeature("f", (r) => {
|
|
|
540
659
|
definition: { id: "tasks", label: "Tasks", screen: "tasks:screen:list" },
|
|
541
660
|
});
|
|
542
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
|
+
});
|
|
543
682
|
});
|
|
544
683
|
|
|
545
684
|
describe("extractWorkspace", () => {
|
|
@@ -555,6 +694,16 @@ defineFeature("f", (r) => {
|
|
|
555
694
|
definition: { id: "admin", label: "Admin" },
|
|
556
695
|
});
|
|
557
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
|
+
});
|
|
558
707
|
});
|
|
559
708
|
|
|
560
709
|
// =============================================================================
|
|
@@ -588,6 +737,26 @@ defineFeature("f", (r) => {
|
|
|
588
737
|
defineFeature("f", (r) => {
|
|
589
738
|
r.config({});
|
|
590
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
|
+
});
|
|
591
760
|
`);
|
|
592
761
|
|
|
593
762
|
expect(result.errors[0]?.methodName).toBe("config");
|
|
@@ -615,6 +784,26 @@ defineFeature("f", (r) => {
|
|
|
615
784
|
},
|
|
616
785
|
});
|
|
617
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
|
+
});
|
|
618
807
|
});
|
|
619
808
|
|
|
620
809
|
describe("extractMetric", () => {
|
|
@@ -631,6 +820,70 @@ defineFeature("f", (r) => {
|
|
|
631
820
|
options: { type: "counter", description: "API requests" },
|
|
632
821
|
});
|
|
633
822
|
});
|
|
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
|
+
});
|
|
634
887
|
});
|
|
635
888
|
|
|
636
889
|
describe("extractSecret", () => {
|
|
@@ -647,6 +900,30 @@ defineFeature("f", (r) => {
|
|
|
647
900
|
options: { description: "Stripe API key" },
|
|
648
901
|
});
|
|
649
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
|
+
});
|
|
650
927
|
});
|
|
651
928
|
|
|
652
929
|
describe("extractClaimKey", () => {
|
|
@@ -669,6 +946,30 @@ defineFeature("f", (r) => {
|
|
|
669
946
|
defineFeature("f", (r) => {
|
|
670
947
|
r.claimKey("teamId", { type: "bigint" });
|
|
671
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
|
+
});
|
|
672
973
|
`);
|
|
673
974
|
|
|
674
975
|
expect(result.errors[0]?.methodName).toBe("claimKey");
|
|
@@ -767,6 +1068,16 @@ defineFeature("f", (r) => {
|
|
|
767
1068
|
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
768
1069
|
});
|
|
769
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
|
+
|
|
770
1081
|
test("emits ParseError when object form's data is not an array of plain objects", () => {
|
|
771
1082
|
const result = parseInline(`
|
|
772
1083
|
defineFeature("f", (r) => {
|
|
@@ -796,10 +1107,41 @@ defineFeature("f", (r) => {
|
|
|
796
1107
|
|
|
797
1108
|
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
798
1109
|
});
|
|
799
|
-
});
|
|
800
1110
|
|
|
801
|
-
|
|
802
|
-
|
|
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", () => {
|
|
803
1145
|
const result = parseInline(`
|
|
804
1146
|
defineFeature("f", (r) => {
|
|
805
1147
|
r.useExtension("audit", "task");
|
|
@@ -906,6 +1248,37 @@ defineFeature("f", (r) => {
|
|
|
906
1248
|
defineFeature("f", (r) => {
|
|
907
1249
|
r.useExtension("audit", "task", "not-an-object");
|
|
908
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
|
+
});
|
|
909
1282
|
`);
|
|
910
1283
|
|
|
911
1284
|
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
@@ -948,6 +1321,143 @@ defineFeature("f", (r) => {
|
|
|
948
1321
|
defineFeature("f", (r) => {
|
|
949
1322
|
r.hook("postCommit", "task", () => {});
|
|
950
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
|
+
});
|
|
951
1461
|
`);
|
|
952
1462
|
|
|
953
1463
|
expect(result.errors[0]?.methodName).toBe("hook");
|
|
@@ -1004,6 +1514,26 @@ defineFeature("f", (r) => {
|
|
|
1004
1514
|
|
|
1005
1515
|
expect(result.patterns[0]).toMatchObject({ kind: "authClaims" });
|
|
1006
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
|
+
});
|
|
1007
1537
|
});
|
|
1008
1538
|
|
|
1009
1539
|
describe("extractWriteHandler", () => {
|
|
@@ -1092,6 +1622,58 @@ defineFeature("f", (r) => {
|
|
|
1092
1622
|
});
|
|
1093
1623
|
});
|
|
1094
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
|
+
|
|
1095
1677
|
describe("extractJob", () => {
|
|
1096
1678
|
test("captures jobName, options and handler body", () => {
|
|
1097
1679
|
const result = parseInline(`
|
|
@@ -1105,6 +1687,124 @@ defineFeature("f", (r) => {
|
|
|
1105
1687
|
jobName: "daily-cleanup",
|
|
1106
1688
|
});
|
|
1107
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
|
+
});
|
|
1108
1808
|
});
|
|
1109
1809
|
|
|
1110
1810
|
describe("extractHttpRoute", () => {
|
|
@@ -1127,6 +1827,66 @@ defineFeature("f", (r) => {
|
|
|
1127
1827
|
anonymous: true,
|
|
1128
1828
|
});
|
|
1129
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 () => {} });
|
|
1845
|
+
});
|
|
1846
|
+
`);
|
|
1847
|
+
|
|
1848
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
test("emits ParseError when method is not a valid HTTP verb", () => {
|
|
1852
|
+
const result = parseInline(`
|
|
1853
|
+
defineFeature("f", (r) => {
|
|
1854
|
+
r.httpRoute({ method: "FETCH", path: "/feed.xml", handler: async () => {} });
|
|
1855
|
+
});
|
|
1856
|
+
`);
|
|
1857
|
+
|
|
1858
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
test("emits ParseError when path is missing", () => {
|
|
1862
|
+
const result = parseInline(`
|
|
1863
|
+
defineFeature("f", (r) => {
|
|
1864
|
+
r.httpRoute({ method: "GET", handler: async () => {} });
|
|
1865
|
+
});
|
|
1866
|
+
`);
|
|
1867
|
+
|
|
1868
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
test("emits ParseError when handler is missing", () => {
|
|
1872
|
+
const result = parseInline(`
|
|
1873
|
+
defineFeature("f", (r) => {
|
|
1874
|
+
r.httpRoute({ method: "GET", path: "/feed.xml" });
|
|
1875
|
+
});
|
|
1876
|
+
`);
|
|
1877
|
+
|
|
1878
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
test("emits ParseError when handler is not an inline function", () => {
|
|
1882
|
+
const result = parseInline(`
|
|
1883
|
+
defineFeature("f", (r) => {
|
|
1884
|
+
r.httpRoute({ method: "GET", path: "/feed.xml", handler: handlerRef });
|
|
1885
|
+
});
|
|
1886
|
+
`);
|
|
1887
|
+
|
|
1888
|
+
expect(result.errors[0]?.methodName).toBe("httpRoute");
|
|
1889
|
+
});
|
|
1130
1890
|
});
|
|
1131
1891
|
|
|
1132
1892
|
describe("extractDefineEvent", () => {
|
|
@@ -1143,6 +1903,76 @@ defineFeature("f", (r) => {
|
|
|
1143
1903
|
version: 2,
|
|
1144
1904
|
});
|
|
1145
1905
|
});
|
|
1906
|
+
|
|
1907
|
+
test("object form captures name, schema, version and migrations", () => {
|
|
1908
|
+
const result = parseInline(`
|
|
1909
|
+
defineFeature("f", (r) => {
|
|
1910
|
+
r.defineEvent({
|
|
1911
|
+
name: "incidentOpened",
|
|
1912
|
+
schema: z.object({ id: z.string() }),
|
|
1913
|
+
version: 2,
|
|
1914
|
+
migrations: { "1": (payload) => payload },
|
|
1915
|
+
});
|
|
1916
|
+
});
|
|
1917
|
+
`);
|
|
1918
|
+
|
|
1919
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1920
|
+
kind: "defineEvent",
|
|
1921
|
+
eventName: "incidentOpened",
|
|
1922
|
+
version: 2,
|
|
1923
|
+
migrations: { "1": expect.anything() },
|
|
1924
|
+
});
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
test("emits ParseError when called with no arguments", () => {
|
|
1928
|
+
const result = parseInline(`
|
|
1929
|
+
defineFeature("f", (r) => {
|
|
1930
|
+
r.defineEvent();
|
|
1931
|
+
});
|
|
1932
|
+
`);
|
|
1933
|
+
|
|
1934
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
test("emits ParseError when object form is missing the name property", () => {
|
|
1938
|
+
const result = parseInline(`
|
|
1939
|
+
defineFeature("f", (r) => {
|
|
1940
|
+
r.defineEvent({ schema: z.object({ id: z.string() }) });
|
|
1941
|
+
});
|
|
1942
|
+
`);
|
|
1943
|
+
|
|
1944
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
test("emits ParseError when object form is missing the schema property", () => {
|
|
1948
|
+
const result = parseInline(`
|
|
1949
|
+
defineFeature("f", (r) => {
|
|
1950
|
+
r.defineEvent({ name: "incidentOpened" });
|
|
1951
|
+
});
|
|
1952
|
+
`);
|
|
1953
|
+
|
|
1954
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
test("emits ParseError when the event name is not a string literal", () => {
|
|
1958
|
+
const result = parseInline(`
|
|
1959
|
+
defineFeature("f", (r) => {
|
|
1960
|
+
r.defineEvent(EVENT, z.object({ id: z.string() }));
|
|
1961
|
+
});
|
|
1962
|
+
`);
|
|
1963
|
+
|
|
1964
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
test("emits ParseError when the schema argument is missing", () => {
|
|
1968
|
+
const result = parseInline(`
|
|
1969
|
+
defineFeature("f", (r) => {
|
|
1970
|
+
r.defineEvent("incidentOpened");
|
|
1971
|
+
});
|
|
1972
|
+
`);
|
|
1973
|
+
|
|
1974
|
+
expect(result.errors[0]?.methodName).toBe("defineEvent");
|
|
1975
|
+
});
|
|
1146
1976
|
});
|
|
1147
1977
|
|
|
1148
1978
|
describe("extractDefineEvent — migrations (formerly extractEventMigration)", () => {
|
|
@@ -1187,6 +2017,26 @@ defineFeature("f", (r) => {
|
|
|
1187
2017
|
migrations: { "1": expect.anything() },
|
|
1188
2018
|
});
|
|
1189
2019
|
});
|
|
2020
|
+
|
|
2021
|
+
test("skips malformed migration array entries instead of failing the whole extract", () => {
|
|
2022
|
+
const result = parseInline(`
|
|
2023
|
+
defineFeature("f", (r) => {
|
|
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
|
+
],
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
`);
|
|
2033
|
+
|
|
2034
|
+
expect(result.errors).toEqual([]);
|
|
2035
|
+
expect(result.patterns[0]).toMatchObject({
|
|
2036
|
+
kind: "defineEvent",
|
|
2037
|
+
migrations: { "1": expect.anything() },
|
|
2038
|
+
});
|
|
2039
|
+
});
|
|
1190
2040
|
});
|
|
1191
2041
|
|
|
1192
2042
|
describe("extractNotification", () => {
|
|
@@ -1207,6 +2057,136 @@ defineFeature("f", (r) => {
|
|
|
1207
2057
|
trigger: { on: "incident:create" },
|
|
1208
2058
|
});
|
|
1209
2059
|
});
|
|
2060
|
+
|
|
2061
|
+
test("object form captures name + trigger/recipient/data", () => {
|
|
2062
|
+
const result = parseInline(`
|
|
2063
|
+
defineFeature("f", (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
|
+
});
|
|
2070
|
+
});
|
|
2071
|
+
`);
|
|
2072
|
+
|
|
2073
|
+
expect(result.patterns[0]).toMatchObject({
|
|
2074
|
+
kind: "notification",
|
|
2075
|
+
notificationName: "incidentOpened",
|
|
2076
|
+
});
|
|
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
|
+
});
|
|
2091
|
+
});
|
|
2092
|
+
`);
|
|
2093
|
+
|
|
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", () => {
|
|
2100
|
+
const result = parseInline(`
|
|
2101
|
+
defineFeature("f", (r) => {
|
|
2102
|
+
r.notification();
|
|
2103
|
+
});
|
|
2104
|
+
`);
|
|
2105
|
+
|
|
2106
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
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: () => ({}) });
|
|
2113
|
+
});
|
|
2114
|
+
`);
|
|
2115
|
+
|
|
2116
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
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: () => ({}) });
|
|
2123
|
+
});
|
|
2124
|
+
`);
|
|
2125
|
+
|
|
2126
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
test("emits ParseError when the definition object is missing", () => {
|
|
2130
|
+
const result = parseInline(`
|
|
2131
|
+
defineFeature("f", (r) => {
|
|
2132
|
+
r.notification("incidentOpened");
|
|
2133
|
+
});
|
|
2134
|
+
`);
|
|
2135
|
+
|
|
2136
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
test("emits ParseError when trigger is missing", () => {
|
|
2140
|
+
const result = parseInline(`
|
|
2141
|
+
defineFeature("f", (r) => {
|
|
2142
|
+
r.notification("incidentOpened", { recipient: () => ({}), data: () => ({}) });
|
|
2143
|
+
});
|
|
2144
|
+
`);
|
|
2145
|
+
|
|
2146
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
2147
|
+
});
|
|
2148
|
+
|
|
2149
|
+
test("emits ParseError when trigger.on is not a string/ref", () => {
|
|
2150
|
+
const result = parseInline(`
|
|
2151
|
+
defineFeature("f", (r) => {
|
|
2152
|
+
r.notification("incidentOpened", {
|
|
2153
|
+
trigger: { on: 123 },
|
|
2154
|
+
recipient: () => ({}),
|
|
2155
|
+
data: () => ({}),
|
|
2156
|
+
});
|
|
2157
|
+
});
|
|
2158
|
+
`);
|
|
2159
|
+
|
|
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: () => ({}),
|
|
2170
|
+
});
|
|
2171
|
+
});
|
|
2172
|
+
`);
|
|
2173
|
+
|
|
2174
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
test("emits ParseError when data is not an inline function", () => {
|
|
2178
|
+
const result = parseInline(`
|
|
2179
|
+
defineFeature("f", (r) => {
|
|
2180
|
+
r.notification("incidentOpened", {
|
|
2181
|
+
trigger: { on: "incident:create" },
|
|
2182
|
+
recipient: () => ({}),
|
|
2183
|
+
data: dataRef,
|
|
2184
|
+
});
|
|
2185
|
+
});
|
|
2186
|
+
`);
|
|
2187
|
+
|
|
2188
|
+
expect(result.errors[0]?.methodName).toBe("notification");
|
|
2189
|
+
});
|
|
1210
2190
|
});
|
|
1211
2191
|
|
|
1212
2192
|
describe("extractProjection", () => {
|