@walkeros/cli 4.8.1-next-1791586788897 → 4.8.1
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 +19 -6
- package/dist/cli.js +4840 -6962
- package/dist/dev.d.ts +62 -1
- package/dist/dev.js +56 -1
- package/dist/dev.js.map +1 -1
- package/dist/examples/flow-complete.json +22 -22
- package/dist/examples/flow-complete.md +7 -7
- package/dist/examples/index.d.ts +0 -1
- package/dist/examples/index.js +30 -31
- package/dist/examples/index.js.map +1 -1
- package/dist/index.d.ts +122 -5747
- package/dist/index.js +1652 -3718
- package/dist/index.js.map +1 -1
- package/dist/validate.d.ts +93 -0
- package/dist/validate.js +2077 -0
- package/dist/validate.js.map +1 -0
- package/examples/flow-complete.json +22 -22
- package/examples/flow-complete.md +7 -7
- package/package.json +34 -38
- package/openapi/client-operations.json +0 -250
- package/openapi/spec.json +0 -7904
package/dist/validate.js
ADDED
|
@@ -0,0 +1,2077 @@
|
|
|
1
|
+
// src/commands/validate/index.ts
|
|
2
|
+
import * as fs9 from "fs";
|
|
3
|
+
import * as path3 from "path";
|
|
4
|
+
import chalk2 from "chalk";
|
|
5
|
+
import { isObject as isObject7 } from "@walkeros/core";
|
|
6
|
+
|
|
7
|
+
// src/core/cli-logger.ts
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import {
|
|
10
|
+
createCLILogger as createCoreCLILogger,
|
|
11
|
+
createCLILoggerConfig as createCoreCLILoggerConfig
|
|
12
|
+
} from "@walkeros/core/node";
|
|
13
|
+
var CLI_COLORS = { error: chalk.red };
|
|
14
|
+
|
|
15
|
+
// src/core/timer.ts
|
|
16
|
+
import { createTimer } from "@walkeros/core";
|
|
17
|
+
|
|
18
|
+
// src/core/output.ts
|
|
19
|
+
import fs from "fs-extra";
|
|
20
|
+
|
|
21
|
+
// src/core/tmp.ts
|
|
22
|
+
import {
|
|
23
|
+
getTmpPath,
|
|
24
|
+
createTmpResolver,
|
|
25
|
+
getDefaultTmpRoot
|
|
26
|
+
} from "@walkeros/core/node";
|
|
27
|
+
|
|
28
|
+
// src/config/utils.ts
|
|
29
|
+
import fs2 from "fs-extra";
|
|
30
|
+
import path from "path";
|
|
31
|
+
|
|
32
|
+
// src/core/fetch-failure.ts
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function fetchFailureReason(error, timeoutMs) {
|
|
37
|
+
if (!isRecord(error)) return String(error);
|
|
38
|
+
if (error.name === "TimeoutError")
|
|
39
|
+
return timeoutMs === void 0 ? "timeout" : `timeout after ${timeoutMs} ms`;
|
|
40
|
+
const message = typeof error.message === "string" ? error.message : String(error);
|
|
41
|
+
const cause = error.cause;
|
|
42
|
+
const code = isRecord(cause) && typeof cause.code === "string" ? cause.code : void 0;
|
|
43
|
+
return code ? `${message} (${code})` : message;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/config/utils.ts
|
|
47
|
+
function isUrl(str) {
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(str);
|
|
50
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
var ConfigFetchError = class extends Error {
|
|
56
|
+
url;
|
|
57
|
+
/** The HTTP status, when the host answered. */
|
|
58
|
+
status;
|
|
59
|
+
constructor(message, url, status) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = "ConfigFetchError";
|
|
62
|
+
this.url = url;
|
|
63
|
+
this.status = status;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
async function fetchContentString(url) {
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
response = await fetch(url);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new ConfigFetchError(
|
|
72
|
+
`Could not reach ${url}: ${fetchFailureReason(error)}`,
|
|
73
|
+
url
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new ConfigFetchError(
|
|
78
|
+
`Failed to fetch ${url}: ${response.status} ${response.statusText}`.trimEnd(),
|
|
79
|
+
url,
|
|
80
|
+
response.status
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return response.text();
|
|
84
|
+
}
|
|
85
|
+
async function resolveContent(input) {
|
|
86
|
+
const trimmed = input.trim();
|
|
87
|
+
if (isUrl(trimmed)) {
|
|
88
|
+
return fetchContentString(trimmed);
|
|
89
|
+
}
|
|
90
|
+
const absolutePath = path.resolve(trimmed);
|
|
91
|
+
if (await fs2.pathExists(absolutePath)) {
|
|
92
|
+
return fs2.readFile(absolutePath, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`Configuration file not found: ${absolutePath}`);
|
|
98
|
+
}
|
|
99
|
+
async function loadConfig(input, options) {
|
|
100
|
+
const json = options?.json !== false;
|
|
101
|
+
if (!json) {
|
|
102
|
+
return resolveContent(input);
|
|
103
|
+
}
|
|
104
|
+
const trimmed = input.trim();
|
|
105
|
+
try {
|
|
106
|
+
const content = await resolveContent(trimmed);
|
|
107
|
+
return JSON.parse(content);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
110
|
+
if (error instanceof ConfigFetchError || errorMessage.includes("not found"))
|
|
111
|
+
throw error;
|
|
112
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Input appears to be JSON but contains errors: ${errorMessage}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Invalid JSON in config file: ${input}. ${errorMessage}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function loadJsonConfig(configPath) {
|
|
121
|
+
return loadConfig(configPath, { json: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/core/utils.ts
|
|
125
|
+
import { getErrorMessage } from "@walkeros/core";
|
|
126
|
+
|
|
127
|
+
// src/core/local-packages.ts
|
|
128
|
+
import fs3 from "fs-extra";
|
|
129
|
+
|
|
130
|
+
// src/core/input-detector.ts
|
|
131
|
+
import fs4 from "fs-extra";
|
|
132
|
+
|
|
133
|
+
// src/core/stdin.ts
|
|
134
|
+
import fs5 from "fs-extra";
|
|
135
|
+
|
|
136
|
+
// src/core/oauth-client.ts
|
|
137
|
+
import { z as z2 } from "zod";
|
|
138
|
+
|
|
139
|
+
// src/core/app-fetch.ts
|
|
140
|
+
import { z } from "zod";
|
|
141
|
+
var AppErrorSchema = z.object({
|
|
142
|
+
error: z.object({ message: z.string().min(1) })
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// src/core/oauth-client.ts
|
|
146
|
+
var DeviceAuthorizationSchema = z2.object({
|
|
147
|
+
device_code: z2.string().min(1),
|
|
148
|
+
user_code: z2.string().min(1),
|
|
149
|
+
verification_uri: z2.string().min(1),
|
|
150
|
+
verification_uri_complete: z2.string().min(1).optional(),
|
|
151
|
+
expires_in: z2.number().int().nonnegative(),
|
|
152
|
+
interval: z2.number().int().nonnegative()
|
|
153
|
+
});
|
|
154
|
+
var TokenSchema = z2.object({
|
|
155
|
+
access_token: z2.string().min(1),
|
|
156
|
+
expires_in: z2.number().int().nonnegative(),
|
|
157
|
+
refresh_token: z2.string().min(1).optional()
|
|
158
|
+
});
|
|
159
|
+
var ErrorSchema = z2.object({
|
|
160
|
+
error: z2.string().min(1),
|
|
161
|
+
error_description: z2.string().optional()
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// src/core/event-validation.ts
|
|
165
|
+
import { schemas } from "@walkeros/core/dev";
|
|
166
|
+
var { PartialEventSchema } = schemas;
|
|
167
|
+
function validateEvent(input, level = "strict") {
|
|
168
|
+
const errors = [];
|
|
169
|
+
const warnings = [];
|
|
170
|
+
const details = {};
|
|
171
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
172
|
+
errors.push({
|
|
173
|
+
path: "root",
|
|
174
|
+
message: "Event must be an object",
|
|
175
|
+
code: "NOT_AN_OBJECT"
|
|
176
|
+
});
|
|
177
|
+
return { valid: false, errors, warnings, details };
|
|
178
|
+
}
|
|
179
|
+
const event = input;
|
|
180
|
+
if (!("name" in event) || event.name === void 0) {
|
|
181
|
+
errors.push({
|
|
182
|
+
path: "name",
|
|
183
|
+
message: "Event must have a name field",
|
|
184
|
+
code: "MISSING_EVENT_NAME"
|
|
185
|
+
});
|
|
186
|
+
} else if (typeof event.name !== "string" || event.name.trim() === "") {
|
|
187
|
+
errors.push({
|
|
188
|
+
path: "name",
|
|
189
|
+
message: "Event name cannot be empty",
|
|
190
|
+
value: event.name,
|
|
191
|
+
code: "EMPTY_EVENT_NAME"
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
const name = event.name;
|
|
195
|
+
if (!name.includes(" ")) {
|
|
196
|
+
if (level === "strict") {
|
|
197
|
+
errors.push({
|
|
198
|
+
path: "name",
|
|
199
|
+
message: 'Event name must be "entity action" format with space (e.g., "page view")',
|
|
200
|
+
value: name,
|
|
201
|
+
code: "INVALID_EVENT_NAME"
|
|
202
|
+
});
|
|
203
|
+
details.entity = null;
|
|
204
|
+
details.action = null;
|
|
205
|
+
} else if (level === "standard") {
|
|
206
|
+
warnings.push({
|
|
207
|
+
path: "name",
|
|
208
|
+
message: `Event name "${name}" should follow "ENTITY ACTION" format (e.g., "page view")`
|
|
209
|
+
});
|
|
210
|
+
details.entity = null;
|
|
211
|
+
details.action = null;
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const parts = name.trim().split(/\s+/);
|
|
215
|
+
const action = parts.pop();
|
|
216
|
+
const entity = parts.join(" ");
|
|
217
|
+
details.entity = entity;
|
|
218
|
+
details.action = action;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (level === "minimal") {
|
|
222
|
+
return { valid: errors.length === 0, errors, warnings, details };
|
|
223
|
+
}
|
|
224
|
+
const zodResult = PartialEventSchema.safeParse(input);
|
|
225
|
+
if (!zodResult.success) {
|
|
226
|
+
for (const issue of zodResult.error.issues) {
|
|
227
|
+
const issuePath = issue.path.join(".");
|
|
228
|
+
if (issuePath === "name") continue;
|
|
229
|
+
errors.push({
|
|
230
|
+
path: issuePath || "root",
|
|
231
|
+
message: issue.message,
|
|
232
|
+
code: "SCHEMA_VALIDATION"
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (level === "strict") {
|
|
237
|
+
if (!event.consent) {
|
|
238
|
+
warnings.push({
|
|
239
|
+
path: "consent",
|
|
240
|
+
message: "No consent object provided",
|
|
241
|
+
suggestion: "Consider adding a consent object for GDPR/privacy compliance"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
details.hasConsent = !!event.consent;
|
|
245
|
+
details.hasData = !!event.data;
|
|
246
|
+
details.hasContext = !!event.context;
|
|
247
|
+
}
|
|
248
|
+
return { valid: errors.length === 0, errors, warnings, details };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/config/validators.ts
|
|
252
|
+
import { isObject } from "@walkeros/core";
|
|
253
|
+
import { schemas as schemas2 } from "@walkeros/core/dev";
|
|
254
|
+
var { safeParseConfig } = schemas2;
|
|
255
|
+
|
|
256
|
+
// src/config/loader.ts
|
|
257
|
+
import fs7 from "fs-extra";
|
|
258
|
+
import { getFlowSettings, getPlatform } from "@walkeros/core";
|
|
259
|
+
|
|
260
|
+
// src/core/content-digest.ts
|
|
261
|
+
import { createHash } from "crypto";
|
|
262
|
+
import fs6 from "fs-extra";
|
|
263
|
+
|
|
264
|
+
// src/config/window-collector.ts
|
|
265
|
+
import { checkWindowCollector } from "@walkeros/core";
|
|
266
|
+
|
|
267
|
+
// src/commands/validate/validators/contract.ts
|
|
268
|
+
import { isObject as isObject2 } from "@walkeros/core";
|
|
269
|
+
var SECTION_KEYS = ["globals", "context", "custom", "user", "consent"];
|
|
270
|
+
var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
271
|
+
"extend",
|
|
272
|
+
"tagging",
|
|
273
|
+
"description",
|
|
274
|
+
"events",
|
|
275
|
+
"schema",
|
|
276
|
+
...SECTION_KEYS
|
|
277
|
+
]);
|
|
278
|
+
function validateContract(input) {
|
|
279
|
+
const errors = [];
|
|
280
|
+
const warnings = [];
|
|
281
|
+
const details = {};
|
|
282
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
283
|
+
errors.push({
|
|
284
|
+
path: "root",
|
|
285
|
+
message: "Contract must be an object of named contract entries",
|
|
286
|
+
code: "INVALID_CONTRACT"
|
|
287
|
+
});
|
|
288
|
+
return { valid: false, type: "contract", errors, warnings, details };
|
|
289
|
+
}
|
|
290
|
+
const contracts = input;
|
|
291
|
+
const contractNames = Object.keys(contracts);
|
|
292
|
+
details.contractCount = contractNames.length;
|
|
293
|
+
for (const name of contractNames) {
|
|
294
|
+
if (name.startsWith("$")) {
|
|
295
|
+
errors.push({
|
|
296
|
+
path: name,
|
|
297
|
+
message: `"${name}" is not a named contract entry. This looks like the flat shape; use named ContractRule entries, lift $tagging to the rule field "tagging", and nest entity-action schemas under "events".`,
|
|
298
|
+
code: "FLAT_CONTRACT_SHAPE"
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (const [name, entry] of Object.entries(contracts)) {
|
|
303
|
+
if (name.startsWith("$")) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (typeof entry !== "object" || entry === null) {
|
|
307
|
+
errors.push({
|
|
308
|
+
path: name,
|
|
309
|
+
message: `Contract "${name}" must be an object`,
|
|
310
|
+
code: "INVALID_CONTRACT_ENTRY"
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const obj = entry;
|
|
315
|
+
const keys = Object.keys(obj);
|
|
316
|
+
const unknownKeys = keys.filter((k) => !KNOWN_KEYS.has(k));
|
|
317
|
+
const hasKnownKey = keys.some((k) => KNOWN_KEYS.has(k));
|
|
318
|
+
const objectValuedUnknown = unknownKeys.filter((k) => isObject2(obj[k]));
|
|
319
|
+
if (!hasKnownKey && objectValuedUnknown.length > 0) {
|
|
320
|
+
errors.push({
|
|
321
|
+
path: name,
|
|
322
|
+
message: `Contract "${name}" looks like a flat entity-action map. Nest entity-action schemas under an "events" key of a named ContractRule.`,
|
|
323
|
+
code: "FLAT_CONTRACT_SHAPE"
|
|
324
|
+
});
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
for (const key of unknownKeys) {
|
|
328
|
+
warnings.push({
|
|
329
|
+
path: `${name}.${key}`,
|
|
330
|
+
message: `Unknown contract key "${key}" is ignored`,
|
|
331
|
+
code: "UNKNOWN_CONTRACT_KEY"
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (obj.tagging !== void 0 && typeof obj.tagging !== "number") {
|
|
335
|
+
errors.push({
|
|
336
|
+
path: `${name}.tagging`,
|
|
337
|
+
message: "tagging must be a number",
|
|
338
|
+
code: "INVALID_TAGGING"
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (obj.schema !== void 0 && !isObject2(obj.schema)) {
|
|
342
|
+
errors.push({
|
|
343
|
+
path: `${name}.schema`,
|
|
344
|
+
message: "schema must be a JSON Schema object",
|
|
345
|
+
code: "INVALID_SCHEMA"
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (obj.extend !== void 0) {
|
|
349
|
+
if (typeof obj.extend !== "string") {
|
|
350
|
+
errors.push({
|
|
351
|
+
path: `${name}.extend`,
|
|
352
|
+
message: "extend must be a string",
|
|
353
|
+
code: "INVALID_EXTENDS"
|
|
354
|
+
});
|
|
355
|
+
} else if (!contractNames.includes(obj.extend)) {
|
|
356
|
+
errors.push({
|
|
357
|
+
path: `${name}.extend`,
|
|
358
|
+
message: `extend references non-existent contract "${obj.extend}"`,
|
|
359
|
+
value: obj.extend,
|
|
360
|
+
code: "INVALID_EXTENDS"
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (const key of SECTION_KEYS) {
|
|
365
|
+
if (key in obj) {
|
|
366
|
+
if (typeof obj[key] !== "object" || obj[key] === null) {
|
|
367
|
+
errors.push({
|
|
368
|
+
path: `${name}.${key}`,
|
|
369
|
+
message: `Section "${key}" must be a JSON Schema object`,
|
|
370
|
+
value: obj[key],
|
|
371
|
+
code: "INVALID_SECTION"
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (obj.events !== void 0) {
|
|
377
|
+
if (typeof obj.events !== "object" || obj.events === null) {
|
|
378
|
+
errors.push({
|
|
379
|
+
path: `${name}.events`,
|
|
380
|
+
message: "events must be an object",
|
|
381
|
+
code: "INVALID_EVENTS"
|
|
382
|
+
});
|
|
383
|
+
} else {
|
|
384
|
+
validateEntityActions(
|
|
385
|
+
obj.events,
|
|
386
|
+
`${name}.events`,
|
|
387
|
+
errors
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const reportedCycles = /* @__PURE__ */ new Set();
|
|
393
|
+
for (const name of contractNames) {
|
|
394
|
+
const chain = [];
|
|
395
|
+
let current = name;
|
|
396
|
+
while (current) {
|
|
397
|
+
const start = chain.indexOf(current);
|
|
398
|
+
if (start !== -1) {
|
|
399
|
+
const cycle = chain.slice(start);
|
|
400
|
+
const key = [...cycle].sort().join("\0");
|
|
401
|
+
if (!reportedCycles.has(key)) {
|
|
402
|
+
reportedCycles.add(key);
|
|
403
|
+
errors.push({
|
|
404
|
+
path: `${cycle[0]}.extend`,
|
|
405
|
+
message: `Circular extend chain: ${[...cycle, current].join(" \u2192 ")}`,
|
|
406
|
+
code: "CIRCULAR_EXTENDS"
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
chain.push(current);
|
|
412
|
+
const entry = contracts[current];
|
|
413
|
+
current = isObject2(entry) && typeof entry.extend === "string" ? entry.extend : "";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
valid: errors.length === 0,
|
|
418
|
+
type: "contract",
|
|
419
|
+
errors,
|
|
420
|
+
warnings,
|
|
421
|
+
details
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function validateEntityActions(obj, prefix, errors) {
|
|
425
|
+
for (const [entityKey, entityValue] of Object.entries(obj)) {
|
|
426
|
+
if (entityKey.trim() === "") {
|
|
427
|
+
errors.push({
|
|
428
|
+
path: `${prefix}.${entityKey}`,
|
|
429
|
+
message: "Entity key cannot be empty",
|
|
430
|
+
code: "INVALID_ENTITY_KEY"
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (typeof entityValue !== "object" || entityValue === null) {
|
|
435
|
+
errors.push({
|
|
436
|
+
path: `${prefix}.${entityKey}`,
|
|
437
|
+
message: `Entity "${entityKey}" must be an object`,
|
|
438
|
+
value: entityValue,
|
|
439
|
+
code: "INVALID_ENTITY"
|
|
440
|
+
});
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
for (const [actionKey, actionValue] of Object.entries(
|
|
444
|
+
entityValue
|
|
445
|
+
)) {
|
|
446
|
+
if (actionKey.trim() === "") {
|
|
447
|
+
errors.push({
|
|
448
|
+
path: `${prefix}.${entityKey}.${actionKey}`,
|
|
449
|
+
message: "Action key cannot be empty",
|
|
450
|
+
code: "INVALID_ACTION_KEY"
|
|
451
|
+
});
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (typeof actionValue !== "object" || actionValue === null || Array.isArray(actionValue)) {
|
|
455
|
+
errors.push({
|
|
456
|
+
path: `${prefix}.${entityKey}.${actionKey}`,
|
|
457
|
+
message: "Contract entry must be a JSON Schema object",
|
|
458
|
+
value: typeof actionValue,
|
|
459
|
+
code: "INVALID_SCHEMA_ENTRY"
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/commands/validate/validators/event.ts
|
|
467
|
+
function validateEvent2(input) {
|
|
468
|
+
const result = validateEvent(input, "strict");
|
|
469
|
+
return {
|
|
470
|
+
valid: result.valid,
|
|
471
|
+
type: "event",
|
|
472
|
+
errors: result.errors,
|
|
473
|
+
warnings: result.warnings,
|
|
474
|
+
details: result.details
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/commands/validate/validators/flow.ts
|
|
479
|
+
import {
|
|
480
|
+
checkWindowCollector as checkWindowCollector2,
|
|
481
|
+
ENV_MARKER_PREFIX as ENV_MARKER_PREFIX2,
|
|
482
|
+
getByPath,
|
|
483
|
+
getRouteGraph,
|
|
484
|
+
isObject as isObject5,
|
|
485
|
+
REF_CONTRACT,
|
|
486
|
+
resolveContracts,
|
|
487
|
+
validateStepEntry
|
|
488
|
+
} from "@walkeros/core";
|
|
489
|
+
import { schemas as schemas3, validateFlowStructure } from "@walkeros/core/dev";
|
|
490
|
+
import {
|
|
491
|
+
validateEventAgainstContract
|
|
492
|
+
} from "@walkeros/transformer-validate";
|
|
493
|
+
|
|
494
|
+
// src/commands/validate/scope.ts
|
|
495
|
+
import { isObject as isObject3 } from "@walkeros/core";
|
|
496
|
+
function isFlowJson(value) {
|
|
497
|
+
return isObject3(value) && "version" in value && isObject3(value.flows);
|
|
498
|
+
}
|
|
499
|
+
var ENTRY_SECTIONS = [
|
|
500
|
+
"sources",
|
|
501
|
+
"destinations",
|
|
502
|
+
"transformers",
|
|
503
|
+
"stores"
|
|
504
|
+
];
|
|
505
|
+
function isEntrySection(value) {
|
|
506
|
+
return ENTRY_SECTIONS.some((section) => section === value);
|
|
507
|
+
}
|
|
508
|
+
function resolveScope(config, options = {}) {
|
|
509
|
+
const flowsValue = isObject3(config) ? config.flows : void 0;
|
|
510
|
+
const flowMap = isObject3(flowsValue) ? flowsValue : {};
|
|
511
|
+
const allFlows = Object.keys(flowMap);
|
|
512
|
+
let flows = allFlows;
|
|
513
|
+
if (options.flow !== void 0) {
|
|
514
|
+
if (!allFlows.includes(options.flow)) {
|
|
515
|
+
return {
|
|
516
|
+
allFlows,
|
|
517
|
+
flows: [],
|
|
518
|
+
entries: [],
|
|
519
|
+
searchedFlows: [],
|
|
520
|
+
errors: [
|
|
521
|
+
{
|
|
522
|
+
path: "flows",
|
|
523
|
+
message: `Flow "${options.flow}" not found. Available: ${allFlows.join(", ") || "none"}`,
|
|
524
|
+
code: "FLOW_NOT_FOUND"
|
|
525
|
+
}
|
|
526
|
+
]
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
flows = [options.flow];
|
|
530
|
+
}
|
|
531
|
+
const path4 = options.path;
|
|
532
|
+
if (path4 === void 0) {
|
|
533
|
+
return { allFlows, flows, entries: [], searchedFlows: [], errors: [] };
|
|
534
|
+
}
|
|
535
|
+
const parts = path4.split(".");
|
|
536
|
+
let sections = ENTRY_SECTIONS;
|
|
537
|
+
let section;
|
|
538
|
+
let key;
|
|
539
|
+
if (parts.length === 2 && parts[0] !== "" && parts[1] !== "") {
|
|
540
|
+
if (!isEntrySection(parts[0])) {
|
|
541
|
+
return {
|
|
542
|
+
allFlows,
|
|
543
|
+
flows,
|
|
544
|
+
entries: [],
|
|
545
|
+
searchedFlows: [],
|
|
546
|
+
errors: [
|
|
547
|
+
{
|
|
548
|
+
path: path4,
|
|
549
|
+
message: `Section "${parts[0]}" cannot be addressed by --path. Use one of: ${ENTRY_SECTIONS.join(", ")}`,
|
|
550
|
+
code: "UNSUPPORTED_SECTION"
|
|
551
|
+
}
|
|
552
|
+
]
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
section = parts[0];
|
|
556
|
+
sections = [section];
|
|
557
|
+
key = parts[1];
|
|
558
|
+
} else if (parts.length === 1 && parts[0] !== "") {
|
|
559
|
+
key = parts[0];
|
|
560
|
+
} else {
|
|
561
|
+
return {
|
|
562
|
+
allFlows,
|
|
563
|
+
flows,
|
|
564
|
+
entries: [],
|
|
565
|
+
searchedFlows: [],
|
|
566
|
+
errors: [
|
|
567
|
+
{
|
|
568
|
+
path: path4,
|
|
569
|
+
message: `Invalid path "${path4}". Use "section.key" or just "key"`,
|
|
570
|
+
code: "INVALID_PATH"
|
|
571
|
+
}
|
|
572
|
+
]
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const entries = [];
|
|
576
|
+
for (const flow of flows) {
|
|
577
|
+
const flowValue = flowMap[flow];
|
|
578
|
+
if (!isObject3(flowValue)) continue;
|
|
579
|
+
for (const candidate of sections) {
|
|
580
|
+
const sectionValue = flowValue[candidate];
|
|
581
|
+
if (!isObject3(sectionValue)) continue;
|
|
582
|
+
const entry = sectionValue[key];
|
|
583
|
+
if (isObject3(entry))
|
|
584
|
+
entries.push({ flow, section: candidate, key, entry });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const matched = [...new Set(entries.map((entry) => entry.section))];
|
|
588
|
+
if (matched.length > 1) {
|
|
589
|
+
return {
|
|
590
|
+
allFlows,
|
|
591
|
+
flows,
|
|
592
|
+
entries: [],
|
|
593
|
+
searchedFlows: flows,
|
|
594
|
+
target: { key },
|
|
595
|
+
errors: [
|
|
596
|
+
{
|
|
597
|
+
path: path4,
|
|
598
|
+
message: `Ambiguous key "${key}" found in multiple sections: ${matched.join(", ")}. Use dot-notation (e.g., ${matched[0]}.${key})`,
|
|
599
|
+
code: "AMBIGUOUS_ENTRY"
|
|
600
|
+
}
|
|
601
|
+
]
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const target = { section: section ?? matched[0], key };
|
|
605
|
+
if (entries.length === 0) {
|
|
606
|
+
return {
|
|
607
|
+
allFlows,
|
|
608
|
+
flows,
|
|
609
|
+
entries,
|
|
610
|
+
searchedFlows: flows,
|
|
611
|
+
target,
|
|
612
|
+
errors: [
|
|
613
|
+
{
|
|
614
|
+
path: path4,
|
|
615
|
+
message: `Entry "${key}" not found in ${section ?? "any section"} of flow(s): ${flows.join(", ") || "none"}`,
|
|
616
|
+
code: "ENTRY_NOT_FOUND"
|
|
617
|
+
}
|
|
618
|
+
]
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
allFlows,
|
|
623
|
+
flows,
|
|
624
|
+
entries,
|
|
625
|
+
searchedFlows: flows,
|
|
626
|
+
target,
|
|
627
|
+
errors: []
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function flowOfPath(path4, flowNames) {
|
|
631
|
+
return flowNames.find(
|
|
632
|
+
(name) => path4 === `flows.${name}` || path4.startsWith(`flows.${name}.`)
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
function describeScope(resolved, checks) {
|
|
636
|
+
const scope = { flows: resolved.flows, checks };
|
|
637
|
+
if (resolved.target) {
|
|
638
|
+
const { section, key } = resolved.target;
|
|
639
|
+
scope.entry = {
|
|
640
|
+
...section ? { section } : {},
|
|
641
|
+
key,
|
|
642
|
+
flows: [...new Set(resolved.entries.map((entry) => entry.flow))],
|
|
643
|
+
searchedFlows: resolved.searchedFlows
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return scope;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/commands/validate/validators/resolve.ts
|
|
650
|
+
import {
|
|
651
|
+
FlowCycleError,
|
|
652
|
+
FlowReferenceError,
|
|
653
|
+
getFlowSettings as getFlowSettings2,
|
|
654
|
+
getErrorMessage as getErrorMessage2
|
|
655
|
+
} from "@walkeros/core";
|
|
656
|
+
function resolveFlow(file, name) {
|
|
657
|
+
try {
|
|
658
|
+
const flow = getFlowSettings2(file, name, { deferred: true });
|
|
659
|
+
return { ok: true, flow };
|
|
660
|
+
} catch (error) {
|
|
661
|
+
return {
|
|
662
|
+
ok: false,
|
|
663
|
+
error: {
|
|
664
|
+
path: `flows.${name}`,
|
|
665
|
+
message: getErrorMessage2(error),
|
|
666
|
+
code: resolutionCode(error)
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function resolutionCode(error) {
|
|
672
|
+
if (error instanceof FlowReferenceError) return error.code;
|
|
673
|
+
if (error instanceof FlowCycleError) return error.code;
|
|
674
|
+
return "UNRESOLVED_REFERENCE";
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/commands/validate/validators/settings.ts
|
|
678
|
+
import * as fs8 from "fs";
|
|
679
|
+
import * as path2 from "path";
|
|
680
|
+
import Ajv from "ajv";
|
|
681
|
+
import {
|
|
682
|
+
ENV_MARKER_PREFIX,
|
|
683
|
+
fetchPackage,
|
|
684
|
+
getErrorMessage as getErrorMessage3,
|
|
685
|
+
isObject as isObject4,
|
|
686
|
+
REF_CODE_PREFIX,
|
|
687
|
+
REF_STORE,
|
|
688
|
+
SECRET_MARKER_PREFIX
|
|
689
|
+
} from "@walkeros/core";
|
|
690
|
+
|
|
691
|
+
// src/core/resolve-export-name.ts
|
|
692
|
+
function getStep(flow, kind, id) {
|
|
693
|
+
switch (kind) {
|
|
694
|
+
case "source":
|
|
695
|
+
return flow.sources?.[id];
|
|
696
|
+
case "transformer":
|
|
697
|
+
return flow.transformers?.[id];
|
|
698
|
+
case "destination":
|
|
699
|
+
return flow.destinations?.[id];
|
|
700
|
+
case "store":
|
|
701
|
+
return flow.stores?.[id];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function getBundlePackages(flow) {
|
|
705
|
+
return flow.config?.bundle?.packages;
|
|
706
|
+
}
|
|
707
|
+
function readFirstImport(packages, packageName) {
|
|
708
|
+
if (!packages || !packageName) return void 0;
|
|
709
|
+
const pkg = packages[packageName];
|
|
710
|
+
if (!pkg || !pkg.imports || pkg.imports.length === 0) return void 0;
|
|
711
|
+
const first = pkg.imports[0];
|
|
712
|
+
return typeof first === "string" && first.length > 0 ? first : void 0;
|
|
713
|
+
}
|
|
714
|
+
function resolveExportName(flow, kind, id) {
|
|
715
|
+
const step = getStep(flow, kind, id);
|
|
716
|
+
if (!step) return { exportName: void 0, source: "default" };
|
|
717
|
+
const packageName = step.package;
|
|
718
|
+
const importName = step.import;
|
|
719
|
+
if (typeof importName === "string" && importName.length > 0) {
|
|
720
|
+
return { exportName: importName, source: "import" };
|
|
721
|
+
}
|
|
722
|
+
const firstImport = readFirstImport(getBundlePackages(flow), packageName);
|
|
723
|
+
if (firstImport) {
|
|
724
|
+
return { exportName: firstImport, source: "imports" };
|
|
725
|
+
}
|
|
726
|
+
return { exportName: void 0, source: "default" };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/core/step-packages.ts
|
|
730
|
+
import { packageNameToVariable } from "@walkeros/core";
|
|
731
|
+
function parsePackageSpec(spec) {
|
|
732
|
+
const at = spec.lastIndexOf("@");
|
|
733
|
+
if (at <= 0) return { name: spec };
|
|
734
|
+
const version = spec.slice(at + 1);
|
|
735
|
+
return version ? { name: spec.slice(0, at), version } : { name: spec.slice(0, at) };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/commands/validate/validators/settings.ts
|
|
739
|
+
var CLIENT_HEADER = "walkeros-cli/4.8.1";
|
|
740
|
+
function createSettingsContext(configDir) {
|
|
741
|
+
return { configDir, cache: /* @__PURE__ */ new Map(), compiled: /* @__PURE__ */ new Map() };
|
|
742
|
+
}
|
|
743
|
+
function selectSettingsSchema(pkg, packageLabel, exportName) {
|
|
744
|
+
if (exportName === void 0) return { ok: true, settings: pkg.settings };
|
|
745
|
+
if (pkg.exportSchemas) {
|
|
746
|
+
const entry = Object.prototype.hasOwnProperty.call(
|
|
747
|
+
pkg.exportSchemas,
|
|
748
|
+
exportName
|
|
749
|
+
) ? pkg.exportSchemas[exportName] : void 0;
|
|
750
|
+
if (!isObject4(entry))
|
|
751
|
+
return {
|
|
752
|
+
ok: false,
|
|
753
|
+
reason: `${packageLabel} has no export "${exportName}"`
|
|
754
|
+
};
|
|
755
|
+
return { ok: true, settings: entry.settings };
|
|
756
|
+
}
|
|
757
|
+
if (pkg.exports && pkg.exports.length < 2)
|
|
758
|
+
return { ok: true, settings: pkg.settings };
|
|
759
|
+
return {
|
|
760
|
+
ok: false,
|
|
761
|
+
reason: `${packageLabel} predates per-export settings schemas; the step imports "${exportName}"`
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
var SECTION_KIND = {
|
|
765
|
+
sources: "source",
|
|
766
|
+
transformers: "transformer",
|
|
767
|
+
destinations: "destination",
|
|
768
|
+
stores: "store"
|
|
769
|
+
};
|
|
770
|
+
function pointerSegments(pointer) {
|
|
771
|
+
if (pointer === "") return [];
|
|
772
|
+
return pointer.split("/").slice(1).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
773
|
+
}
|
|
774
|
+
function valueAt(root, segments) {
|
|
775
|
+
let current = root;
|
|
776
|
+
for (const segment of segments) {
|
|
777
|
+
if (Array.isArray(current)) current = current[Number(segment)];
|
|
778
|
+
else if (isObject4(current)) current = current[segment];
|
|
779
|
+
else return void 0;
|
|
780
|
+
}
|
|
781
|
+
return current;
|
|
782
|
+
}
|
|
783
|
+
var MARKER = ENV_MARKER_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
784
|
+
var ENV_MARKER = new RegExp(
|
|
785
|
+
`${MARKER}([a-zA-Z_][a-zA-Z0-9_]*)(?::((?:(?!${MARKER})[^\\s"'])*))?`,
|
|
786
|
+
"g"
|
|
787
|
+
);
|
|
788
|
+
function prepareSettings(settings) {
|
|
789
|
+
const deferred = [];
|
|
790
|
+
const walk = (value, segments) => {
|
|
791
|
+
if (Array.isArray(value))
|
|
792
|
+
return value.map((item, index) => walk(item, [...segments, `${index}`]));
|
|
793
|
+
if (isObject4(value))
|
|
794
|
+
return Object.fromEntries(
|
|
795
|
+
Object.entries(value).map(([key, item]) => [
|
|
796
|
+
key,
|
|
797
|
+
walk(item, [...segments, key])
|
|
798
|
+
])
|
|
799
|
+
);
|
|
800
|
+
if (typeof value !== "string") return value;
|
|
801
|
+
if (value.startsWith(SECRET_MARKER_PREFIX)) {
|
|
802
|
+
deferred.push({
|
|
803
|
+
segments,
|
|
804
|
+
reference: `$secret.${value.slice(SECRET_MARKER_PREFIX.length)}`
|
|
805
|
+
});
|
|
806
|
+
return value;
|
|
807
|
+
}
|
|
808
|
+
if (value.includes(ENV_MARKER_PREFIX)) {
|
|
809
|
+
let runtimeOnly = false;
|
|
810
|
+
const withDefaults = value.replace(
|
|
811
|
+
ENV_MARKER,
|
|
812
|
+
(_match, _name, fallback) => {
|
|
813
|
+
if (fallback === void 0) runtimeOnly = true;
|
|
814
|
+
return fallback ?? "";
|
|
815
|
+
}
|
|
816
|
+
);
|
|
817
|
+
if (!runtimeOnly) return withDefaults;
|
|
818
|
+
deferred.push({
|
|
819
|
+
segments,
|
|
820
|
+
reference: value.replace(
|
|
821
|
+
ENV_MARKER,
|
|
822
|
+
(_match, name, fallback) => fallback === void 0 ? `$env.${name}` : `$env.${name}:${fallback}`
|
|
823
|
+
)
|
|
824
|
+
});
|
|
825
|
+
return value;
|
|
826
|
+
}
|
|
827
|
+
if (REF_STORE.test(value) || value.startsWith(REF_CODE_PREFIX)) {
|
|
828
|
+
deferred.push({ segments, reference: value });
|
|
829
|
+
}
|
|
830
|
+
return value;
|
|
831
|
+
};
|
|
832
|
+
return { value: walk(settings, []), deferred };
|
|
833
|
+
}
|
|
834
|
+
function isUnder(segments, prefix) {
|
|
835
|
+
return prefix.every((segment, index) => segments[index] === segment);
|
|
836
|
+
}
|
|
837
|
+
function bundlePin(rawFlow, name) {
|
|
838
|
+
const config = isObject4(rawFlow) ? rawFlow.config : void 0;
|
|
839
|
+
const bundle = isObject4(config) ? config.bundle : void 0;
|
|
840
|
+
const packages = isObject4(bundle) ? bundle.packages : void 0;
|
|
841
|
+
const pin = isObject4(packages) ? packages[name] : void 0;
|
|
842
|
+
if (!isObject4(pin)) return {};
|
|
843
|
+
return {
|
|
844
|
+
...typeof pin.version === "string" ? { version: pin.version } : {},
|
|
845
|
+
...typeof pin.path === "string" ? { path: pin.path } : {}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function packageSource(spec, rawFlow) {
|
|
849
|
+
if (spec.startsWith(".") || spec.startsWith("/"))
|
|
850
|
+
return { name: spec, localPath: spec };
|
|
851
|
+
const { name, version } = parsePackageSpec(spec);
|
|
852
|
+
const pin = bundlePin(rawFlow, name);
|
|
853
|
+
if (pin.path) return { name, localPath: pin.path };
|
|
854
|
+
const pinned = pin.version ?? version;
|
|
855
|
+
return pinned ? { name, version: pinned } : { name };
|
|
856
|
+
}
|
|
857
|
+
function readJson(file) {
|
|
858
|
+
return JSON.parse(fs8.readFileSync(file, "utf-8"));
|
|
859
|
+
}
|
|
860
|
+
function readLocalSchema(source, configDir) {
|
|
861
|
+
const localPath = source.localPath ?? "";
|
|
862
|
+
const root = path2.isAbsolute(localPath) ? localPath : path2.resolve(configDir, localPath);
|
|
863
|
+
const file = [
|
|
864
|
+
path2.join(root, "dist", "walkerOS.json"),
|
|
865
|
+
path2.join(root, "walkerOS.json")
|
|
866
|
+
].find((candidate) => fs8.existsSync(candidate));
|
|
867
|
+
if (!file) throw new Error(`no walkerOS.json under ${root}`);
|
|
868
|
+
const meta = readJson(file);
|
|
869
|
+
const pkgFile = path2.join(root, "package.json");
|
|
870
|
+
const pkg = fs8.existsSync(pkgFile) ? readJson(pkgFile) : void 0;
|
|
871
|
+
const schemas5 = isObject4(meta) ? meta.schemas : void 0;
|
|
872
|
+
const exportSchemas = isObject4(meta) ? meta.exportSchemas : void 0;
|
|
873
|
+
const declared = isObject4(meta) && isObject4(meta.$meta) ? meta.$meta.exports : void 0;
|
|
874
|
+
const version = isObject4(pkg) && typeof pkg.version === "string" ? pkg.version : void 0;
|
|
875
|
+
return {
|
|
876
|
+
...version ? { version } : {},
|
|
877
|
+
settings: isObject4(schemas5) ? schemas5.settings : void 0,
|
|
878
|
+
...isObject4(exportSchemas) ? { exportSchemas } : {},
|
|
879
|
+
...isObject4(declared) ? { exports: Object.keys(declared) } : {}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
async function loadSchema(source, context) {
|
|
883
|
+
if (source.localPath !== void 0) {
|
|
884
|
+
const configDir = context.configDir;
|
|
885
|
+
if (configDir === void 0)
|
|
886
|
+
throw new Error(
|
|
887
|
+
"it is a local path package and the input is not a file, so there is no directory to read it from"
|
|
888
|
+
);
|
|
889
|
+
return readLocalSchema(source, configDir);
|
|
890
|
+
}
|
|
891
|
+
const info = await fetchPackage(source.name, {
|
|
892
|
+
...source.version ? { version: source.version } : {},
|
|
893
|
+
client: CLIENT_HEADER
|
|
894
|
+
});
|
|
895
|
+
return {
|
|
896
|
+
version: info.version,
|
|
897
|
+
settings: info.schemas.settings,
|
|
898
|
+
...info.exportSchemas ? { exportSchemas: info.exportSchemas } : {},
|
|
899
|
+
...info.exports ? { exports: Object.keys(info.exports) } : {}
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function compileSettings(settings) {
|
|
903
|
+
if (!isObject4(settings)) return void 0;
|
|
904
|
+
const ajv = new Ajv({ allErrors: true, validateFormats: false });
|
|
905
|
+
try {
|
|
906
|
+
return { ok: true, validate: ajv.compile(settings) };
|
|
907
|
+
} catch (error) {
|
|
908
|
+
return { ok: false, message: getErrorMessage3(error) };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
function sourceKey(source) {
|
|
912
|
+
return source.localPath ? `path:${source.localPath}` : `${source.name}@${source.version ?? "latest"}`;
|
|
913
|
+
}
|
|
914
|
+
function cachedSchema(source, context) {
|
|
915
|
+
const key = sourceKey(source);
|
|
916
|
+
let pending = context.cache.get(key);
|
|
917
|
+
if (!pending) {
|
|
918
|
+
pending = loadSchema(source, context);
|
|
919
|
+
context.cache.set(key, pending);
|
|
920
|
+
}
|
|
921
|
+
return pending;
|
|
922
|
+
}
|
|
923
|
+
function cachedCompile(source, exportName, settings, context) {
|
|
924
|
+
const key = `${sourceKey(source)}#${exportName ?? ""}`;
|
|
925
|
+
if (context.compiled.has(key)) return context.compiled.get(key);
|
|
926
|
+
const compiled = compileSettings(settings);
|
|
927
|
+
context.compiled.set(key, compiled);
|
|
928
|
+
return compiled;
|
|
929
|
+
}
|
|
930
|
+
async function checkStepSettings(target, resolvedFlow, rawFlow, check, context) {
|
|
931
|
+
const at = `flows.${target.flow}.${target.section}.${target.key}`;
|
|
932
|
+
const outcome = { findings: [], skipped: [], deferred: [] };
|
|
933
|
+
const spec = typeof target.entry.package === "string" ? target.entry.package : "";
|
|
934
|
+
if (!spec) {
|
|
935
|
+
outcome.skipped.push({
|
|
936
|
+
path: at,
|
|
937
|
+
check,
|
|
938
|
+
reason: "No package field, so there is no settings schema to fetch",
|
|
939
|
+
code: "NO_PACKAGE"
|
|
940
|
+
});
|
|
941
|
+
return outcome;
|
|
942
|
+
}
|
|
943
|
+
const source = packageSource(spec, rawFlow);
|
|
944
|
+
let schema;
|
|
945
|
+
try {
|
|
946
|
+
schema = await cachedSchema(source, context);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
outcome.skipped.push({
|
|
949
|
+
path: at,
|
|
950
|
+
check,
|
|
951
|
+
reason: `Schema for ${source.name}${source.version ? `@${source.version}` : ""} could not be loaded: ${getErrorMessage3(error)}`,
|
|
952
|
+
code: "SCHEMA_UNAVAILABLE"
|
|
953
|
+
});
|
|
954
|
+
return outcome;
|
|
955
|
+
}
|
|
956
|
+
outcome.checked = {
|
|
957
|
+
path: at,
|
|
958
|
+
package: source.name,
|
|
959
|
+
...schema.version ? { version: schema.version } : {}
|
|
960
|
+
};
|
|
961
|
+
const { exportName } = resolveExportName(
|
|
962
|
+
resolvedFlow,
|
|
963
|
+
SECTION_KIND[target.section],
|
|
964
|
+
target.key
|
|
965
|
+
);
|
|
966
|
+
const version = schema.version ?? source.version;
|
|
967
|
+
const selection = selectSettingsSchema(
|
|
968
|
+
schema,
|
|
969
|
+
version ? `${source.name}@${version}` : source.name,
|
|
970
|
+
exportName
|
|
971
|
+
);
|
|
972
|
+
if (!selection.ok) {
|
|
973
|
+
outcome.skipped.push({
|
|
974
|
+
path: at,
|
|
975
|
+
check,
|
|
976
|
+
reason: selection.reason,
|
|
977
|
+
code: "NO_SETTINGS_SCHEMA"
|
|
978
|
+
});
|
|
979
|
+
return outcome;
|
|
980
|
+
}
|
|
981
|
+
const compiled = cachedCompile(
|
|
982
|
+
source,
|
|
983
|
+
exportName,
|
|
984
|
+
selection.settings,
|
|
985
|
+
context
|
|
986
|
+
);
|
|
987
|
+
if (!compiled) {
|
|
988
|
+
outcome.skipped.push({
|
|
989
|
+
path: at,
|
|
990
|
+
check,
|
|
991
|
+
reason: `Package ${source.name} has no settings schema`,
|
|
992
|
+
code: "NO_SETTINGS_SCHEMA"
|
|
993
|
+
});
|
|
994
|
+
return outcome;
|
|
995
|
+
}
|
|
996
|
+
if (!compiled.ok) {
|
|
997
|
+
outcome.skipped.push({
|
|
998
|
+
path: at,
|
|
999
|
+
check,
|
|
1000
|
+
reason: `The settings schema of ${source.name} cannot be compiled: ${compiled.message}`,
|
|
1001
|
+
code: "SCHEMA_UNAVAILABLE"
|
|
1002
|
+
});
|
|
1003
|
+
return outcome;
|
|
1004
|
+
}
|
|
1005
|
+
const validate2 = compiled.validate;
|
|
1006
|
+
const step = stepOf(resolvedFlow, target.section, target.key);
|
|
1007
|
+
const config = isObject4(step?.config) ? step.config : {};
|
|
1008
|
+
const prepared = prepareSettings(config.settings ?? {});
|
|
1009
|
+
const base = `${at}.config.settings`;
|
|
1010
|
+
for (const { segments, reference } of prepared.deferred) {
|
|
1011
|
+
outcome.deferred.push({
|
|
1012
|
+
path: [base, ...segments].join("."),
|
|
1013
|
+
reference
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
if (validate2(prepared.value)) return outcome;
|
|
1017
|
+
for (const e of validate2.errors || []) {
|
|
1018
|
+
const segments = pointerSegments(e.instancePath);
|
|
1019
|
+
if (prepared.deferred.some((d) => isUnder(segments, d.segments))) continue;
|
|
1020
|
+
const finding = {
|
|
1021
|
+
path: [base, ...segments].join("."),
|
|
1022
|
+
message: describeAjvError(e),
|
|
1023
|
+
code: "ENTRY_SCHEMA",
|
|
1024
|
+
keyword: e.keyword
|
|
1025
|
+
};
|
|
1026
|
+
if (segments.length > 0) finding.value = valueAt(prepared.value, segments);
|
|
1027
|
+
outcome.findings.push(finding);
|
|
1028
|
+
}
|
|
1029
|
+
return outcome;
|
|
1030
|
+
}
|
|
1031
|
+
function describeAjvError(error) {
|
|
1032
|
+
const message = error.message || "Unknown error";
|
|
1033
|
+
const extra = error.params.additionalProperty;
|
|
1034
|
+
return error.keyword === "additionalProperties" && typeof extra === "string" ? `${message}: "${extra}"` : message;
|
|
1035
|
+
}
|
|
1036
|
+
function stepOf(flow, section, key) {
|
|
1037
|
+
switch (section) {
|
|
1038
|
+
case "sources":
|
|
1039
|
+
return flow.sources?.[key];
|
|
1040
|
+
case "destinations":
|
|
1041
|
+
return flow.destinations?.[key];
|
|
1042
|
+
case "transformers":
|
|
1043
|
+
return flow.transformers?.[key];
|
|
1044
|
+
case "stores":
|
|
1045
|
+
return flow.stores?.[key];
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/commands/validate/validators/flow.ts
|
|
1050
|
+
var { validateFlowConfig: validateFlowConfig2 } = schemas3;
|
|
1051
|
+
var DEPRECATED_PACKAGES = {
|
|
1052
|
+
"@walkeros/store-memory": "has been removed. Use the built-in cache by omitting cache.store, or remove the store declaration if it was only used as a cache target."
|
|
1053
|
+
};
|
|
1054
|
+
var OPEN_STEP_KINDS = [
|
|
1055
|
+
["destinations", "Destination"],
|
|
1056
|
+
["stores", "Store"]
|
|
1057
|
+
];
|
|
1058
|
+
async function validateFlowWithPackages(input, options = {}) {
|
|
1059
|
+
const run = runFlowChecks(input, options);
|
|
1060
|
+
const { result, scope } = run;
|
|
1061
|
+
if (options.offline) {
|
|
1062
|
+
const described = describeScope(scope, run.checks);
|
|
1063
|
+
described.offline = true;
|
|
1064
|
+
result.details.scope = described;
|
|
1065
|
+
return result;
|
|
1066
|
+
}
|
|
1067
|
+
run.checks.push("flow:package-settings");
|
|
1068
|
+
const context = createSettingsContext(options.configDir);
|
|
1069
|
+
const deferred = [];
|
|
1070
|
+
const packages = [];
|
|
1071
|
+
const pending = [];
|
|
1072
|
+
for (const name of scope.flows) {
|
|
1073
|
+
const flow = run.resolved.get(name);
|
|
1074
|
+
const rawFlow = run.typed?.flows[name];
|
|
1075
|
+
const reason = run.gate(name);
|
|
1076
|
+
if (!flow || !rawFlow || reason) {
|
|
1077
|
+
run.skipped.push({
|
|
1078
|
+
path: `flows.${name}`,
|
|
1079
|
+
check: "flow:package-settings",
|
|
1080
|
+
reason: reason ?? `flows.${name} did not resolve`,
|
|
1081
|
+
code: "GATED_BY_ERRORS"
|
|
1082
|
+
});
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
for (const section of ENTRY_SECTIONS) {
|
|
1086
|
+
const steps = isObject5(rawFlow[section]) ? rawFlow[section] : {};
|
|
1087
|
+
for (const [key, entry] of Object.entries(steps)) {
|
|
1088
|
+
if (!isObject5(entry) || entry.package === void 0) continue;
|
|
1089
|
+
const target = { flow: name, section, key, entry };
|
|
1090
|
+
pending.push(
|
|
1091
|
+
checkStepSettings(
|
|
1092
|
+
target,
|
|
1093
|
+
flow,
|
|
1094
|
+
rawFlow,
|
|
1095
|
+
"flow:package-settings",
|
|
1096
|
+
context
|
|
1097
|
+
)
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
for (const outcome of await Promise.all(pending)) {
|
|
1103
|
+
result.warnings.push(...outcome.findings);
|
|
1104
|
+
run.skipped.push(...outcome.skipped);
|
|
1105
|
+
deferred.push(...outcome.deferred);
|
|
1106
|
+
if (outcome.checked) packages.push(outcome.checked);
|
|
1107
|
+
}
|
|
1108
|
+
result.details.scope = describeScope(scope, run.checks);
|
|
1109
|
+
result.details.deferred = [...result.details.deferred ?? [], ...deferred];
|
|
1110
|
+
if (packages.length > 0) result.details.packages = packages;
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1113
|
+
function runFlowChecks(input, options) {
|
|
1114
|
+
const errors = [];
|
|
1115
|
+
const warnings = [];
|
|
1116
|
+
const skipped = [];
|
|
1117
|
+
const checks = ["file:schema", "flow:schema"];
|
|
1118
|
+
const details = {};
|
|
1119
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1120
|
+
const result = {
|
|
1121
|
+
valid: false,
|
|
1122
|
+
type: "flow",
|
|
1123
|
+
errors,
|
|
1124
|
+
warnings,
|
|
1125
|
+
details
|
|
1126
|
+
};
|
|
1127
|
+
let json;
|
|
1128
|
+
const scope = resolveScope(input, { flow: options.flow });
|
|
1129
|
+
try {
|
|
1130
|
+
json = JSON.stringify(input, null, 2);
|
|
1131
|
+
} catch {
|
|
1132
|
+
errors.push({
|
|
1133
|
+
path: "root",
|
|
1134
|
+
message: "Input cannot be serialized to JSON",
|
|
1135
|
+
code: "SERIALIZATION_ERROR"
|
|
1136
|
+
});
|
|
1137
|
+
return {
|
|
1138
|
+
result,
|
|
1139
|
+
scope,
|
|
1140
|
+
resolved,
|
|
1141
|
+
checks,
|
|
1142
|
+
skipped,
|
|
1143
|
+
gate: () => "the input cannot be serialized"
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
errors.push(...scope.errors);
|
|
1147
|
+
const config = isObject5(input) ? input : {};
|
|
1148
|
+
const flowsValue = config.flows;
|
|
1149
|
+
const flows = isObject5(flowsValue) ? flowsValue : void 0;
|
|
1150
|
+
const allFlows = scope.allFlows;
|
|
1151
|
+
const inScope = (path4) => {
|
|
1152
|
+
const flow = flowOfPath(path4, allFlows);
|
|
1153
|
+
return flow === void 0 || scope.flows.includes(flow);
|
|
1154
|
+
};
|
|
1155
|
+
let contractErrors = 0;
|
|
1156
|
+
if ("contract" in config) {
|
|
1157
|
+
checks.push("file:contract");
|
|
1158
|
+
const contract = validateContract(config.contract);
|
|
1159
|
+
contractErrors = contract.errors.length;
|
|
1160
|
+
for (const error of contract.errors)
|
|
1161
|
+
errors.push({ ...error, path: `contract.${error.path}` });
|
|
1162
|
+
for (const warning of contract.warnings)
|
|
1163
|
+
warnings.push({ ...warning, path: `contract.${warning.path}` });
|
|
1164
|
+
}
|
|
1165
|
+
const coreResult = validateFlowConfig2(json);
|
|
1166
|
+
for (const issue of coreResult.errors) {
|
|
1167
|
+
const path4 = issue.path || "root";
|
|
1168
|
+
if (!inScope(path4)) continue;
|
|
1169
|
+
if (path4 === "contract" && contractErrors > 0) continue;
|
|
1170
|
+
errors.push({ path: path4, message: issue.message, code: "SCHEMA_VALIDATION" });
|
|
1171
|
+
}
|
|
1172
|
+
for (const issue of coreResult.warnings) {
|
|
1173
|
+
const path4 = issue.path || "root";
|
|
1174
|
+
if (!inScope(path4)) continue;
|
|
1175
|
+
warnings.push({ path: path4, message: issue.message, code: "CONFIG_WARNING" });
|
|
1176
|
+
}
|
|
1177
|
+
if (flows && allFlows.length === 0) {
|
|
1178
|
+
errors.push({
|
|
1179
|
+
path: "flows",
|
|
1180
|
+
message: "At least one flow is required",
|
|
1181
|
+
code: "EMPTY_FLOWS"
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
checks.push("flow:steps");
|
|
1185
|
+
for (const flowName of scope.flows) {
|
|
1186
|
+
const flowValue = flows?.[flowName];
|
|
1187
|
+
if (!isObject5(flowValue)) continue;
|
|
1188
|
+
const transformersValue = flowValue.transformers;
|
|
1189
|
+
if (isObject5(transformersValue)) {
|
|
1190
|
+
for (const [name, transformerValue] of Object.entries(
|
|
1191
|
+
transformersValue
|
|
1192
|
+
)) {
|
|
1193
|
+
if (!isObject5(transformerValue)) continue;
|
|
1194
|
+
const result2 = validateStepEntry(transformerValue, "Transformer");
|
|
1195
|
+
if (!result2.ok) {
|
|
1196
|
+
errors.push({
|
|
1197
|
+
path: `flows.${flowName}.transformers.${name}`,
|
|
1198
|
+
message: result2.reason || "Invalid transformer entry.",
|
|
1199
|
+
code: result2.code
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
for (const [section, kind] of OPEN_STEP_KINDS) {
|
|
1205
|
+
const steps = flowValue[section];
|
|
1206
|
+
if (!isObject5(steps)) continue;
|
|
1207
|
+
for (const [name, step] of Object.entries(steps)) {
|
|
1208
|
+
if (!isObject5(step)) continue;
|
|
1209
|
+
const at = `flows.${flowName}.${section}.${name}`;
|
|
1210
|
+
for (const key of unknownStepKeys(step, kind)) {
|
|
1211
|
+
warnings.push({
|
|
1212
|
+
path: `${at}.${key}`,
|
|
1213
|
+
message: `Unknown key "${key}" on ${kind} is ignored by the runtime`,
|
|
1214
|
+
suggestion: "Remove the key, or move it to where it belongs.",
|
|
1215
|
+
code: "UNKNOWN_KEY"
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
const pkg = step.package;
|
|
1219
|
+
if (typeof pkg === "string" && pkg in DEPRECATED_PACKAGES) {
|
|
1220
|
+
warnings.push({
|
|
1221
|
+
path: at,
|
|
1222
|
+
message: `${kind} "${name}" uses ${pkg}, which ${DEPRECATED_PACKAGES[pkg]}`,
|
|
1223
|
+
code: "DEPRECATED_PACKAGE"
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (flows) {
|
|
1230
|
+
details.flowNames = allFlows;
|
|
1231
|
+
details.flowCount = allFlows.length;
|
|
1232
|
+
}
|
|
1233
|
+
checks.push("flow:package-versions");
|
|
1234
|
+
let totalPackageCount = 0;
|
|
1235
|
+
for (const flowName of scope.flows) {
|
|
1236
|
+
const flowValue = flows?.[flowName];
|
|
1237
|
+
if (!isObject5(flowValue)) continue;
|
|
1238
|
+
const flowConfig = flowValue.config;
|
|
1239
|
+
if (!isObject5(flowConfig)) continue;
|
|
1240
|
+
const bundle = flowConfig.bundle;
|
|
1241
|
+
if (!isObject5(bundle)) continue;
|
|
1242
|
+
const packages = bundle.packages;
|
|
1243
|
+
if (!isObject5(packages)) continue;
|
|
1244
|
+
for (const [pkgName, pkgConfigValue] of Object.entries(packages)) {
|
|
1245
|
+
if (!isObject5(pkgConfigValue)) continue;
|
|
1246
|
+
if (!pkgConfigValue.version && !pkgConfigValue.path) {
|
|
1247
|
+
warnings.push({
|
|
1248
|
+
path: `flows.${flowName}.config.bundle.packages.${pkgName}`,
|
|
1249
|
+
message: `Package "${pkgName}" has no version specified`,
|
|
1250
|
+
suggestion: "Consider specifying a version for reproducible builds",
|
|
1251
|
+
code: "PACKAGE_VERSION_MISSING"
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
totalPackageCount += Object.keys(packages).length;
|
|
1256
|
+
}
|
|
1257
|
+
if (totalPackageCount > 0) {
|
|
1258
|
+
details.packageCount = totalPackageCount;
|
|
1259
|
+
}
|
|
1260
|
+
if (coreResult.context) {
|
|
1261
|
+
details.context = coreResult.context;
|
|
1262
|
+
}
|
|
1263
|
+
const typed = isFlowJson(input) ? input : void 0;
|
|
1264
|
+
const gate = (name) => {
|
|
1265
|
+
if (!typed || errors.some((e) => flowOfPath(e.path, allFlows) === void 0))
|
|
1266
|
+
return "file-level errors: the file shape or its contract is not valid";
|
|
1267
|
+
if (errors.some((e) => flowOfPath(e.path, allFlows) === name))
|
|
1268
|
+
return `earlier errors in flows.${name}`;
|
|
1269
|
+
return void 0;
|
|
1270
|
+
};
|
|
1271
|
+
const perFlow = (stageChecks, run) => {
|
|
1272
|
+
checks.push(...stageChecks);
|
|
1273
|
+
for (const name of scope.flows) {
|
|
1274
|
+
const reason = gate(name);
|
|
1275
|
+
const flow = typed?.flows[name];
|
|
1276
|
+
if (reason || !typed || !flow) {
|
|
1277
|
+
for (const check of stageChecks) {
|
|
1278
|
+
skipped.push({
|
|
1279
|
+
path: `flows.${name}`,
|
|
1280
|
+
check,
|
|
1281
|
+
reason: reason ?? `flows.${name} is not a flow object`,
|
|
1282
|
+
code: "GATED_BY_ERRORS"
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
run(name, flow, typed);
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
perFlow(["flow:structure"], (name, flow, file) => {
|
|
1291
|
+
const structure = validateFlowStructure({
|
|
1292
|
+
...file,
|
|
1293
|
+
flows: { [name]: flow }
|
|
1294
|
+
});
|
|
1295
|
+
errors.push(...structure.errors);
|
|
1296
|
+
warnings.push(...structure.warnings);
|
|
1297
|
+
});
|
|
1298
|
+
perFlow(["flow:routes"], (name, flow) => {
|
|
1299
|
+
lintFlowRoutes(name, flow, errors, warnings);
|
|
1300
|
+
});
|
|
1301
|
+
const resolutions = /* @__PURE__ */ new Map();
|
|
1302
|
+
const resolutionOf = (name, file) => {
|
|
1303
|
+
let resolution = resolutions.get(name);
|
|
1304
|
+
if (!resolution) {
|
|
1305
|
+
resolution = resolveFlow(file, name);
|
|
1306
|
+
resolutions.set(name, resolution);
|
|
1307
|
+
}
|
|
1308
|
+
return resolution;
|
|
1309
|
+
};
|
|
1310
|
+
let totalConnections;
|
|
1311
|
+
perFlow(
|
|
1312
|
+
["flow:examples", "flow:contract-examples", "flow:mapping-keys"],
|
|
1313
|
+
(name, flow, file) => {
|
|
1314
|
+
const connections = buildConnectionGraph(flow);
|
|
1315
|
+
for (const conn of connections) {
|
|
1316
|
+
checkCompatibility(name, conn, errors, warnings);
|
|
1317
|
+
}
|
|
1318
|
+
totalConnections = (totalConnections ?? 0) + connections.length;
|
|
1319
|
+
const resolution = resolutionOf(name, file);
|
|
1320
|
+
for (const [stepName, transformer] of Object.entries(
|
|
1321
|
+
flow.transformers || {}
|
|
1322
|
+
)) {
|
|
1323
|
+
if (!isValidateStep(transformer)) continue;
|
|
1324
|
+
const resolvedConfig = resolution.ok ? resolution.flow.transformers?.[stepName]?.config : void 0;
|
|
1325
|
+
checkValidateStepExamples(
|
|
1326
|
+
`flows.${name}.transformers.${stepName}`,
|
|
1327
|
+
transformer,
|
|
1328
|
+
file.contract,
|
|
1329
|
+
errors,
|
|
1330
|
+
warnings,
|
|
1331
|
+
options.strict === true,
|
|
1332
|
+
skipped,
|
|
1333
|
+
isObject5(resolvedConfig) && isObject5(resolvedConfig.settings) ? resolvedConfig.settings : void 0
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
for (const [destName, dest] of Object.entries(flow.destinations || {})) {
|
|
1337
|
+
if (!isObject5(dest.config)) continue;
|
|
1338
|
+
const mapping = dest.config.mapping;
|
|
1339
|
+
if (!isObject5(mapping)) continue;
|
|
1340
|
+
for (const key of Object.keys(mapping)) {
|
|
1341
|
+
if (key.includes(".") && !key.includes(" ")) {
|
|
1342
|
+
const parts = key.split(".");
|
|
1343
|
+
warnings.push({
|
|
1344
|
+
path: `flows.${name}.destinations.${destName}.config.mapping`,
|
|
1345
|
+
message: `Mapping key "${key}" looks like dot-notation. Mapping uses nested entity \u2192 action structure.`,
|
|
1346
|
+
suggestion: `Use nested format: { "${parts[0]}": { "${parts.slice(1).join(".")}": { ... } } }`,
|
|
1347
|
+
code: "MAPPING_DOT_KEY"
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
);
|
|
1354
|
+
if (totalConnections !== void 0) {
|
|
1355
|
+
details.connectionsChecked = totalConnections;
|
|
1356
|
+
}
|
|
1357
|
+
const deferred = [];
|
|
1358
|
+
perFlow(["flow:resolve"], (name, flow, file) => {
|
|
1359
|
+
const resolution = resolutionOf(name, file);
|
|
1360
|
+
if (!resolution.ok) {
|
|
1361
|
+
errors.push(resolution.error);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
resolved.set(name, resolution.flow);
|
|
1365
|
+
checkResolvedWindowCollector(name, flow, resolution.flow, errors, deferred);
|
|
1366
|
+
});
|
|
1367
|
+
details.scope = describeScope(scope, checks);
|
|
1368
|
+
details.skipped = skipped;
|
|
1369
|
+
details.deferred = deferred;
|
|
1370
|
+
result.valid = errors.length === 0;
|
|
1371
|
+
return {
|
|
1372
|
+
result,
|
|
1373
|
+
scope,
|
|
1374
|
+
typed,
|
|
1375
|
+
resolved,
|
|
1376
|
+
checks,
|
|
1377
|
+
skipped,
|
|
1378
|
+
gate
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
function checkResolvedWindowCollector(name, raw, flow, errors, deferred) {
|
|
1382
|
+
const written = raw.config?.settings?.windowCollector;
|
|
1383
|
+
const value = flow.config?.settings?.windowCollector;
|
|
1384
|
+
if (flow.config?.platform !== "web" || typeof written !== "string" || !/\$(?:var|env)\./.test(written))
|
|
1385
|
+
return;
|
|
1386
|
+
const path4 = `flows.${name}.config.settings.windowCollector`;
|
|
1387
|
+
if (typeof value === "string" && value.includes(ENV_MARKER_PREFIX2)) {
|
|
1388
|
+
deferred.push({ path: path4, reference: written });
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const check = checkWindowCollector2(value);
|
|
1392
|
+
if (check.ok) return;
|
|
1393
|
+
errors.push({
|
|
1394
|
+
path: path4,
|
|
1395
|
+
message: `${JSON.stringify(written)} resolved to ${JSON.stringify(value)}, which ${check.reason}`,
|
|
1396
|
+
code: "SCHEMA_VALIDATION"
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
function unknownStepKeys(step, kind) {
|
|
1400
|
+
const unknown = [];
|
|
1401
|
+
const rest = { ...step };
|
|
1402
|
+
for (; ; ) {
|
|
1403
|
+
const verdict = validateStepEntry(rest, kind);
|
|
1404
|
+
if (verdict.code !== "UNKNOWN_KEY" || verdict.key === void 0) break;
|
|
1405
|
+
unknown.push(verdict.key);
|
|
1406
|
+
Reflect.deleteProperty(rest, verdict.key);
|
|
1407
|
+
}
|
|
1408
|
+
return unknown;
|
|
1409
|
+
}
|
|
1410
|
+
function routeTargets(spec) {
|
|
1411
|
+
if (spec === void 0) return [];
|
|
1412
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1413
|
+
for (const node of getRouteGraph(spec)) {
|
|
1414
|
+
for (const target of node.targets) targets.add(target);
|
|
1415
|
+
}
|
|
1416
|
+
return [...targets];
|
|
1417
|
+
}
|
|
1418
|
+
function buildConnectionGraph(config) {
|
|
1419
|
+
const connections = [];
|
|
1420
|
+
for (const [name, source] of Object.entries(config.sources || {})) {
|
|
1421
|
+
if (!source.next || !source.examples) continue;
|
|
1422
|
+
const nextNames = routeTargets(source.next);
|
|
1423
|
+
for (const nextName of nextNames) {
|
|
1424
|
+
const transformer = config.transformers?.[nextName];
|
|
1425
|
+
if (transformer?.examples) {
|
|
1426
|
+
connections.push({
|
|
1427
|
+
from: { type: "source", name, examples: source.examples },
|
|
1428
|
+
to: {
|
|
1429
|
+
type: "transformer",
|
|
1430
|
+
name: nextName,
|
|
1431
|
+
examples: transformer.examples
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
for (const [name, transformer] of Object.entries(config.transformers || {})) {
|
|
1438
|
+
if (!transformer.next || !transformer.examples) continue;
|
|
1439
|
+
const nextNames = routeTargets(transformer.next);
|
|
1440
|
+
for (const nextName of nextNames) {
|
|
1441
|
+
const nextTransformer = config.transformers?.[nextName];
|
|
1442
|
+
if (nextTransformer?.examples) {
|
|
1443
|
+
connections.push({
|
|
1444
|
+
from: {
|
|
1445
|
+
type: "transformer",
|
|
1446
|
+
name,
|
|
1447
|
+
examples: transformer.examples
|
|
1448
|
+
},
|
|
1449
|
+
to: {
|
|
1450
|
+
type: "transformer",
|
|
1451
|
+
name: nextName,
|
|
1452
|
+
examples: nextTransformer.examples
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
for (const [name, dest] of Object.entries(config.destinations || {})) {
|
|
1459
|
+
if (!dest.before || !dest.examples) continue;
|
|
1460
|
+
const beforeNames = routeTargets(dest.before);
|
|
1461
|
+
for (const beforeName of beforeNames) {
|
|
1462
|
+
const transformer = config.transformers?.[beforeName];
|
|
1463
|
+
if (transformer?.examples) {
|
|
1464
|
+
connections.push({
|
|
1465
|
+
from: {
|
|
1466
|
+
type: "transformer",
|
|
1467
|
+
name: beforeName,
|
|
1468
|
+
examples: transformer.examples
|
|
1469
|
+
},
|
|
1470
|
+
to: { type: "destination", name, examples: dest.examples }
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return connections;
|
|
1476
|
+
}
|
|
1477
|
+
function checkCompatibility(flowName, conn, errors, warnings) {
|
|
1478
|
+
const fromOuts = Object.entries(conn.from.examples).filter(([, ex]) => hasComparableOut(ex.out)).map(([name, ex]) => ({ name, value: ex.out }));
|
|
1479
|
+
const toIns = Object.entries(conn.to.examples).filter(([, ex]) => ex.in !== void 0 && !ex.command).map(([name, ex]) => ({ name, value: ex.in }));
|
|
1480
|
+
const path4 = `flows.${flowName}.${conn.from.type}s.${conn.from.name} \u2192 ${conn.to.type}s.${conn.to.name}`;
|
|
1481
|
+
if (fromOuts.length === 0 || toIns.length === 0) {
|
|
1482
|
+
warnings.push({
|
|
1483
|
+
path: path4,
|
|
1484
|
+
message: "Cannot check compatibility: missing out or in examples",
|
|
1485
|
+
suggestion: "Add out examples to the source step or in examples to the target step",
|
|
1486
|
+
code: "EXAMPLES_INCOMPLETE"
|
|
1487
|
+
});
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const hasMatch = fromOuts.some(
|
|
1491
|
+
(out) => outEvents(out.value).some(
|
|
1492
|
+
(event) => toIns.some((inp) => isStructurallyCompatible(event, inp.value))
|
|
1493
|
+
)
|
|
1494
|
+
);
|
|
1495
|
+
if (!hasMatch) {
|
|
1496
|
+
errors.push({
|
|
1497
|
+
path: path4,
|
|
1498
|
+
message: "No compatible out/in pair found between connected steps",
|
|
1499
|
+
code: "INCOMPATIBLE_EXAMPLES"
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
function isStepOut(value) {
|
|
1504
|
+
return Array.isArray(value) && value.every((entry) => Array.isArray(entry) && typeof entry[0] === "string");
|
|
1505
|
+
}
|
|
1506
|
+
function returnedEvents(value) {
|
|
1507
|
+
if (isObject5(value)) return [isObject5(value.event) ? value.event : value];
|
|
1508
|
+
if (Array.isArray(value)) return value.flatMap(returnedEvents);
|
|
1509
|
+
return [];
|
|
1510
|
+
}
|
|
1511
|
+
function outEvents(out) {
|
|
1512
|
+
if (isStepOut(out)) {
|
|
1513
|
+
return out.flatMap(([head, first]) => {
|
|
1514
|
+
if (head === "elb") return isObject5(first) ? [first] : [];
|
|
1515
|
+
if (head === "return") return returnedEvents(first);
|
|
1516
|
+
return [];
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
if (Array.isArray(out) || typeof out === "string")
|
|
1520
|
+
return out.length > 0 ? [out] : [];
|
|
1521
|
+
return isObject5(out) && Object.keys(out).length > 0 ? [out] : [];
|
|
1522
|
+
}
|
|
1523
|
+
function hasComparableOut(out) {
|
|
1524
|
+
return outEvents(out).length > 0;
|
|
1525
|
+
}
|
|
1526
|
+
function isStructurallyCompatible(a, b) {
|
|
1527
|
+
if (typeof a !== typeof b) return false;
|
|
1528
|
+
if (a === null || b === null) return a === b;
|
|
1529
|
+
if (Array.isArray(a) && Array.isArray(b)) return true;
|
|
1530
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
1531
|
+
const keysA = Object.keys(a);
|
|
1532
|
+
const keysB = Object.keys(b);
|
|
1533
|
+
const shared = keysA.filter((k) => keysB.includes(k));
|
|
1534
|
+
return shared.length >= Math.min(keysA.length, keysB.length) * 0.5;
|
|
1535
|
+
}
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
function lintFlowRoutes(flowName, flow, errors, warnings) {
|
|
1539
|
+
const known = new Set(Object.keys(flow.transformers || {}));
|
|
1540
|
+
const check = (spec, position) => {
|
|
1541
|
+
if (spec !== void 0) lintRoute(spec, position, known, errors, warnings);
|
|
1542
|
+
};
|
|
1543
|
+
const at = `flows.${flowName}`;
|
|
1544
|
+
for (const [name, source] of Object.entries(flow.sources || {})) {
|
|
1545
|
+
check(source.before, `${at}.sources.${name}.before`);
|
|
1546
|
+
check(source.next, `${at}.sources.${name}.next`);
|
|
1547
|
+
}
|
|
1548
|
+
for (const [name, transformer] of Object.entries(flow.transformers || {})) {
|
|
1549
|
+
check(transformer.before, `${at}.transformers.${name}.before`);
|
|
1550
|
+
check(transformer.next, `${at}.transformers.${name}.next`);
|
|
1551
|
+
}
|
|
1552
|
+
check(flow.collector?.next, `${at}.collector.next`);
|
|
1553
|
+
for (const [name, dest] of Object.entries(flow.destinations || {})) {
|
|
1554
|
+
check(dest.before, `${at}.destinations.${name}.before`);
|
|
1555
|
+
check(dest.next, `${at}.destinations.${name}.next`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
function routeAt(spec, path4) {
|
|
1559
|
+
let current = spec;
|
|
1560
|
+
for (const key of path4) {
|
|
1561
|
+
if (typeof key === "number")
|
|
1562
|
+
current = Array.isArray(current) ? current[key] : void 0;
|
|
1563
|
+
else current = isObject5(current) ? current[key] : void 0;
|
|
1564
|
+
}
|
|
1565
|
+
return current;
|
|
1566
|
+
}
|
|
1567
|
+
function lintRoute(spec, position, known, errors, warnings) {
|
|
1568
|
+
const where = (path4) => path4.length > 0 ? `${position}.${path4.join(".")}` : position;
|
|
1569
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1570
|
+
const once = (key) => {
|
|
1571
|
+
if (reported.has(key)) return false;
|
|
1572
|
+
reported.add(key);
|
|
1573
|
+
return true;
|
|
1574
|
+
};
|
|
1575
|
+
for (const node of getRouteGraph(spec)) {
|
|
1576
|
+
for (const target of node.targets) {
|
|
1577
|
+
if (known.has(target) || !once(`unknown:${target}`)) continue;
|
|
1578
|
+
errors.push({
|
|
1579
|
+
path: position,
|
|
1580
|
+
message: `Unknown transformer "${target}" in route at ${position}`,
|
|
1581
|
+
code: "UNKNOWN_ROUTE_TARGET"
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
const last = node.path[node.path.length - 1];
|
|
1585
|
+
if (node.stop && !node.match && node.kind !== "many") {
|
|
1586
|
+
const self = routeAt(spec, node.path);
|
|
1587
|
+
const list = routeAt(spec, node.path.slice(0, -1));
|
|
1588
|
+
if (isObject5(self) && self.stop === true && typeof last === "number" && Array.isArray(list) && last < list.length - 1 && once(`stop:${where(node.path)}`)) {
|
|
1589
|
+
const at = where(node.path);
|
|
1590
|
+
warnings.push({
|
|
1591
|
+
path: at,
|
|
1592
|
+
message: `dead code after stop at ${at}: entries after an unconditional stop never run`,
|
|
1593
|
+
code: "ROUTE_DEAD_CODE",
|
|
1594
|
+
suggestion: "Remove the entries after the stop, or give the stop a match."
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (node.kind === "many" && last === "many" && node.targets.length === 0 && once(`empty:${where(node.path)}`)) {
|
|
1599
|
+
const at = where(node.path.slice(0, -1));
|
|
1600
|
+
warnings.push({
|
|
1601
|
+
path: at,
|
|
1602
|
+
message: `empty many at ${at}: selects no branch, so it has no effect`,
|
|
1603
|
+
code: "ROUTE_EMPTY_MANY",
|
|
1604
|
+
suggestion: "Add two or more branch targets to many, or remove it."
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
for (const decision of [...node.via ?? [], node]) {
|
|
1608
|
+
const index = decision.path[decision.path.length - 1];
|
|
1609
|
+
if (typeof index !== "number") continue;
|
|
1610
|
+
const listPath = decision.path.slice(0, -1);
|
|
1611
|
+
const list = routeAt(spec, listPath);
|
|
1612
|
+
if (!Array.isArray(list)) continue;
|
|
1613
|
+
const at = where(listPath);
|
|
1614
|
+
if (decision.kind === "many" && list.length === 1 && once(`single:${at}`)) {
|
|
1615
|
+
const only = list[0];
|
|
1616
|
+
const hint = typeof only === "string" ? `use 'next: "${only}"' for clarity` : `use 'next' for clarity`;
|
|
1617
|
+
warnings.push({
|
|
1618
|
+
path: at,
|
|
1619
|
+
message: `single-entry many at ${at}: ${hint}`,
|
|
1620
|
+
code: "ROUTE_SINGLE_MANY",
|
|
1621
|
+
suggestion: "Replace many with next when only one branch exists."
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
const explicitOne = listPath[listPath.length - 1] === "one";
|
|
1625
|
+
if (decision.kind === "one" && !explicitOne && list.length > 1 && once(`first-match:${at}`)) {
|
|
1626
|
+
warnings.push({
|
|
1627
|
+
path: at,
|
|
1628
|
+
message: `first-match array at ${at}: an array made only of route configs is an implicit one, the first matching entry wins`,
|
|
1629
|
+
code: "ROUTE_FIRST_MATCH",
|
|
1630
|
+
suggestion: 'Write { "one": [...] } explicitly, or add the step ids as a sequence to run every entry in order.'
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
var VALIDATE_PACKAGE = "@walkeros/transformer-validate";
|
|
1637
|
+
function isValidateStep(step) {
|
|
1638
|
+
return step.package === VALIDATE_PACKAGE;
|
|
1639
|
+
}
|
|
1640
|
+
function resolveStepContracts(entries, contract) {
|
|
1641
|
+
let resolved = {};
|
|
1642
|
+
if (contract) {
|
|
1643
|
+
try {
|
|
1644
|
+
resolved = resolveContracts(contract);
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
return {
|
|
1647
|
+
ok: false,
|
|
1648
|
+
message: `the root contract does not resolve (${error instanceof Error ? error.message : String(error)})`
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const sources = [];
|
|
1653
|
+
for (const [index, entry] of entries.entries()) {
|
|
1654
|
+
if (isObject5(entry)) {
|
|
1655
|
+
sources.push(entry);
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
const match = typeof entry === "string" ? entry.match(REF_CONTRACT) : null;
|
|
1659
|
+
if (!match)
|
|
1660
|
+
return {
|
|
1661
|
+
ok: false,
|
|
1662
|
+
index,
|
|
1663
|
+
message: "an entry must be a $contract reference or a JSON Schema"
|
|
1664
|
+
};
|
|
1665
|
+
if (!(match[1] in resolved))
|
|
1666
|
+
return {
|
|
1667
|
+
ok: false,
|
|
1668
|
+
index,
|
|
1669
|
+
message: `contract "${match[1]}" is not defined in the root contract (defined: ${Object.keys(resolved).join(", ") || "none"})`
|
|
1670
|
+
};
|
|
1671
|
+
const rule = match[2] ? getByPath(resolved[match[1]], match[2]) : resolved[match[1]];
|
|
1672
|
+
if (!isObject5(rule))
|
|
1673
|
+
return {
|
|
1674
|
+
ok: false,
|
|
1675
|
+
index,
|
|
1676
|
+
message: `${match[0]} does not point at a contract rule object`
|
|
1677
|
+
};
|
|
1678
|
+
sources.push(rule);
|
|
1679
|
+
}
|
|
1680
|
+
return { ok: true, sources };
|
|
1681
|
+
}
|
|
1682
|
+
function checkValidateStepExamples(stepPath, step, contract, errors, warnings, strict, skipped, resolvedSettings) {
|
|
1683
|
+
const settings = resolvedSettings ?? (isObject5(step.config) && isObject5(step.config.settings) ? step.config.settings : {});
|
|
1684
|
+
const contractPath = `${stepPath}.config.settings.contract`;
|
|
1685
|
+
if (settings.contract !== void 0 && !Array.isArray(settings.contract)) {
|
|
1686
|
+
if (step.examples)
|
|
1687
|
+
skipped?.push({
|
|
1688
|
+
path: stepPath,
|
|
1689
|
+
check: "flow:contract-examples",
|
|
1690
|
+
reason: "config.settings.contract is not a list, so the step cannot be judged statically",
|
|
1691
|
+
code: "CONTRACT_NOT_STATIC"
|
|
1692
|
+
});
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const resolution = Array.isArray(settings.contract) ? resolveStepContracts(settings.contract, contract) : { ok: true, sources: [] };
|
|
1696
|
+
if (!resolution.ok) {
|
|
1697
|
+
errors.push({
|
|
1698
|
+
path: resolution.index === void 0 ? contractPath : `${contractPath}.${resolution.index}`,
|
|
1699
|
+
message: `The validate step's contract does not resolve: ${resolution.message}`,
|
|
1700
|
+
code: "UNRESOLVED_CONTRACT"
|
|
1701
|
+
});
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (!step.examples) return;
|
|
1705
|
+
const contracts = resolution.sources;
|
|
1706
|
+
const mode = settings.mode === "strict" ? "strict" : "pass";
|
|
1707
|
+
const format = settings.format === true;
|
|
1708
|
+
const output = isObject5(settings.output) ? settings.output : {};
|
|
1709
|
+
const isValidPath = typeof output.isValid === "string" ? output.isValid : "source.valid";
|
|
1710
|
+
for (const [exName, example] of Object.entries(step.examples)) {
|
|
1711
|
+
if (example.command || !isObject5(example.in)) continue;
|
|
1712
|
+
if (example.out === void 0) continue;
|
|
1713
|
+
const event = example.in;
|
|
1714
|
+
const verdict = validateEventAgainstContract(event, void 0, {
|
|
1715
|
+
contracts,
|
|
1716
|
+
format
|
|
1717
|
+
});
|
|
1718
|
+
const passed = outEvents(example.out);
|
|
1719
|
+
const dropped = isStepOut(example.out) && example.out.some(([head, first]) => head === "return" && first === false);
|
|
1720
|
+
let agrees;
|
|
1721
|
+
if (mode === "strict" && !verdict.isValid) {
|
|
1722
|
+
agrees = dropped && passed.length === 0;
|
|
1723
|
+
} else {
|
|
1724
|
+
agrees = passed.length > 0 && passed.every((out) => {
|
|
1725
|
+
if (!isValidPath) return true;
|
|
1726
|
+
const flag = getByPath(out, isValidPath);
|
|
1727
|
+
return verdict.isValid ? flag !== false : flag === false;
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
if (agrees) continue;
|
|
1731
|
+
const reason = verdict.isValid ? "the event satisfies the contract" : `the event breaks the contract (${verdict.errors.map((e) => `${e.path || "/"}: ${e.message}`).join("; ")})`;
|
|
1732
|
+
const expected = mode === "strict" && !verdict.isValid ? 'drops it (["return", false])' : `passes it on${isValidPath ? ` with ${isValidPath} ${verdict.isValid}` : ""}`;
|
|
1733
|
+
const path4 = `${stepPath}.examples.${exName}.out`;
|
|
1734
|
+
const message = `Example out disagrees with the validate step: ${reason}, so with mode "${mode}" the step ${expected}.`;
|
|
1735
|
+
if (strict) {
|
|
1736
|
+
errors.push({ path: path4, message, code: "CONTRACT_VIOLATION" });
|
|
1737
|
+
} else {
|
|
1738
|
+
warnings.push({
|
|
1739
|
+
path: path4,
|
|
1740
|
+
message,
|
|
1741
|
+
suggestion: "Make the example out show what the validate step does with its settings.",
|
|
1742
|
+
code: "CONTRACT_VIOLATION"
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/commands/validate/validators/mapping.ts
|
|
1749
|
+
import { isObject as isObject6 } from "@walkeros/core";
|
|
1750
|
+
import { schemas as schemas4 } from "@walkeros/core/dev";
|
|
1751
|
+
var { RuleSchema } = schemas4.MappingSchemas;
|
|
1752
|
+
function isRule(value) {
|
|
1753
|
+
return isObject6(value) && RuleSchema.safeParse(value).success;
|
|
1754
|
+
}
|
|
1755
|
+
function isNestedActions(key, value) {
|
|
1756
|
+
if (key.includes(" ") || !isObject6(value)) return false;
|
|
1757
|
+
const rules = Object.values(value);
|
|
1758
|
+
return rules.length > 0 && rules.every(
|
|
1759
|
+
(rule) => Array.isArray(rule) ? rule.length > 0 && rule.every(isRule) : isRule(rule)
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
function validateMapping(input) {
|
|
1763
|
+
const errors = [];
|
|
1764
|
+
const warnings = [];
|
|
1765
|
+
const details = {};
|
|
1766
|
+
if (!isObject6(input)) {
|
|
1767
|
+
errors.push({
|
|
1768
|
+
path: "root",
|
|
1769
|
+
message: "Mapping must be an object with event patterns as keys",
|
|
1770
|
+
code: "INVALID_MAPPING_TYPE"
|
|
1771
|
+
});
|
|
1772
|
+
return { valid: false, type: "mapping", errors, warnings, details };
|
|
1773
|
+
}
|
|
1774
|
+
const mapping = input;
|
|
1775
|
+
const nestedPatterns = [];
|
|
1776
|
+
const flatPatterns = [];
|
|
1777
|
+
for (const [key, value] of Object.entries(mapping)) {
|
|
1778
|
+
if (isNestedActions(key, value)) {
|
|
1779
|
+
for (const action of Object.keys(value))
|
|
1780
|
+
nestedPatterns.push(`${key} ${action}`);
|
|
1781
|
+
} else {
|
|
1782
|
+
flatPatterns.push(key);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
const patterns = [...nestedPatterns, ...flatPatterns];
|
|
1786
|
+
details.eventPatterns = patterns;
|
|
1787
|
+
details.patternCount = patterns.length;
|
|
1788
|
+
flatPatterns.forEach((pattern, index) => {
|
|
1789
|
+
const isWildcard = pattern.includes("*");
|
|
1790
|
+
const hasSpace = pattern.includes(" ");
|
|
1791
|
+
if (!isWildcard && !hasSpace) {
|
|
1792
|
+
errors.push({
|
|
1793
|
+
path: pattern,
|
|
1794
|
+
message: `Invalid event pattern "${pattern}". Must be "entity action" format or contain wildcard (*)`,
|
|
1795
|
+
code: "INVALID_EVENT_PATTERN"
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
if (pattern === "*" && index !== flatPatterns.length - 1) {
|
|
1799
|
+
warnings.push({
|
|
1800
|
+
path: "*",
|
|
1801
|
+
code: "CATCH_ALL_NOT_LAST",
|
|
1802
|
+
message: "Catch-all pattern (*) should be last",
|
|
1803
|
+
suggestion: "Move the catch-all pattern (*) to last position for predictable matching"
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
const rule = mapping[pattern];
|
|
1807
|
+
const isValidRule = Array.isArray(rule) ? rule.every((r) => typeof r === "object" && r !== null) : typeof rule === "object" && rule !== null;
|
|
1808
|
+
if (!isValidRule) {
|
|
1809
|
+
errors.push({
|
|
1810
|
+
path: pattern,
|
|
1811
|
+
message: "Mapping rule must be an object or array of objects",
|
|
1812
|
+
code: "INVALID_RULE_TYPE"
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
return {
|
|
1817
|
+
valid: errors.length === 0,
|
|
1818
|
+
type: "mapping",
|
|
1819
|
+
errors,
|
|
1820
|
+
warnings,
|
|
1821
|
+
details
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/commands/validate/validators/entry.ts
|
|
1826
|
+
async function validateEntry(path4, flowConfig, options = {}) {
|
|
1827
|
+
const resolved = resolveScope(flowConfig, { flow: options.flow, path: path4 });
|
|
1828
|
+
const errors = [...resolved.errors];
|
|
1829
|
+
const skipped = [];
|
|
1830
|
+
const deferred = [];
|
|
1831
|
+
const packages = [];
|
|
1832
|
+
const context = createSettingsContext(options.configDir);
|
|
1833
|
+
const file = isFlowJson(flowConfig) ? flowConfig : void 0;
|
|
1834
|
+
const resolutions = /* @__PURE__ */ new Map();
|
|
1835
|
+
for (const target of resolved.entries) {
|
|
1836
|
+
const at = `flows.${target.flow}.${target.section}.${target.key}`;
|
|
1837
|
+
let resolution = resolutions.get(target.flow);
|
|
1838
|
+
if (!resolution && file) {
|
|
1839
|
+
resolution = resolveFlow(file, target.flow);
|
|
1840
|
+
resolutions.set(target.flow, resolution);
|
|
1841
|
+
if (!resolution.ok) errors.push(resolution.error);
|
|
1842
|
+
}
|
|
1843
|
+
if (!resolution?.ok) {
|
|
1844
|
+
skipped.push({
|
|
1845
|
+
path: at,
|
|
1846
|
+
check: "entry:settings",
|
|
1847
|
+
reason: `flows.${target.flow} does not resolve, so its settings cannot be checked`,
|
|
1848
|
+
code: "GATED_BY_ERRORS"
|
|
1849
|
+
});
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
const outcome = await checkStepSettings(
|
|
1853
|
+
target,
|
|
1854
|
+
resolution.flow,
|
|
1855
|
+
file?.flows[target.flow],
|
|
1856
|
+
"entry:settings",
|
|
1857
|
+
context
|
|
1858
|
+
);
|
|
1859
|
+
errors.push(...outcome.findings);
|
|
1860
|
+
skipped.push(...outcome.skipped);
|
|
1861
|
+
deferred.push(...outcome.deferred);
|
|
1862
|
+
if (outcome.checked) packages.push(outcome.checked);
|
|
1863
|
+
}
|
|
1864
|
+
return {
|
|
1865
|
+
valid: errors.length === 0,
|
|
1866
|
+
type: "entry",
|
|
1867
|
+
errors,
|
|
1868
|
+
warnings: [],
|
|
1869
|
+
details: {
|
|
1870
|
+
scope: describeScope(resolved, ["entry:settings"]),
|
|
1871
|
+
skipped,
|
|
1872
|
+
deferred,
|
|
1873
|
+
...packages.length > 0 ? { packages } : {}
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/commands/validate/types.ts
|
|
1879
|
+
var VALIDATE_CODES = [
|
|
1880
|
+
// Input and options
|
|
1881
|
+
"INPUT_ERROR",
|
|
1882
|
+
"SERIALIZATION_ERROR",
|
|
1883
|
+
"OPTION_NOT_APPLICABLE",
|
|
1884
|
+
// Scope (--flow, --path)
|
|
1885
|
+
"FLOW_NOT_FOUND",
|
|
1886
|
+
"ENTRY_NOT_FOUND",
|
|
1887
|
+
"AMBIGUOUS_ENTRY",
|
|
1888
|
+
"UNSUPPORTED_SECTION",
|
|
1889
|
+
"INVALID_PATH",
|
|
1890
|
+
// Flow file
|
|
1891
|
+
"SCHEMA_VALIDATION",
|
|
1892
|
+
"CONFIG_WARNING",
|
|
1893
|
+
"EMPTY_FLOWS",
|
|
1894
|
+
"UNKNOWN_KEY",
|
|
1895
|
+
"CONFLICT",
|
|
1896
|
+
"MISSING_PACKAGE",
|
|
1897
|
+
"OBSOLETE_CODE_STRING",
|
|
1898
|
+
"INVALID_IMPORT",
|
|
1899
|
+
"INVALID_CODE_SHAPE",
|
|
1900
|
+
"PACKAGE_VERSION_MISSING",
|
|
1901
|
+
"DEPRECATED_PACKAGE",
|
|
1902
|
+
"UNKNOWN_ROUTE_TARGET",
|
|
1903
|
+
"ROUTE_DEAD_CODE",
|
|
1904
|
+
"ROUTE_EMPTY_MANY",
|
|
1905
|
+
"ROUTE_SINGLE_MANY",
|
|
1906
|
+
"ROUTE_FIRST_MATCH",
|
|
1907
|
+
"INCOMPATIBLE_EXAMPLES",
|
|
1908
|
+
"EXAMPLES_INCOMPLETE",
|
|
1909
|
+
"CONTRACT_VIOLATION",
|
|
1910
|
+
"MAPPING_DOT_KEY",
|
|
1911
|
+
"FLOW_REF_WARNING",
|
|
1912
|
+
"FLOW_CYCLE",
|
|
1913
|
+
"VAR_CYCLE",
|
|
1914
|
+
// Structure (the bundler's own preflight, validateFlowStructure)
|
|
1915
|
+
"INVALID_COMPONENT_NAME",
|
|
1916
|
+
"INVALID_REFERENCE",
|
|
1917
|
+
"STORE_REFERENCE_NOT_FOUND",
|
|
1918
|
+
"STATE_STORE_UNKNOWN",
|
|
1919
|
+
"STATE_STORE_FILE",
|
|
1920
|
+
"TRANSFORMER_MAPPING_NO_OP",
|
|
1921
|
+
// Resolution (getFlowSettings, as the bundler resolves the flow)
|
|
1922
|
+
"UNRESOLVED_REFERENCE",
|
|
1923
|
+
"MISSING_ENV",
|
|
1924
|
+
"WEB_SECRET_REF",
|
|
1925
|
+
"UNRESOLVED_CONTRACT",
|
|
1926
|
+
// Entry settings against the package schema
|
|
1927
|
+
"ENTRY_SCHEMA",
|
|
1928
|
+
// Contract
|
|
1929
|
+
"NO_CONTRACT_SECTION",
|
|
1930
|
+
"INVALID_CONTRACT",
|
|
1931
|
+
"FLAT_CONTRACT_SHAPE",
|
|
1932
|
+
"INVALID_CONTRACT_ENTRY",
|
|
1933
|
+
"UNKNOWN_CONTRACT_KEY",
|
|
1934
|
+
"INVALID_TAGGING",
|
|
1935
|
+
"INVALID_SCHEMA",
|
|
1936
|
+
"INVALID_EXTENDS",
|
|
1937
|
+
"INVALID_SECTION",
|
|
1938
|
+
"INVALID_EVENTS",
|
|
1939
|
+
"CIRCULAR_EXTENDS",
|
|
1940
|
+
"INVALID_ENTITY_KEY",
|
|
1941
|
+
"INVALID_ENTITY",
|
|
1942
|
+
"INVALID_ACTION_KEY",
|
|
1943
|
+
"INVALID_SCHEMA_ENTRY",
|
|
1944
|
+
// Event
|
|
1945
|
+
"NOT_AN_OBJECT",
|
|
1946
|
+
"MISSING_EVENT_NAME",
|
|
1947
|
+
"EMPTY_EVENT_NAME",
|
|
1948
|
+
"INVALID_EVENT_NAME",
|
|
1949
|
+
// Mapping
|
|
1950
|
+
"INVALID_MAPPING_TYPE",
|
|
1951
|
+
"INVALID_EVENT_PATTERN",
|
|
1952
|
+
"INVALID_RULE_TYPE",
|
|
1953
|
+
"CATCH_ALL_NOT_LAST",
|
|
1954
|
+
// Skips (details.skipped[].code)
|
|
1955
|
+
"GATED_BY_ERRORS",
|
|
1956
|
+
"CONTRACT_NOT_STATIC",
|
|
1957
|
+
"NO_PACKAGE",
|
|
1958
|
+
"NO_SETTINGS_SCHEMA",
|
|
1959
|
+
"SCHEMA_UNAVAILABLE"
|
|
1960
|
+
];
|
|
1961
|
+
|
|
1962
|
+
// src/commands/validate/index.ts
|
|
1963
|
+
function inapplicableOptions(type, options) {
|
|
1964
|
+
const errors = [];
|
|
1965
|
+
if (options.offline === true && (type !== "flow" || options.path)) {
|
|
1966
|
+
errors.push({
|
|
1967
|
+
path: "options.offline",
|
|
1968
|
+
message: type === "flow" ? "--offline removes the package settings check, which is all --path checks; drop one of them" : `--offline applies to a whole flow file and does not apply to -t ${type}`,
|
|
1969
|
+
code: "OPTION_NOT_APPLICABLE"
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
if (type === "flow") return errors;
|
|
1973
|
+
if (options.path !== void 0) {
|
|
1974
|
+
errors.push({
|
|
1975
|
+
path: "options.path",
|
|
1976
|
+
message: `--path addresses an entry in a flow file and does not apply to -t ${type}`,
|
|
1977
|
+
code: "OPTION_NOT_APPLICABLE"
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
if (options.flow !== void 0) {
|
|
1981
|
+
errors.push({
|
|
1982
|
+
path: "options.flow",
|
|
1983
|
+
message: `--flow names a flow in a flow file and does not apply to -t ${type}`,
|
|
1984
|
+
code: "OPTION_NOT_APPLICABLE"
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
return errors;
|
|
1988
|
+
}
|
|
1989
|
+
function validateContractInput(input) {
|
|
1990
|
+
if (!isFlowJson(input)) return validateContract(input);
|
|
1991
|
+
const contract = isObject7(input) ? input.contract : void 0;
|
|
1992
|
+
if (contract === void 0) {
|
|
1993
|
+
return {
|
|
1994
|
+
valid: false,
|
|
1995
|
+
type: "contract",
|
|
1996
|
+
errors: [
|
|
1997
|
+
{
|
|
1998
|
+
path: "contract",
|
|
1999
|
+
message: "The flow file has no contract section to validate",
|
|
2000
|
+
code: "NO_CONTRACT_SECTION"
|
|
2001
|
+
}
|
|
2002
|
+
],
|
|
2003
|
+
warnings: [],
|
|
2004
|
+
details: {}
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
return validateContract(contract);
|
|
2008
|
+
}
|
|
2009
|
+
function configDirOf(input) {
|
|
2010
|
+
if (typeof input !== "string") return void 0;
|
|
2011
|
+
const candidate = path3.resolve(input);
|
|
2012
|
+
return fs9.existsSync(candidate) && fs9.statSync(candidate).isFile() ? path3.dirname(candidate) : void 0;
|
|
2013
|
+
}
|
|
2014
|
+
function withDocumentScope(result, check) {
|
|
2015
|
+
return {
|
|
2016
|
+
...result,
|
|
2017
|
+
details: {
|
|
2018
|
+
...result.details,
|
|
2019
|
+
scope: result.details.scope ?? { flows: [], checks: [check] },
|
|
2020
|
+
skipped: result.details.skipped ?? []
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
async function validate(type, input, options = {}) {
|
|
2025
|
+
let resolved = input;
|
|
2026
|
+
if (typeof input === "string") {
|
|
2027
|
+
if (input.trim() === "") throw new Error(`${type} is required`);
|
|
2028
|
+
resolved = await loadJsonConfig(input);
|
|
2029
|
+
}
|
|
2030
|
+
const misuse = inapplicableOptions(type, options);
|
|
2031
|
+
if (misuse.length > 0) {
|
|
2032
|
+
return {
|
|
2033
|
+
valid: false,
|
|
2034
|
+
type,
|
|
2035
|
+
errors: misuse,
|
|
2036
|
+
warnings: [],
|
|
2037
|
+
details: { scope: { flows: [], checks: [] }, skipped: [] }
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
const result = await validateByType(type, input, resolved, options);
|
|
2041
|
+
return options.strict === true ? escalateStrict(result) : result;
|
|
2042
|
+
}
|
|
2043
|
+
function escalateStrict(result) {
|
|
2044
|
+
const skipped = result.details.skipped ?? [];
|
|
2045
|
+
if (result.warnings.length === 0 && skipped.length === 0) return result;
|
|
2046
|
+
return { ...result, valid: false };
|
|
2047
|
+
}
|
|
2048
|
+
async function validateByType(type, input, resolved, options) {
|
|
2049
|
+
switch (type) {
|
|
2050
|
+
case "contract":
|
|
2051
|
+
return withDocumentScope(validateContractInput(resolved), "contract");
|
|
2052
|
+
case "event":
|
|
2053
|
+
return withDocumentScope(validateEvent2(resolved), "event");
|
|
2054
|
+
case "flow":
|
|
2055
|
+
if (options.path !== void 0) {
|
|
2056
|
+
return validateEntry(options.path, resolved, {
|
|
2057
|
+
flow: options.flow,
|
|
2058
|
+
configDir: options.configDir ?? configDirOf(input)
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
return validateFlowWithPackages(resolved, {
|
|
2062
|
+
flow: options.flow,
|
|
2063
|
+
strict: options.strict,
|
|
2064
|
+
offline: options.offline,
|
|
2065
|
+
configDir: options.configDir ?? configDirOf(input)
|
|
2066
|
+
});
|
|
2067
|
+
case "mapping":
|
|
2068
|
+
return withDocumentScope(validateMapping(resolved), "mapping");
|
|
2069
|
+
default:
|
|
2070
|
+
throw new Error(`Unknown validation type: ${type}`);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
export {
|
|
2074
|
+
VALIDATE_CODES,
|
|
2075
|
+
validate
|
|
2076
|
+
};
|
|
2077
|
+
//# sourceMappingURL=validate.js.map
|