@eventvisor/core 0.23.0 → 0.24.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/linter/conditionsSchema.d.ts +1 -301
  3. package/lib/linter/conditionsSchema.js +8 -14
  4. package/lib/linter/conditionsSchema.js.map +1 -1
  5. package/lib/linter/destinationSchema.d.ts +89 -1
  6. package/lib/linter/destinationSchema.js +0 -2
  7. package/lib/linter/destinationSchema.js.map +1 -1
  8. package/lib/linter/effectSchema.d.ts +9 -1209
  9. package/lib/linter/effectSchema.js +6 -3
  10. package/lib/linter/effectSchema.js.map +1 -1
  11. package/lib/linter/entitySchemas.spec.d.ts +1 -0
  12. package/lib/linter/entitySchemas.spec.js +169 -0
  13. package/lib/linter/entitySchemas.spec.js.map +1 -0
  14. package/lib/linter/eventSchema.js +5 -3
  15. package/lib/linter/eventSchema.js.map +1 -1
  16. package/lib/linter/lintProject.d.ts +5 -0
  17. package/lib/linter/lintProject.js +141 -5
  18. package/lib/linter/lintProject.js.map +1 -1
  19. package/lib/linter/lintProject.spec.d.ts +1 -0
  20. package/lib/linter/lintProject.spec.js +103 -0
  21. package/lib/linter/lintProject.spec.js.map +1 -0
  22. package/lib/linter/persistSchema.d.ts +4 -604
  23. package/lib/linter/persistSchema.js +4 -2
  24. package/lib/linter/persistSchema.js.map +1 -1
  25. package/lib/linter/printError.js +4 -0
  26. package/lib/linter/printError.js.map +1 -1
  27. package/lib/linter/sampleSchema.d.ts +13 -285
  28. package/lib/linter/sampleSchema.js +2 -1
  29. package/lib/linter/sampleSchema.js.map +1 -1
  30. package/lib/linter/semanticValidation.d.ts +11 -0
  31. package/lib/linter/semanticValidation.js +592 -0
  32. package/lib/linter/semanticValidation.js.map +1 -0
  33. package/lib/linter/semanticValidation.spec.d.ts +1 -0
  34. package/lib/linter/semanticValidation.spec.js +190 -0
  35. package/lib/linter/semanticValidation.spec.js.map +1 -0
  36. package/lib/linter/testSchema.d.ts +64 -3
  37. package/lib/linter/testSchema.js +81 -4
  38. package/lib/linter/testSchema.js.map +1 -1
  39. package/lib/linter/testSchema.spec.d.ts +1 -0
  40. package/lib/linter/testSchema.spec.js +260 -0
  41. package/lib/linter/testSchema.spec.js.map +1 -0
  42. package/package.json +5 -5
  43. package/src/linter/conditionsSchema.ts +12 -18
  44. package/src/linter/destinationSchema.ts +0 -3
  45. package/src/linter/effectSchema.ts +12 -9
  46. package/src/linter/entitySchemas.spec.ts +212 -0
  47. package/src/linter/eventSchema.ts +8 -6
  48. package/src/linter/lintProject.spec.ts +112 -0
  49. package/src/linter/lintProject.ts +134 -6
  50. package/src/linter/persistSchema.ts +6 -4
  51. package/src/linter/printError.ts +3 -0
  52. package/src/linter/sampleSchema.ts +3 -1
  53. package/src/linter/semanticValidation.spec.ts +239 -0
  54. package/src/linter/semanticValidation.ts +953 -0
  55. package/src/linter/testSchema.spec.ts +279 -0
  56. package/src/linter/testSchema.ts +89 -4
@@ -52,6 +52,7 @@ export function getConditionsSchema(deps: Dependencies) {
52
52
  "notIn",
53
53
  ]),
54
54
  value: z.any().optional(),
55
+ regexFlags: z.string().optional(),
55
56
  })
56
57
  .refine(...getSourceBaseRefine())
57
58
  // @TODO: refine "value" type against each "operator"
@@ -66,24 +67,17 @@ export function getConditionsSchema(deps: Dependencies) {
66
67
  { message: "Value is required for all operators except exists and notExists" },
67
68
  );
