@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.
- package/CHANGELOG.md +11 -0
- package/lib/linter/conditionsSchema.d.ts +1 -301
- package/lib/linter/conditionsSchema.js +8 -14
- package/lib/linter/conditionsSchema.js.map +1 -1
- package/lib/linter/destinationSchema.d.ts +89 -1
- package/lib/linter/destinationSchema.js +0 -2
- package/lib/linter/destinationSchema.js.map +1 -1
- package/lib/linter/effectSchema.d.ts +9 -1209
- package/lib/linter/effectSchema.js +6 -3
- package/lib/linter/effectSchema.js.map +1 -1
- package/lib/linter/entitySchemas.spec.d.ts +1 -0
- package/lib/linter/entitySchemas.spec.js +169 -0
- package/lib/linter/entitySchemas.spec.js.map +1 -0
- package/lib/linter/eventSchema.js +5 -3
- package/lib/linter/eventSchema.js.map +1 -1
- package/lib/linter/lintProject.d.ts +5 -0
- package/lib/linter/lintProject.js +141 -5
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/lintProject.spec.d.ts +1 -0
- package/lib/linter/lintProject.spec.js +103 -0
- package/lib/linter/lintProject.spec.js.map +1 -0
- package/lib/linter/persistSchema.d.ts +4 -604
- package/lib/linter/persistSchema.js +4 -2
- package/lib/linter/persistSchema.js.map +1 -1
- package/lib/linter/printError.js +4 -0
- package/lib/linter/printError.js.map +1 -1
- package/lib/linter/sampleSchema.d.ts +13 -285
- package/lib/linter/sampleSchema.js +2 -1
- package/lib/linter/sampleSchema.js.map +1 -1
- package/lib/linter/semanticValidation.d.ts +11 -0
- package/lib/linter/semanticValidation.js +592 -0
- package/lib/linter/semanticValidation.js.map +1 -0
- package/lib/linter/semanticValidation.spec.d.ts +1 -0
- package/lib/linter/semanticValidation.spec.js +190 -0
- package/lib/linter/semanticValidation.spec.js.map +1 -0
- package/lib/linter/testSchema.d.ts +64 -3
- package/lib/linter/testSchema.js +81 -4
- package/lib/linter/testSchema.js.map +1 -1
- package/lib/linter/testSchema.spec.d.ts +1 -0
- package/lib/linter/testSchema.spec.js +260 -0
- package/lib/linter/testSchema.spec.js.map +1 -0
- package/package.json +5 -5
- package/src/linter/conditionsSchema.ts +12 -18
- package/src/linter/destinationSchema.ts +0 -3
- package/src/linter/effectSchema.ts +12 -9
- package/src/linter/entitySchemas.spec.ts +212 -0
- package/src/linter/eventSchema.ts +8 -6
- package/src/linter/lintProject.spec.ts +112 -0
- package/src/linter/lintProject.ts +134 -6
- package/src/linter/persistSchema.ts +6 -4
- package/src/linter/printError.ts +3 -0
- package/src/linter/sampleSchema.ts +3 -1
- package/src/linter/semanticValidation.spec.ts +239 -0
- package/src/linter/semanticValidation.ts +953 -0
- package/src/linter/testSchema.spec.ts +279 -0
- 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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
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,
|
package/src/linter/printError.ts
CHANGED
|
@@ -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 {
|