@cosmicdrift/kumiko-framework 0.155.1 → 0.156.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 +2 -2
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/raw-table.integration.test.ts +17 -40
- package/src/crypto/__tests__/pii-field-encryption.test.ts +77 -0
- package/src/crypto/index.ts +1 -0
- package/src/crypto/pii-field-encryption.ts +19 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +27 -1
- package/src/db/__tests__/blind-index.integration.test.ts +16 -0
- package/src/db/__tests__/collect-table-metas.test.ts +6 -6
- package/src/db/__tests__/feature-table-sources.test.ts +7 -7
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +1 -1
- package/src/db/__tests__/tenant-db-where-merge.test.ts +39 -0
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/entity-table-meta.ts +1 -1
- package/src/db/feature-table-sources.ts +5 -11
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/queries/shadow-swap.ts +85 -3
- package/src/db/tenant-db.ts +8 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +102 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +16 -0
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/raw-table.test.ts +134 -84
- package/src/engine/__tests__/registry.test.ts +40 -0
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +52 -2
- package/src/engine/boot-validator/action-wiring.ts +2 -2
- package/src/engine/boot-validator/config-deps.ts +35 -0
- package/src/engine/boot-validator/index.ts +2 -0
- package/src/engine/boot-validator/pii-retention.ts +35 -3
- package/src/engine/build-config-feature-schema.ts +3 -3
- package/src/engine/config-helpers.ts +2 -0
- package/src/engine/define-feature.ts +2 -6
- package/src/engine/entity-handlers.ts +2 -4
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +170 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +31 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +6 -1
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +222 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +32 -24
- 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/round5.ts +3 -3
- package/src/engine/feature-ast/parse.ts +3 -3
- package/src/engine/feature-ast/render.ts +16 -11
- package/src/engine/feature-builder-state.ts +0 -3
- package/src/engine/feature-config-events-jobs.ts +3 -6
- package/src/engine/feature-entity-handlers.ts +9 -1
- package/src/engine/feature-ui-extensions.ts +10 -40
- package/src/engine/field-access.ts +13 -2
- package/src/engine/object-form.ts +15 -0
- package/src/engine/registry-facade.ts +4 -0
- package/src/engine/registry-ingest.ts +19 -29
- package/src/engine/registry-state.ts +1 -7
- package/src/engine/registry-validate.ts +5 -28
- package/src/engine/registry.ts +0 -2
- package/src/engine/types/config.ts +7 -0
- package/src/engine/types/feature.ts +40 -68
- package/src/engine/types/fields.ts +6 -0
- package/src/engine/types/index.ts +0 -3
- 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/pipeline/msp-rebuild.ts +4 -0
- package/src/pipeline/projection-rebuild.ts +35 -0
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +8 -1
- package/src/stack/test-stack.ts +17 -2
- package/src/ui-types/index.ts +1 -0
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -172
- package/src/engine/feature-ast/extractors/round4.ts +0 -1227
|
@@ -709,6 +709,93 @@ defineFeature("f", (r) => {
|
|
|
709
709
|
upsertKey: "id",
|
|
710
710
|
});
|
|
711
711
|
});
|
|
712
|
+
|
|
713
|
+
test("object form captures entity, data and upsertKey", () => {
|
|
714
|
+
const result = parseInline(`
|
|
715
|
+
defineFeature("f", (r) => {
|
|
716
|
+
r.referenceData({
|
|
717
|
+
entity: "status",
|
|
718
|
+
data: [{ id: "open" }, { id: "closed" }],
|
|
719
|
+
upsertKey: "id",
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
`);
|
|
723
|
+
|
|
724
|
+
expect(result.errors).toEqual([]);
|
|
725
|
+
expect(result.patterns[0]).toMatchObject({
|
|
726
|
+
kind: "referenceData",
|
|
727
|
+
entityName: "status",
|
|
728
|
+
data: [{ id: "open" }, { id: "closed" }],
|
|
729
|
+
upsertKey: "id",
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("accepts an inline { name: '...' } entity ref, both forms", () => {
|
|
734
|
+
const positional = parseInline(`
|
|
735
|
+
defineFeature("f", (r) => {
|
|
736
|
+
r.referenceData({ name: "status" }, [{ id: "open" }]);
|
|
737
|
+
});
|
|
738
|
+
`);
|
|
739
|
+
expect(positional.patterns[0]).toMatchObject({ kind: "referenceData", entityName: "status" });
|
|
740
|
+
|
|
741
|
+
const objectForm = parseInline(`
|
|
742
|
+
defineFeature("f", (r) => {
|
|
743
|
+
r.referenceData({ entity: { name: "status" }, data: [{ id: "open" }] });
|
|
744
|
+
});
|
|
745
|
+
`);
|
|
746
|
+
expect(objectForm.patterns[0]).toMatchObject({ kind: "referenceData", entityName: "status" });
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("emits ParseError when called with no arguments", () => {
|
|
750
|
+
const result = parseInline(`
|
|
751
|
+
defineFeature("f", (r) => {
|
|
752
|
+
r.referenceData();
|
|
753
|
+
});
|
|
754
|
+
`);
|
|
755
|
+
|
|
756
|
+
expect(result.patterns).toEqual([]);
|
|
757
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("emits ParseError when object form is missing the data property", () => {
|
|
761
|
+
const result = parseInline(`
|
|
762
|
+
defineFeature("f", (r) => {
|
|
763
|
+
r.referenceData({ entity: "status" });
|
|
764
|
+
});
|
|
765
|
+
`);
|
|
766
|
+
|
|
767
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("emits ParseError when object form's data is not an array of plain objects", () => {
|
|
771
|
+
const result = parseInline(`
|
|
772
|
+
defineFeature("f", (r) => {
|
|
773
|
+
r.referenceData({ entity: "status", data: "not-an-array" });
|
|
774
|
+
});
|
|
775
|
+
`);
|
|
776
|
+
|
|
777
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("emits ParseError when the positional data argument is missing", () => {
|
|
781
|
+
const result = parseInline(`
|
|
782
|
+
defineFeature("f", (r) => {
|
|
783
|
+
r.referenceData("status");
|
|
784
|
+
});
|
|
785
|
+
`);
|
|
786
|
+
|
|
787
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test("emits ParseError when positional upsertKey option is not a string", () => {
|
|
791
|
+
const result = parseInline(`
|
|
792
|
+
defineFeature("f", (r) => {
|
|
793
|
+
r.referenceData("status", [{ id: "open" }], { upsertKey: 123 });
|
|
794
|
+
});
|
|
795
|
+
`);
|
|
796
|
+
|
|
797
|
+
expect(result.errors[0]?.methodName).toBe("referenceData");
|
|
798
|
+
});
|
|
712
799
|
});
|
|
713
800
|
|
|
714
801
|
describe("extractUseExtension", () => {
|
|
@@ -740,6 +827,89 @@ defineFeature("f", (r) => {
|
|
|
740
827
|
options: { mode: "verbose" },
|
|
741
828
|
});
|
|
742
829
|
});
|
|
830
|
+
|
|
831
|
+
test("object form captures name, entity ref and options", () => {
|
|
832
|
+
const result = parseInline(`
|
|
833
|
+
defineFeature("f", (r) => {
|
|
834
|
+
r.useExtension({ name: "audit", entity: "task", options: { mode: "verbose" } });
|
|
835
|
+
});
|
|
836
|
+
`);
|
|
837
|
+
|
|
838
|
+
expect(result.errors).toEqual([]);
|
|
839
|
+
expect(result.patterns[0]).toMatchObject({
|
|
840
|
+
kind: "useExtension",
|
|
841
|
+
extensionName: "audit",
|
|
842
|
+
entityName: "task",
|
|
843
|
+
options: { mode: "verbose" },
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("accepts an inline { name: '...' } entity ref, both forms", () => {
|
|
848
|
+
const positional = parseInline(`
|
|
849
|
+
defineFeature("f", (r) => {
|
|
850
|
+
r.useExtension("audit", { name: "task" });
|
|
851
|
+
});
|
|
852
|
+
`);
|
|
853
|
+
expect(positional.patterns[0]).toMatchObject({ kind: "useExtension", entityName: "task" });
|
|
854
|
+
|
|
855
|
+
const objectForm = parseInline(`
|
|
856
|
+
defineFeature("f", (r) => {
|
|
857
|
+
r.useExtension({ name: "audit", entity: { name: "task" } });
|
|
858
|
+
});
|
|
859
|
+
`);
|
|
860
|
+
expect(objectForm.patterns[0]).toMatchObject({ kind: "useExtension", entityName: "task" });
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("emits ParseError when called with no arguments", () => {
|
|
864
|
+
const result = parseInline(`
|
|
865
|
+
defineFeature("f", (r) => {
|
|
866
|
+
r.useExtension();
|
|
867
|
+
});
|
|
868
|
+
`);
|
|
869
|
+
|
|
870
|
+
expect(result.patterns).toEqual([]);
|
|
871
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test("emits ParseError when object form is missing the entity property", () => {
|
|
875
|
+
const result = parseInline(`
|
|
876
|
+
defineFeature("f", (r) => {
|
|
877
|
+
r.useExtension({ name: "audit" });
|
|
878
|
+
});
|
|
879
|
+
`);
|
|
880
|
+
|
|
881
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
test("emits ParseError when object form's options is not a plain object", () => {
|
|
885
|
+
const result = parseInline(`
|
|
886
|
+
defineFeature("f", (r) => {
|
|
887
|
+
r.useExtension({ name: "audit", entity: "task", options: "not-an-object" });
|
|
888
|
+
});
|
|
889
|
+
`);
|
|
890
|
+
|
|
891
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
test("emits ParseError when the positional entity argument is missing", () => {
|
|
895
|
+
const result = parseInline(`
|
|
896
|
+
defineFeature("f", (r) => {
|
|
897
|
+
r.useExtension("audit");
|
|
898
|
+
});
|
|
899
|
+
`);
|
|
900
|
+
|
|
901
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test("emits ParseError when positional options argument is not a plain object", () => {
|
|
905
|
+
const result = parseInline(`
|
|
906
|
+
defineFeature("f", (r) => {
|
|
907
|
+
r.useExtension("audit", "task", "not-an-object");
|
|
908
|
+
});
|
|
909
|
+
`);
|
|
910
|
+
|
|
911
|
+
expect(result.errors[0]?.methodName).toBe("useExtension");
|
|
912
|
+
});
|
|
743
913
|
});
|
|
744
914
|
|
|
745
915
|
// =============================================================================
|
|
@@ -312,6 +312,37 @@ describe("patch coverage for the remaining pattern-kinds", () => {
|
|
|
312
312
|
removePattern(sf, { kind: "defineEvent", eventName: "itemCreated" });
|
|
313
313
|
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "defineEvent")).toBeUndefined();
|
|
314
314
|
});
|
|
315
|
+
|
|
316
|
+
test("add + remove via PatternId for nav (id key)", () => {
|
|
317
|
+
const sf = makeSourceFile(STARTER);
|
|
318
|
+
createFeaturePatcher(sf).addNav({
|
|
319
|
+
definition: { id: "items", label: "Items", screen: "inventory:screen:list" },
|
|
320
|
+
});
|
|
321
|
+
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "nav")).toBeDefined();
|
|
322
|
+
removePattern(sf, { kind: "nav", id: "items" });
|
|
323
|
+
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "nav")).toBeUndefined();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("add + remove via PatternId for workspace (id key)", () => {
|
|
327
|
+
const sf = makeSourceFile(STARTER);
|
|
328
|
+
createFeaturePatcher(sf).addWorkspace({ definition: { id: "admin", label: "Admin" } });
|
|
329
|
+
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "workspace")).toBeDefined();
|
|
330
|
+
removePattern(sf, { kind: "workspace", id: "admin" });
|
|
331
|
+
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "workspace")).toBeUndefined();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("add + remove via PatternId for extendsRegistrar (extensionName key)", () => {
|
|
335
|
+
const sf = makeSourceFile(STARTER);
|
|
336
|
+
addPattern(sf, {
|
|
337
|
+
kind: "extendsRegistrar",
|
|
338
|
+
source: SAMPLE_LOC,
|
|
339
|
+
extensionName: "auditLog",
|
|
340
|
+
defBody: { ...SAMPLE_LOC, raw: "{ hooks: {} }" },
|
|
341
|
+
});
|
|
342
|
+
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "extendsRegistrar")).toBeDefined();
|
|
343
|
+
removePattern(sf, { kind: "extendsRegistrar", extensionName: "auditLog" });
|
|
344
|
+
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "extendsRegistrar")).toBeUndefined();
|
|
345
|
+
});
|
|
315
346
|
});
|
|
316
347
|
|
|
317
348
|
describe("singleton-pattern guards", () => {
|
|
@@ -448,6 +448,7 @@ describe("render → parse roundtrip — r.screen({ nav }) inline sugar", () =>
|
|
|
448
448
|
|
|
449
449
|
const DEFINE_EVENT_WITH_MIGRATIONS_FEATURE = `
|
|
450
450
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
451
|
+
import { z } from "zod";
|
|
451
452
|
|
|
452
453
|
defineFeature("billing", (r) => {
|
|
453
454
|
r.defineEvent("invoicePaid", z.object({ totalCents: z.number() }), {
|
|
@@ -456,7 +457,7 @@ defineFeature("billing", (r) => {
|
|
|
456
457
|
{
|
|
457
458
|
fromVersion: 1,
|
|
458
459
|
toVersion: 2,
|
|
459
|
-
transform: (payload) => ({ totalCents: Math.round(payload.total * 100) }),
|
|
460
|
+
transform: (payload: unknown) => ({ totalCents: Math.round((payload as { total: number }).total * 100) }),
|
|
460
461
|
},
|
|
461
462
|
],
|
|
462
463
|
});
|
|
@@ -556,6 +557,10 @@ defineFeature("teeth-check-positive", (r) => {
|
|
|
556
557
|
test("STATIC_FEATURE's rendered header-data patterns compile clean", () => {
|
|
557
558
|
expect(renderAndCompile(STATIC_FEATURE)).toEqual([]);
|
|
558
559
|
}, 20_000);
|
|
560
|
+
|
|
561
|
+
test("r.defineEvent's rendered migrations array compiles against the real registrar", () => {
|
|
562
|
+
expect(renderAndCompile(DEFINE_EVENT_WITH_MIGRATIONS_FEATURE)).toEqual([]);
|
|
563
|
+
}, 20_000);
|
|
559
564
|
});
|
|
560
565
|
|
|
561
566
|
// One self-contained, compile-clean fixture per remaining pattern kind not
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import type { CallExpression, Node, SourceFile } from "ts-morph";
|
|
2
|
+
import { SyntaxKind } from "ts-morph";
|
|
3
|
+
import type { DefineEventPattern, NotificationPattern } from "../patterns";
|
|
4
|
+
import type { SourceLocation } from "../source-location";
|
|
5
|
+
import { sourceLocationFromNode } from "../source-location";
|
|
6
|
+
import {
|
|
7
|
+
type ExtractOutput,
|
|
8
|
+
fail,
|
|
9
|
+
findFunctionLiteral,
|
|
10
|
+
ok,
|
|
11
|
+
readDataLiteralNode,
|
|
12
|
+
readNameOrRef,
|
|
13
|
+
readPropertyKey,
|
|
14
|
+
} from "./shared";
|
|
15
|
+
|
|
16
|
+
// Reads defineEvent's `migrations` option — either the array-of-steps shape
|
|
17
|
+
// used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
|
|
18
|
+
// toVersion is redundant on-disk since it is always fromVersion + 1) or the
|
|
19
|
+
// keyed-object canonical shape the Designer renders (`{ "1": transform }`).
|
|
20
|
+
// Keyed by fromVersion (as a string) → the transform closure's location.
|
|
21
|
+
// Skips malformed entries rather than failing the whole defineEvent extract
|
|
22
|
+
// — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
|
|
23
|
+
function extractEventMigrationsFromArray(
|
|
24
|
+
arr: Node,
|
|
25
|
+
sourceFile: SourceFile,
|
|
26
|
+
): Record<string, SourceLocation> {
|
|
27
|
+
const result: Record<string, SourceLocation> = {};
|
|
28
|
+
const arrLit = arr.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
29
|
+
for (const el of arrLit.getElements()) {
|
|
30
|
+
const obj = el.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
31
|
+
if (!obj) continue;
|
|
32
|
+
const fromInit = obj
|
|
33
|
+
.getProperty("fromVersion")
|
|
34
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
35
|
+
?.getInitializer();
|
|
36
|
+
const fromVersion = fromInit ? readDataLiteralNode(fromInit) : undefined;
|
|
37
|
+
if (typeof fromVersion !== "number") continue;
|
|
38
|
+
const transformInit = obj
|
|
39
|
+
.getProperty("transform")
|
|
40
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
41
|
+
?.getInitializer();
|
|
42
|
+
const fn = transformInit ? findFunctionLiteral(transformInit) : undefined;
|
|
43
|
+
if (!fn) continue;
|
|
44
|
+
result[String(fromVersion)] = sourceLocationFromNode(fn, sourceFile);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractEventMigrationsFromKeyedObject(
|
|
50
|
+
objLit: Node,
|
|
51
|
+
sourceFile: SourceFile,
|
|
52
|
+
): Record<string, SourceLocation> {
|
|
53
|
+
const result: Record<string, SourceLocation> = {};
|
|
54
|
+
const obj = objLit.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
55
|
+
for (const prop of obj.getProperties()) {
|
|
56
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
57
|
+
const initializer = propAssign?.getInitializer();
|
|
58
|
+
const fn = initializer ? findFunctionLiteral(initializer) : undefined;
|
|
59
|
+
if (!propAssign || !fn) continue;
|
|
60
|
+
result[readPropertyKey(propAssign)] = sourceLocationFromNode(fn, sourceFile);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Reads defineEvent's `migrations` option — either the array-of-steps shape
|
|
66
|
+
// used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
|
|
67
|
+
// toVersion is redundant on-disk since it is always fromVersion + 1) or the
|
|
68
|
+
// keyed-object canonical shape the Designer renders (`{ "1": transform }`).
|
|
69
|
+
// Keyed by fromVersion (as a string) → the transform closure's location.
|
|
70
|
+
// Skips malformed entries rather than failing the whole defineEvent extract
|
|
71
|
+
// — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
|
|
72
|
+
function extractEventMigrationsField(
|
|
73
|
+
node: Node,
|
|
74
|
+
sourceFile: SourceFile,
|
|
75
|
+
): Readonly<Record<string, SourceLocation>> | undefined {
|
|
76
|
+
const result = node.asKind(SyntaxKind.ArrayLiteralExpression)
|
|
77
|
+
? extractEventMigrationsFromArray(node, sourceFile)
|
|
78
|
+
: node.asKind(SyntaxKind.ObjectLiteralExpression)
|
|
79
|
+
? extractEventMigrationsFromKeyedObject(node, sourceFile)
|
|
80
|
+
: undefined;
|
|
81
|
+
return result && Object.keys(result).length > 0 ? result : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function extractDefineEvent(
|
|
85
|
+
call: CallExpression,
|
|
86
|
+
sourceFile: SourceFile,
|
|
87
|
+
): ExtractOutput<DefineEventPattern> {
|
|
88
|
+
const args = call.getArguments();
|
|
89
|
+
const first = args[0];
|
|
90
|
+
if (!first) {
|
|
91
|
+
return fail(
|
|
92
|
+
"defineEvent",
|
|
93
|
+
sourceLocationFromNode(call, sourceFile),
|
|
94
|
+
"expected at least one argument",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
99
|
+
if (obj && args.length === 1) {
|
|
100
|
+
const nameInit = obj
|
|
101
|
+
.getProperty("name")
|
|
102
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
103
|
+
?.getInitializer()
|
|
104
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
105
|
+
if (!nameInit) {
|
|
106
|
+
return fail(
|
|
107
|
+
"defineEvent",
|
|
108
|
+
sourceLocationFromNode(call, sourceFile),
|
|
109
|
+
"object form requires a string-literal `name` property",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const schemaInit = obj
|
|
113
|
+
.getProperty("schema")
|
|
114
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
115
|
+
?.getInitializer();
|
|
116
|
+
if (!schemaInit) {
|
|
117
|
+
return fail(
|
|
118
|
+
"defineEvent",
|
|
119
|
+
sourceLocationFromNode(call, sourceFile),
|
|
120
|
+
"object form requires a `schema` property",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
let version: number | undefined;
|
|
124
|
+
const versionInit = obj
|
|
125
|
+
.getProperty("version")
|
|
126
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
127
|
+
?.getInitializer();
|
|
128
|
+
if (versionInit) {
|
|
129
|
+
const v = readDataLiteralNode(versionInit);
|
|
130
|
+
if (typeof v === "number") version = v;
|
|
131
|
+
}
|
|
132
|
+
const migrationsInit = obj
|
|
133
|
+
.getProperty("migrations")
|
|
134
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
135
|
+
?.getInitializer();
|
|
136
|
+
const migrations = migrationsInit
|
|
137
|
+
? extractEventMigrationsField(migrationsInit, sourceFile)
|
|
138
|
+
: undefined;
|
|
139
|
+
return ok({
|
|
140
|
+
kind: "defineEvent",
|
|
141
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
142
|
+
eventName: nameInit.getLiteralValue(),
|
|
143
|
+
schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
|
|
144
|
+
...(version !== undefined && { version }),
|
|
145
|
+
...(migrations !== undefined && { migrations }),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
150
|
+
if (!nameArg) {
|
|
151
|
+
return fail(
|
|
152
|
+
"defineEvent",
|
|
153
|
+
sourceLocationFromNode(call, sourceFile),
|
|
154
|
+
"first argument must be a string literal event name (or use the object form)",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const schemaArg = args[1];
|
|
158
|
+
if (!schemaArg) {
|
|
159
|
+
return fail(
|
|
160
|
+
"defineEvent",
|
|
161
|
+
sourceLocationFromNode(call, sourceFile),
|
|
162
|
+
"expected a Zod schema as second argument",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
let version: number | undefined;
|
|
166
|
+
let migrations: Readonly<Record<string, SourceLocation>> | undefined;
|
|
167
|
+
const optionsArg = args[2];
|
|
168
|
+
const optionsObj = optionsArg?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
169
|
+
if (optionsObj) {
|
|
170
|
+
const versionInit = optionsObj
|
|
171
|
+
.getProperty("version")
|
|
172
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
173
|
+
?.getInitializer();
|
|
174
|
+
if (versionInit) {
|
|
175
|
+
const v = readDataLiteralNode(versionInit);
|
|
176
|
+
if (typeof v === "number") version = v;
|
|
177
|
+
}
|
|
178
|
+
const migrationsInit = optionsObj
|
|
179
|
+
.getProperty("migrations")
|
|
180
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
181
|
+
?.getInitializer();
|
|
182
|
+
if (migrationsInit) {
|
|
183
|
+
migrations = extractEventMigrationsField(migrationsInit, sourceFile);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return ok({
|
|
187
|
+
kind: "defineEvent",
|
|
188
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
189
|
+
eventName: nameArg.getLiteralValue(),
|
|
190
|
+
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
191
|
+
...(version !== undefined && { version }),
|
|
192
|
+
...(migrations !== undefined && { migrations }),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function extractNotification(
|
|
197
|
+
call: CallExpression,
|
|
198
|
+
sourceFile: SourceFile,
|
|
199
|
+
): ExtractOutput<NotificationPattern> {
|
|
200
|
+
const args = call.getArguments();
|
|
201
|
+
const first = args[0];
|
|
202
|
+
if (!first) {
|
|
203
|
+
return fail(
|
|
204
|
+
"notification",
|
|
205
|
+
sourceLocationFromNode(call, sourceFile),
|
|
206
|
+
"expected at least one argument",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let nameLiteral: ReturnType<typeof first.asKind<SyntaxKind.StringLiteral>>;
|
|
211
|
+
let defObj: ReturnType<typeof first.asKind<SyntaxKind.ObjectLiteralExpression>>;
|
|
212
|
+
|
|
213
|
+
const firstObj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
214
|
+
if (firstObj && args.length === 1) {
|
|
215
|
+
nameLiteral = firstObj
|
|
216
|
+
.getProperty("name")
|
|
217
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
218
|
+
?.getInitializer()
|
|
219
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
220
|
+
if (!nameLiteral) {
|
|
221
|
+
return fail(
|
|
222
|
+
"notification",
|
|
223
|
+
sourceLocationFromNode(call, sourceFile),
|
|
224
|
+
"object form requires a string-literal `name` property",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
defObj = firstObj;
|
|
228
|
+
} else {
|
|
229
|
+
nameLiteral = first.asKind(SyntaxKind.StringLiteral);
|
|
230
|
+
if (!nameLiteral) {
|
|
231
|
+
return fail(
|
|
232
|
+
"notification",
|
|
233
|
+
sourceLocationFromNode(call, sourceFile),
|
|
234
|
+
"first argument must be a string literal notification name (or use the object form)",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
defObj = args[1]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
238
|
+
if (!defObj) {
|
|
239
|
+
return fail(
|
|
240
|
+
"notification",
|
|
241
|
+
sourceLocationFromNode(call, sourceFile),
|
|
242
|
+
"second argument must be an inline definition object",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const nameArg = nameLiteral;
|
|
247
|
+
const triggerObj = defObj
|
|
248
|
+
.getProperty("trigger")
|
|
249
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
250
|
+
?.getInitializer()
|
|
251
|
+
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
252
|
+
if (!triggerObj) {
|
|
253
|
+
return fail(
|
|
254
|
+
"notification",
|
|
255
|
+
sourceLocationFromNode(call, sourceFile),
|
|
256
|
+
"missing or non-object `trigger` property",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const onInit = triggerObj
|
|
260
|
+
.getProperty("on")
|
|
261
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
262
|
+
?.getInitializer();
|
|
263
|
+
const onName = onInit ? readNameOrRef(onInit) : undefined;
|
|
264
|
+
if (!onName) {
|
|
265
|
+
return fail(
|
|
266
|
+
"notification",
|
|
267
|
+
sourceLocationFromNode(call, sourceFile),
|
|
268
|
+
"trigger.on must be a string literal or inline { name } object",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const recipientInit = defObj
|
|
272
|
+
.getProperty("recipient")
|
|
273
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
274
|
+
?.getInitializer();
|
|
275
|
+
const recipientFn = recipientInit ? findFunctionLiteral(recipientInit) : undefined;
|
|
276
|
+
if (!recipientFn) {
|
|
277
|
+
return fail(
|
|
278
|
+
"notification",
|
|
279
|
+
sourceLocationFromNode(call, sourceFile),
|
|
280
|
+
"recipient must be an inline arrow function or function expression",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const dataInit = defObj
|
|
284
|
+
.getProperty("data")
|
|
285
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
286
|
+
?.getInitializer();
|
|
287
|
+
const dataFn = dataInit ? findFunctionLiteral(dataInit) : undefined;
|
|
288
|
+
if (!dataFn) {
|
|
289
|
+
return fail(
|
|
290
|
+
"notification",
|
|
291
|
+
sourceLocationFromNode(call, sourceFile),
|
|
292
|
+
"data must be an inline arrow function or function expression",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
let templates: Record<string, SourceLocation> | undefined;
|
|
296
|
+
const templatesObj = defObj
|
|
297
|
+
.getProperty("templates")
|
|
298
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
299
|
+
?.getInitializer()
|
|
300
|
+
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
301
|
+
if (templatesObj) {
|
|
302
|
+
templates = {};
|
|
303
|
+
for (const prop of templatesObj.getProperties()) {
|
|
304
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
305
|
+
if (!propAssign) continue;
|
|
306
|
+
const init = propAssign.getInitializer();
|
|
307
|
+
if (!init) continue;
|
|
308
|
+
const tfn = findFunctionLiteral(init);
|
|
309
|
+
if (!tfn) continue;
|
|
310
|
+
templates[readPropertyKey(propAssign)] = sourceLocationFromNode(tfn, sourceFile);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return ok({
|
|
314
|
+
kind: "notification",
|
|
315
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
316
|
+
notificationName: nameArg.getLiteralValue(),
|
|
317
|
+
trigger: { on: onName },
|
|
318
|
+
recipientBody: sourceLocationFromNode(recipientFn, sourceFile),
|
|
319
|
+
dataBody: sourceLocationFromNode(dataFn, sourceFile),
|
|
320
|
+
...(templates !== undefined && { templates }),
|
|
321
|
+
});
|
|
322
|
+
}
|