68
69
 
69
- const andConditionSchema = z.object({
70
- and: z.array(plainConditionSchema),
71
- });
72
-
73
- const orConditionSchema = z.object({
74
- or: z.array(plainConditionSchema),
75
- });
76
-
77
- const notConditionSchema = z.object({
78
- not: z.array(plainConditionSchema),
79
- });
80
-
81
- const conditionSchema = z.union([
82
- plainConditionSchema,
83
- andConditionSchema,
84
- orConditionSchema,
85
- notConditionSchema,
86
- ]);
70
+ const stringConditionSchema = z.string();
71
+
72
+ const conditionSchema: z.ZodTypeAny = z.lazy(() =>
73
+ z.union([
74
+ plainConditionSchema,
75
+ z.object({ and: z.array(conditionSchema) }).strict(),
76
+ z.object({ or: z.array(conditionSchema) }).strict(),
77
+ z.object({ not: z.array(conditionSchema) }).strict(),
78
+ stringConditionSchema,
79
+ ]),
80
+ );
87
81
 
88
82
  return z.union([conditionSchema, z.array(conditionSchema)]);
89
83
  }
@@ -1,7 +1,6 @@
1
1
  import * as z from "zod";
2
2
  import { Dependencies } from "../dependencies";
3
3
 
4
- import { JSONZodSchema } from "./jsonSchema";
5
4
  import { getTagsSchema } from "./tagsSchema";
6
5
  import { getConditionsSchema } from "./conditionsSchema";
7
6
  import { getSampleSchema } from "./sampleSchema";
@@ -10,8 +9,6 @@ import { getTransformsSchema } from "./transformsSchema";
10
9
  export function getDestinationSchema(deps: Dependencies) {
11
10
  return z
12
11
  .object({
13
- ...JSONZodSchema.shape,
14
-
15
12
  archived: z.boolean().optional(),
16
13
  description: z.string(),
17
14
  tags: getTagsSchema(deps),
@@ -13,6 +13,7 @@ export function getEffectSchema(deps: Dependencies) {
13
13
  event_tracked: z.array(z.string()).optional(),
14
14
  attribute_set: z.array(z.string()).optional(),
15
15
  })
16
+ .strict()
16
17
  .refine(
17
18
  (data) => {
18
19
  return Object.values(data).some((value) => value !== undefined);
@@ -24,14 +25,16 @@ export function getEffectSchema(deps: Dependencies) {
24
25
  );
25
26
  const effectOn = z.union([z.array(effectOnType), effectOnRecord]);
26
27
 
27
- const step = z.object({
28
- description: z.string().optional(),
29
- handler: z.string().optional(),
30
- conditions: getConditionsSchema(deps).optional(),
31
- params: z.record(z.string(), z.any()).optional(),
32
- transforms: getTransformsSchema(deps).optional(),
33
- continueOnError: z.boolean().optional(),
34
- });
28
+ const step = z
29
+ .object({
30
+ description: z.string().optional(),
31
+ handler: z.string().optional(),
32
+ conditions: getConditionsSchema(deps).optional(),
33
+ params: z.record(z.string(), z.any()).optional(),
34
+ transforms: getTransformsSchema(deps).optional(),
35
+ continueOnError: z.boolean().optional(),
36
+ })
37
+ .strict();
35
38
 
36
39
  return z
37
40
  .object({
@@ -42,7 +45,7 @@ export function getEffectSchema(deps: Dependencies) {
42
45
  on: effectOn,
43
46
  state: z.any().optional(),
44
47
  conditions: getConditionsSchema(deps).optional(),
45
- steps: z.array(step),
48
+ steps: z.array(step).optional(),
46
49
  persist: getPersistSchema(deps).optional(),
47
50
  })
48
51
  .strict();
@@ -0,0 +1,212 @@
1
+ import { getAttributeSchema } from "./attributeSchema";
2
+ import { getDestinationSchema } from "./destinationSchema";
3
+ import { getEffectSchema } from "./effectSchema";
4
+ import { getEventSchema } from "./eventSchema";
5
+ import { getConditionsSchema } from "./conditionsSchema";
6
+ import { getSampleSchema } from "./sampleSchema";
7
+ import type { Dependencies } from "../dependencies";
8
+
9
+ function createDeps(): Dependencies {
10
+ return {
11
+ rootDirectoryPath: "/tmp/eventvisor",
12
+ projectConfig: {
13
+ eventsDirectoryPath: "/tmp/eventvisor/events",
14
+ attributesDirectoryPath: "/tmp/eventvisor/attributes",
15
+ destinationsDirectoryPath: "/tmp/eventvisor/destinations",
16
+ statesDirectoryPath: "/tmp/eventvisor/states",
17
+ effectsDirectoryPath: "/tmp/eventvisor/effects",
18
+ testsDirectoryPath: "/tmp/eventvisor/tests",
19
+ datafilesDirectoryPath: "/tmp/eventvisor/datafiles",
20
+ systemDirectoryPath: "/tmp/eventvisor/.eventvisor",
21
+ catalogExportDirectoryPath: "/tmp/eventvisor/out",
22
+ datafileNamePattern: "eventvisor-%s.json",
23
+ tags: ["all", "web"],
24
+ adapter: class {},
25
+ plugins: [],
26
+ parser: { extension: "yml", parse: jest.fn(), stringify: jest.fn() },
27
+ prettyDatafile: false,
28
+ stringify: true,
29
+ },
30
+ datasource: {} as Dependencies["datasource"],
31
+ options: {},
32
+ };
33
+ }
34
+
35
+ describe("entity lint schemas", () => {
36
+ const deps = createDeps();
37
+
38
+ it("accepts string and nested conditions including regexFlags", () => {
39
+ const schema = getConditionsSchema(deps);
40
+
41
+ expect(schema.safeParse("*").success).toBe(true);
42
+ expect(
43
+ schema.safeParse({
44
+ and: [
45
+ "*",
46
+ {
47
+ or: [
48
+ {
49
+ payload: "url",
50
+ operator: "matches",
51
+ value: "^https://",
52
+ regexFlags: "i",
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ }).success,
58
+ ).toBe(true);
59
+ });
60
+
61
+ it("accepts arrays of samples", () => {
62
+ const schema = getSampleSchema(deps);
63
+
64
+ const result = schema.safeParse([
65
+ {
66
+ by: "userId",
67
+ percentage: 50,
68
+ },
69
+ {
70
+ by: { payload: "country" },
71
+ range: [50, 100],
72
+ },
73
+ ]);
74
+
75
+ expect(result.success).toBe(true);
76
+ });
77
+
78
+ it("requires attribute metadata", () => {
79
+ const schema = getAttributeSchema(deps);
80
+
81
+ expect(
82
+ schema.safeParse({
83
+ type: "string",
84
+ }).success,
85
+ ).toBe(false);
86
+
87
+ expect(
88
+ schema.safeParse({
89
+ description: "User ID attribute",
90
+ tags: ["web"],
91
+ type: "string",
92
+ }).success,
93
+ ).toBe(true);
94
+ });
95
+
96
+ it("requires event metadata and keeps strict skipValidation/destination overrides", () => {
97
+ const schema = getEventSchema(deps);
98
+
99
+ expect(
100
+ schema.safeParse({
101
+ description: "Page view",
102
+ tags: ["web"],
103
+ type: "object",
104
+ skipValidation: {
105
+ conditions: "*",
106
+ },
107
+ destinations: {
108
+ console: {
109
+ sample: [
110
+ {
111
+ by: "userId",
112
+ percentage: 50,
113
+ },
114
+ ],
115
+ },
116
+ },
117
+ }).success,
118
+ ).toBe(true);
119
+
120
+ expect(
121
+ schema.safeParse({
122
+ description: "Page view",
123
+ tags: ["web"],
124
+ type: "object",
125
+ skipValidation: {
126
+ conditions: "*",
127
+ extra: true,
128
+ },
129
+ }).success,
130
+ ).toBe(false);
131
+
132
+ expect(
133
+ schema.safeParse({
134
+ type: "object",
135
+ }).success,
136
+ ).toBe(false);
137
+ });
138
+
139
+ it("requires destination metadata and rejects JSON schema-only fields", () => {
140
+ const schema = getDestinationSchema(deps);
141
+
142
+ expect(
143
+ schema.safeParse({
144
+ description: "Console destination",
145
+ tags: ["web"],
146
+ transport: "console",
147
+ type: "object",
148
+ }).success,
149
+ ).toBe(false);
150
+
151
+ expect(
152
+ schema.safeParse({
153
+ description: "Console destination",
154
+ tags: ["web"],
155
+ transport: "console",
156
+ sample: [
157
+ {
158
+ by: "userId",
159
+ percentage: 100,
160
+ },
161
+ ],
162
+ }).success,
163
+ ).toBe(true);
164
+
165
+ expect(
166
+ schema.safeParse({
167
+ transport: "console",
168
+ }).success,
169
+ ).toBe(false);
170
+ });
171
+
172
+ it("requires effect metadata and on, and rejects unknown step fields", () => {
173
+ const schema = getEffectSchema(deps);
174
+
175
+ expect(
176
+ schema.safeParse({
177
+ description: "Inject cookie banner",
178
+ tags: ["web"],
179
+ on: {
180
+ event_tracked: ["page_view"],
181
+ },
182
+ state: {
183
+ injected: false,
184
+ },
185
+ }).success,
186
+ ).toBe(true);
187
+
188
+ expect(
189
+ schema.safeParse({
190
+ description: "Inject cookie banner",
191
+ tags: ["web"],
192
+ on: {
193
+ event_tracked: ["page_view"],
194
+ },
195
+ steps: [
196
+ {
197
+ handler: "pixel",
198
+ unknown: true,
199
+ },
200
+ ],
201
+ }).success,
202
+ ).toBe(false);
203
+
204
+ expect(
205
+ schema.safeParse({
206
+ description: "Inject cookie banner",
207
+ tags: ["web"],
208
+ steps: [],
209
+ }).success,
210
+ ).toBe(false);
211
+ });
212
+ });
@@ -18,7 +18,7 @@ export function getEventSchema(deps: Dependencies) {
18
18
  tags: getTagsSchema(deps),
19
19
 
20
20
  skipValidation: z
21
- .union([z.boolean(), z.object({ conditions: getConditionsSchema(deps) })])
21
+ .union([z.boolean(), z.object({ conditions: getConditionsSchema(deps) }).strict()])
22
22
  .optional(),
23
23
  level: z.enum(["fatal", "error", "warning", "log", "info", "debug"]).optional(),
24
24
  requiredAttributes: z.array(z.string()).optional(),
@@ -30,11 +30,13 @@ export function getEventSchema(deps: Dependencies) {
30
30
  z.string(), // @TODO: get real destination names here
31
31
  z.union([
32
32
  z.boolean(),
33
- z.object({
34
- conditions: getConditionsSchema(deps).optional(),
35
- sample: getSampleSchema(deps).optional(),
36
- transforms: getTransformsSchema(deps).optional(),
37
- }),
33
+ z
34
+ .object({
35
+ conditions: getConditionsSchema(deps).optional(),
36
+ sample: getSampleSchema(deps).optional(),
37
+ transforms: getTransformsSchema(deps).optional(),
38
+ })
39
+ .strict(),
38
40
  ]),
39
41
  )
40
42
  .optional(),
@@ -0,0 +1,112 @@
1
+ import type { Dependencies } from "../dependencies";
2
+
3
+ jest.mock("chalk", () => ({
4
+ __esModule: true,
5
+ default: {
6
+ bold: {
7
+ red: {
8
+ underline: jest.fn((value: string) => value),
9
+ },
10
+ },
11
+ },
12
+ }));
13
+
14
+ import { lintProject } from "./lintProject";
15
+
16
+ function createDeps(testContent: Record<string, any>): Dependencies {
17
+ return {
18
+ rootDirectoryPath: "/tmp/eventvisor",
19
+ projectConfig: {
20
+ eventsDirectoryPath: "/tmp/eventvisor/events",
21
+ attributesDirectoryPath: "/tmp/eventvisor/attributes",
22
+ destinationsDirectoryPath: "/tmp/eventvisor/destinations",
23
+ statesDirectoryPath: "/tmp/eventvisor/states",
24
+ effectsDirectoryPath: "/tmp/eventvisor/effects",
25
+ testsDirectoryPath: "/tmp/eventvisor/tests",
26
+ datafilesDirectoryPath: "/tmp/eventvisor/datafiles",
27
+ systemDirectoryPath: "/tmp/eventvisor/.eventvisor",
28
+ catalogExportDirectoryPath: "/tmp/eventvisor/out",
29
+ datafileNamePattern: "eventvisor-%s.json",
30
+ tags: ["all"],
31
+ adapter: class {},
32
+ plugins: [],
33
+ parser: { extension: "yml", parse: jest.fn(), stringify: jest.fn() },
34
+ prettyDatafile: false,
35
+ stringify: true,
36
+ },
37
+ datasource: {
38
+ listAttributes: jest.fn().mockResolvedValue([]),
39
+ listEvents: jest.fn().mockResolvedValue(["page_view"]),
40
+ listDestinations: jest.fn().mockResolvedValue([]),
41
+ listEffects: jest.fn().mockResolvedValue([]),
42
+ listTests: jest.fn().mockResolvedValue(["events/page_view.spec"]),
43
+ readEvent: jest.fn().mockResolvedValue({
44
+ description: "Page view",
45
+ tags: ["all"],
46
+ type: "object",
47
+ properties: {
48
+ url: {
49
+ type: "string",
50
+ },
51
+ },
52
+ }),
53
+ readTest: jest.fn().mockResolvedValue(testContent),
54
+ } as unknown as Dependencies["datasource"],
55
+ options: {},
56
+ };
57
+ }
58
+
59
+ describe("lintProject", () => {
60
+ let logSpy: jest.SpyInstance;
61
+
62
+ beforeEach(() => {
63
+ logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
64
+ });
65
+
66
+ afterEach(() => {
67
+ logSpy.mockRestore();
68
+ });
69
+
70
+ it("includes test specs in linting", async () => {
71
+ const deps = createDeps({
72
+ event: "page_view",
73
+ assertions: [
74
+ {
75
+ expectedDestinationsByTag: {
76
+ marketing: ["console"],
77
+ },
78
+ },
79
+ ],
80
+ });
81
+
82
+ const result = await lintProject(deps);
83
+
84
+ expect(result).toBe(false);
85
+ expect(deps.datasource.listTests).toHaveBeenCalled();
86
+ expect(deps.datasource.readTest).toHaveBeenCalledWith("events/page_view.spec");
87
+ });
88
+
89
+ it("supports filtering by entityType=test", async () => {
90
+ const deps = createDeps({
91
+ event: "page_view",
92
+ assertions: [
93
+ {
94
+ track: {
95
+ url: "https://example.com",
96
+ },
97
+ expectedToBeValid: true,
98
+ },
99
+ ],
100
+ });
101
+
102
+ const result = await lintProject(deps, { entityType: "test" });
103
+
104
+ expect(result).toBe(true);
105
+ expect(deps.datasource.listAttributes).toHaveBeenCalled();
106
+ expect(deps.datasource.listEvents).toHaveBeenCalled();
107
+ expect(deps.datasource.listDestinations).toHaveBeenCalled();
108
+ expect(deps.datasource.listEffects).toHaveBeenCalled();
109
+ expect(deps.datasource.listTests).toHaveBeenCalled();
110
+ expect(deps.datasource.readTest).toHaveBeenCalledWith("events/page_view.spec");
111
+ });
112
+ });
@@ -1,3 +1,5 @@
1
+ import * as z from "zod";
2
+
1
3
  import { Plugin } from "../cli";
2
4
  import { Dependencies } from "../dependencies";
3
5
 
@@ -5,9 +7,42 @@ import { getAttributeSchema } from "./attributeSchema";
5
7
  import { getDestinationSchema } from "./destinationSchema";
6
8
  import { getEffectSchema } from "./effectSchema";
7
9
  import { getEventSchema } from "./eventSchema";
10
+ import { getTestSchema } from "./testSchema";
8
11
  import { printError } from "./printError";
12
+ import { getSemanticIssues, type LintContext } from "./semanticValidation";
13
+
14
+ async function createLintContext(options: Dependencies): Promise<LintContext> {
15
+ const { datasource } = options;
16
+
17
+ const [attributeNames, eventNames, destinationNames, effectNames] = await Promise.all([
18
+ datasource.listAttributes(),
19
+ datasource.listEvents(),
20
+ datasource.listDestinations(),
21
+ datasource.listEffects(),
22
+ ]);
23
+
24
+ const [attributes, events, destinations, effects] = await Promise.all([
25
+ Promise.all(
26
+ attributeNames.map(async (name) => [name, await datasource.readAttribute(name)] as const),
27
+ ),
28
+ Promise.all(eventNames.map(async (name) => [name, await datasource.readEvent(name)] as const)),
29
+ Promise.all(
30
+ destinationNames.map(async (name) => [name, await datasource.readDestination(name)] as const),
31
+ ),
32
+ Promise.all(
33
+ effectNames.map(async (name) => [name, await datasource.readEffect(name)] as const),
34
+ ),
35
+ ]);
36
+
37
+ return {
38
+ attributes: Object.fromEntries(attributes),
39
+ events: Object.fromEntries(events),
40
+ destinations: Object.fromEntries(destinations),
41
+ effects: Object.fromEntries(effects),
42
+ };
43
+ }
9
44
 
10
- async function lintProject(
45
+ export async function lintProject(
11
46
  options: Dependencies,
12
47
  filterOptions: {
13
48
  keyPattern?: string;
@@ -16,6 +51,7 @@ async function lintProject(
16
51
  ): Promise<boolean> {
17
52
  const { projectConfig, datasource } = options;
18
53
  const { keyPattern, entityType } = filterOptions;
54
+ const lintContext = await createLintContext(options);
19
55
 
20
56
  let hasErrors = false;
21
57
 
@@ -35,7 +71,7 @@ async function lintProject(
35
71
  }
36
72
 
37
73
  const attributeContent = await datasource.readAttribute(attributeKey);
38
- const result = attributeSchema.safeParse(attributeContent);
74
+ const result = await attributeSchema.safeParseAsync(attributeContent);
39
75
 
40
76
  if (!result.success) {
41
77
  printError({
@@ -45,6 +81,19 @@ async function lintProject(
45
81
  projectConfig,
46
82
  });
47
83
  hasErrors = true;
84
+ continue;
85
+ }
86
+
87
+ const semanticIssues = getSemanticIssues("attribute", result.data as any, lintContext);
88
+
89
+ if (semanticIssues.length > 0) {
90
+ printError({
91
+ entityType: "attribute",
92
+ entityKey: attributeKey,
93
+ error: new z.ZodError(semanticIssues),
94
+ projectConfig,
95
+ });
96
+ hasErrors = true;
48
97
  }
49
98
  }
50
99
 
@@ -64,7 +113,7 @@ async function lintProject(
64
113
  }
65
114
 
66
115
  const eventContent = await datasource.readEvent(eventKey);
67
- const result = eventSchema.safeParse(eventContent);
116
+ const result = await eventSchema.safeParseAsync(eventContent);
68
117
 
69
118
  if (!result.success) {
70
119
  printError({
@@ -75,6 +124,19 @@ async function lintProject(
75
124
  });
76
125
 
77
126
  hasErrors = true;
127
+ continue;
128
+ }
129
+
130
+ const semanticIssues = getSemanticIssues("event", result.data as any, lintContext);
131
+
132
+ if (semanticIssues.length > 0) {
133
+ printError({
134
+ entityType: "event",
135
+ entityKey: eventKey,
136
+ error: new z.ZodError(semanticIssues),
137
+ projectConfig,
138
+ });
139
+ hasErrors = true;
78
140
  }
79
141
  }
80
142
 
@@ -94,7 +156,7 @@ async function lintProject(
94
156
  }
95
157
 
96
158
  const destinationContent = await datasource.readDestination(destinationKey);
97
- const result = destinationSchema.safeParse(destinationContent);
159
+ const result = await destinationSchema.safeParseAsync(destinationContent);
98
160
 
99
161
  if (!result.success) {
100
162
  printError({
@@ -104,6 +166,19 @@ async function lintProject(
104
166
  projectConfig,
105
167
  });
106
168
  hasErrors = true;
169
+ continue;
170
+ }
171
+
172
+ const semanticIssues = getSemanticIssues("destination", result.data as any, lintContext);
173
+
174
+ if (semanticIssues.length > 0) {
175
+ printError({
176
+ entityType: "destination",
177
+ entityKey: destinationKey,
178
+ error: new z.ZodError(semanticIssues),
179
+ projectConfig,
180
+ });
181
+ hasErrors = true;
107
182
  }
108
183
  }
109
184
 
@@ -123,7 +198,7 @@ async function lintProject(
123
198
  }
124
199
 
125
200
  const effectContent = await datasource.readEffect(effectKey);
126
- const result = effectSchema.safeParse(effectContent);
201
+ const result = await effectSchema.safeParseAsync(effectContent);
127
202
 
128
203
  if (!result.success) {
129
204
  printError({
@@ -133,10 +208,63 @@ async function lintProject(
133
208
  projectConfig,
134
209
  });
135
210
  hasErrors = true;
211
+ continue;
212
+ }
213
+
214
+ const semanticIssues = getSemanticIssues("effect", result.data as any, lintContext);
215
+
216
+ if (semanticIssues.length > 0) {
217
+ printError({
218
+ entityType: "effect",
219
+ entityKey: effectKey,
220
+ error: new z.ZodError(semanticIssues),
221
+ projectConfig,
222
+ });
223
+ hasErrors = true;
136
224
  }
137
225
  }
138
226
 
139
- // @TODO: tests
227
+ // tests
228
+ console.log("\nLinting tests...");
229
+
230
+ const tests = await datasource.listTests();
231
+ const testSchema = getTestSchema(options);
232
+
233
+ for (const testKey of tests) {
234
+ if (entityType && entityType !== "test") {
235
+ continue;
236
+ }
237
+
238
+ if (keyPattern && !testKey.includes(keyPattern)) {
239
+ continue;
240
+ }
241
+
242
+ const testContent = await datasource.readTest(testKey);
243
+ const result = await testSchema.safeParseAsync(testContent);
244
+
245
+ if (!result.success) {
246
+ printError({
247
+ entityType: "test",
248
+ entityKey: testKey,
249
+ error: result.error,
250
+ projectConfig,
251
+ });
252
+ hasErrors = true;
253
+ continue;
254
+ }
255
+
256
+ const semanticIssues = getSemanticIssues("test", result.data as any, lintContext);
257
+
258
+ if (semanticIssues.length > 0) {
259
+ printError({
260
+ entityType: "test",
261
+ entityKey: testKey,
262
+ error: new z.ZodError(semanticIssues),
263
+ projectConfig,
264
+ });
265
+ hasErrors = true;
266
+ }
267
+ }
140
268
 
141
269
  if (hasErrors) {
142
270
  return false;
@@ -8,10 +8,12 @@ export function getPersistSchema(deps: Dependencies) {
8
8
 
9
9
  const simplePersist = z.string();
10
10
 
11
- const complexPersist = z.object({
12
- storage: z.string(),
13
- conditions: conditionsSchema.optional(),
14
- });
11
+ const complexPersist = z
12
+ .object({
13
+ storage: z.string(),
14
+ conditions: conditionsSchema.optional(),
15
+ })
16
+ .strict();
15
17
 
16
18
  return z.union([
17
19
  simplePersist,
@@ -23,6 +23,9 @@ function getFilePath(options: PrintErrorOptions) {
23
23
  } else if (entityType === "attribute") {
24
24
  directoryPath = projectConfig.attributesDirectoryPath;
25
25
  } else if (entityType === "destination") {
26
+ directoryPath = projectConfig.destinationsDirectoryPath;
27
+ } else if (entityType === "effect") {
28
+ directoryPath = projectConfig.effectsDirectoryPath;
26
29
  } else if (entityType === "test") {
27
30
  directoryPath = projectConfig.testsDirectoryPath;
28
31
  } else {