@waypointjs/core 0.1.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/LICENSE +21 -0
- package/dist/index.cjs +785 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +385 -0
- package/dist/index.d.ts +385 -0
- package/dist/index.js +758 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { createStore } from 'zustand/vanilla';
|
|
2
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
// src/conditions.ts
|
|
6
|
+
function resolveFieldValue(path, data, externalVars) {
|
|
7
|
+
if (path.startsWith("$ext.")) {
|
|
8
|
+
const varId = path.slice(5);
|
|
9
|
+
return externalVars[varId];
|
|
10
|
+
}
|
|
11
|
+
const dotIndex = path.indexOf(".");
|
|
12
|
+
if (dotIndex === -1) return void 0;
|
|
13
|
+
const stepId = path.slice(0, dotIndex);
|
|
14
|
+
const fieldId = path.slice(dotIndex + 1);
|
|
15
|
+
return data[stepId]?.[fieldId];
|
|
16
|
+
}
|
|
17
|
+
function evaluateOperator(operator, actual, expected) {
|
|
18
|
+
switch (operator) {
|
|
19
|
+
case "exists":
|
|
20
|
+
return actual !== void 0 && actual !== null && actual !== "";
|
|
21
|
+
case "notExists":
|
|
22
|
+
return actual === void 0 || actual === null || actual === "";
|
|
23
|
+
case "equals":
|
|
24
|
+
return actual === expected;
|
|
25
|
+
case "notEquals":
|
|
26
|
+
return actual !== expected;
|
|
27
|
+
case "greaterThan":
|
|
28
|
+
return typeof actual === "number" && typeof expected === "number" ? actual > expected : Number(actual) > Number(expected);
|
|
29
|
+
case "greaterThanOrEqual":
|
|
30
|
+
return typeof actual === "number" && typeof expected === "number" ? actual >= expected : Number(actual) >= Number(expected);
|
|
31
|
+
case "lessThan":
|
|
32
|
+
return typeof actual === "number" && typeof expected === "number" ? actual < expected : Number(actual) < Number(expected);
|
|
33
|
+
case "lessThanOrEqual":
|
|
34
|
+
return typeof actual === "number" && typeof expected === "number" ? actual <= expected : Number(actual) <= Number(expected);
|
|
35
|
+
case "contains":
|
|
36
|
+
if (typeof actual === "string" && typeof expected === "string") {
|
|
37
|
+
return actual.includes(expected);
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(actual)) {
|
|
40
|
+
return actual.includes(expected);
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
case "notContains":
|
|
44
|
+
if (typeof actual === "string" && typeof expected === "string") {
|
|
45
|
+
return !actual.includes(expected);
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(actual)) {
|
|
48
|
+
return !actual.includes(expected);
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
case "in":
|
|
52
|
+
return Array.isArray(expected) && expected.includes(actual);
|
|
53
|
+
case "notIn":
|
|
54
|
+
return Array.isArray(expected) && !expected.includes(actual);
|
|
55
|
+
case "matches":
|
|
56
|
+
if (typeof actual !== "string" || typeof expected !== "string") return false;
|
|
57
|
+
try {
|
|
58
|
+
return new RegExp(expected).test(actual);
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function evaluateRule(rule, data, externalVars) {
|
|
67
|
+
const actual = resolveFieldValue(rule.field, data, externalVars);
|
|
68
|
+
return evaluateOperator(rule.operator, actual, rule.value);
|
|
69
|
+
}
|
|
70
|
+
function evaluateConditionGroup(group, data, externalVars) {
|
|
71
|
+
const ruleResults = group.rules.map(
|
|
72
|
+
(rule) => evaluateRule(rule, data, externalVars)
|
|
73
|
+
);
|
|
74
|
+
const groupResults = (group.groups ?? []).map(
|
|
75
|
+
(subGroup) => evaluateConditionGroup(subGroup, data, externalVars)
|
|
76
|
+
);
|
|
77
|
+
const allResults = [...ruleResults, ...groupResults];
|
|
78
|
+
if (allResults.length === 0) return true;
|
|
79
|
+
return group.combinator === "and" ? allResults.every(Boolean) : allResults.some(Boolean);
|
|
80
|
+
}
|
|
81
|
+
function isVisible(visibleWhen, data, externalVars) {
|
|
82
|
+
if (!visibleWhen) return true;
|
|
83
|
+
return evaluateConditionGroup(visibleWhen, data, externalVars);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/tree-resolver.ts
|
|
87
|
+
function areDependenciesMet(dependsOn, data, externalVars) {
|
|
88
|
+
if (!dependsOn || dependsOn.length === 0) return true;
|
|
89
|
+
return dependsOn.every((path) => {
|
|
90
|
+
const value = resolveFieldValue(path, data, externalVars);
|
|
91
|
+
return value !== void 0 && value !== null && value !== "";
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function findMissingBlockingVars(externalVariables, externalVars, visibleSteps) {
|
|
95
|
+
if (!externalVariables || externalVariables.length === 0) return [];
|
|
96
|
+
const visibleStepIds = new Set(visibleSteps.map((s) => s.id));
|
|
97
|
+
return externalVariables.filter((extVar) => {
|
|
98
|
+
if (!extVar.blocking) return false;
|
|
99
|
+
const usedInVisibleStep = extVar.usedIn?.some(
|
|
100
|
+
(ref) => visibleStepIds.has(ref.stepId)
|
|
101
|
+
);
|
|
102
|
+
if (!usedInVisibleStep) return false;
|
|
103
|
+
const value = externalVars[extVar.id];
|
|
104
|
+
return value === void 0 || value === null;
|
|
105
|
+
}).map((extVar) => extVar.id);
|
|
106
|
+
}
|
|
107
|
+
function resolveTree(schema, data, externalVars) {
|
|
108
|
+
const visibleSteps = [];
|
|
109
|
+
const hiddenSteps = [];
|
|
110
|
+
for (const stepDef of schema.steps) {
|
|
111
|
+
const stepVisible = isVisible(stepDef.visibleWhen, data, externalVars);
|
|
112
|
+
const resolvedFields = stepDef.fields.map((fieldDef) => ({
|
|
113
|
+
definition: fieldDef,
|
|
114
|
+
visible: isVisible(fieldDef.visibleWhen, data, externalVars),
|
|
115
|
+
dependenciesMet: areDependenciesMet(fieldDef.dependsOn, data, externalVars)
|
|
116
|
+
}));
|
|
117
|
+
const resolvedStep = {
|
|
118
|
+
definition: stepDef,
|
|
119
|
+
visible: stepVisible,
|
|
120
|
+
fields: resolvedFields
|
|
121
|
+
};
|
|
122
|
+
if (stepVisible) {
|
|
123
|
+
visibleSteps.push(resolvedStep);
|
|
124
|
+
} else {
|
|
125
|
+
hiddenSteps.push(resolvedStep);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const missingExternalVars = findMissingBlockingVars(
|
|
129
|
+
schema.externalVariables,
|
|
130
|
+
externalVars,
|
|
131
|
+
visibleSteps.map((s) => s.definition)
|
|
132
|
+
);
|
|
133
|
+
return {
|
|
134
|
+
steps: visibleSteps,
|
|
135
|
+
hiddenSteps,
|
|
136
|
+
missingExternalVars
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function findStepIndex(steps, stepId) {
|
|
140
|
+
return steps.findIndex((s) => s.definition.id === stepId);
|
|
141
|
+
}
|
|
142
|
+
function getNextStep(steps, currentStepId) {
|
|
143
|
+
const index = findStepIndex(steps, currentStepId);
|
|
144
|
+
if (index === -1 || index === steps.length - 1) return void 0;
|
|
145
|
+
return steps[index + 1];
|
|
146
|
+
}
|
|
147
|
+
function getPreviousStep(steps, currentStepId) {
|
|
148
|
+
const index = findStepIndex(steps, currentStepId);
|
|
149
|
+
if (index <= 0) return void 0;
|
|
150
|
+
return steps[index - 1];
|
|
151
|
+
}
|
|
152
|
+
function calculateProgress(steps, currentStepId) {
|
|
153
|
+
if (steps.length === 0) return 0;
|
|
154
|
+
const index = findStepIndex(steps, currentStepId);
|
|
155
|
+
if (index === -1) return 0;
|
|
156
|
+
return Math.round((index + 1) / (steps.length + 1) * 100);
|
|
157
|
+
}
|
|
158
|
+
function findLastValidStep(steps, data, _externalVars) {
|
|
159
|
+
let lastValid;
|
|
160
|
+
for (const step of steps) {
|
|
161
|
+
const visibleFields = step.fields.filter((f) => f.visible);
|
|
162
|
+
const allDepsMet = visibleFields.every((f) => f.dependenciesMet);
|
|
163
|
+
if (allDepsMet) {
|
|
164
|
+
const stepData = data[step.definition.id];
|
|
165
|
+
const hasData = stepData !== void 0 && Object.keys(stepData).length > 0;
|
|
166
|
+
if (hasData || step === steps[0]) {
|
|
167
|
+
lastValid = step;
|
|
168
|
+
} else {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return lastValid ?? steps[0];
|
|
176
|
+
}
|
|
177
|
+
function getResolvedTree(state) {
|
|
178
|
+
if (!state.schema) {
|
|
179
|
+
return { steps: [], hiddenSteps: [], missingExternalVars: [] };
|
|
180
|
+
}
|
|
181
|
+
return resolveTree(state.schema, state.data, state.externalVars);
|
|
182
|
+
}
|
|
183
|
+
function getCurrentStep(state) {
|
|
184
|
+
if (!state.currentStepId) return void 0;
|
|
185
|
+
return getResolvedTree(state).steps.find(
|
|
186
|
+
(s) => s.definition.id === state.currentStepId
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
function getNextStepFromState(state) {
|
|
190
|
+
if (!state.currentStepId) return void 0;
|
|
191
|
+
const tree = getResolvedTree(state);
|
|
192
|
+
return getNextStep(tree.steps, state.currentStepId);
|
|
193
|
+
}
|
|
194
|
+
function getPreviousStepFromState(state) {
|
|
195
|
+
if (!state.currentStepId) return void 0;
|
|
196
|
+
const tree = getResolvedTree(state);
|
|
197
|
+
return getPreviousStep(tree.steps, state.currentStepId);
|
|
198
|
+
}
|
|
199
|
+
function calculateProgressFromState(state) {
|
|
200
|
+
if (!state.currentStepId) return 0;
|
|
201
|
+
const tree = getResolvedTree(state);
|
|
202
|
+
return calculateProgress(tree.steps, state.currentStepId);
|
|
203
|
+
}
|
|
204
|
+
function getMissingBlockingVars(state) {
|
|
205
|
+
return getResolvedTree(state).missingExternalVars;
|
|
206
|
+
}
|
|
207
|
+
var initialState = {
|
|
208
|
+
schema: null,
|
|
209
|
+
data: {},
|
|
210
|
+
externalVars: {},
|
|
211
|
+
currentStepId: null,
|
|
212
|
+
history: [],
|
|
213
|
+
isSubmitting: false,
|
|
214
|
+
completed: false
|
|
215
|
+
};
|
|
216
|
+
function buildStateCreator() {
|
|
217
|
+
return (set, _get) => ({
|
|
218
|
+
...initialState,
|
|
219
|
+
init(schema, options = {}) {
|
|
220
|
+
const { data = {}, externalVars = {}, startStepId } = options;
|
|
221
|
+
const firstStepId = startStepId ?? schema.steps[0]?.id ?? null;
|
|
222
|
+
set({
|
|
223
|
+
schema,
|
|
224
|
+
data,
|
|
225
|
+
externalVars,
|
|
226
|
+
currentStepId: firstStepId,
|
|
227
|
+
history: firstStepId ? [firstStepId] : [],
|
|
228
|
+
isSubmitting: false,
|
|
229
|
+
completed: false
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
resume(schema, externalVars = {}) {
|
|
233
|
+
set((state) => ({
|
|
234
|
+
schema,
|
|
235
|
+
externalVars: { ...state.externalVars, ...externalVars },
|
|
236
|
+
isSubmitting: false
|
|
237
|
+
}));
|
|
238
|
+
},
|
|
239
|
+
setFieldValue(stepId, fieldId, value) {
|
|
240
|
+
set((state) => ({
|
|
241
|
+
data: {
|
|
242
|
+
...state.data,
|
|
243
|
+
[stepId]: { ...state.data[stepId], [fieldId]: value }
|
|
244
|
+
}
|
|
245
|
+
}));
|
|
246
|
+
},
|
|
247
|
+
setStepData(stepId, data) {
|
|
248
|
+
set((state) => ({
|
|
249
|
+
data: { ...state.data, [stepId]: data }
|
|
250
|
+
}));
|
|
251
|
+
},
|
|
252
|
+
setExternalVar(varId, value) {
|
|
253
|
+
set((state) => ({
|
|
254
|
+
externalVars: { ...state.externalVars, [varId]: value }
|
|
255
|
+
}));
|
|
256
|
+
},
|
|
257
|
+
setCurrentStep(stepId) {
|
|
258
|
+
set((state) => ({
|
|
259
|
+
currentStepId: stepId,
|
|
260
|
+
history: state.history.includes(stepId) ? state.history : [...state.history, stepId]
|
|
261
|
+
}));
|
|
262
|
+
},
|
|
263
|
+
setIsSubmitting(b) {
|
|
264
|
+
set({ isSubmitting: b });
|
|
265
|
+
},
|
|
266
|
+
setCompleted(b) {
|
|
267
|
+
set({ completed: b });
|
|
268
|
+
},
|
|
269
|
+
truncateHistoryAt(stepId) {
|
|
270
|
+
set((state) => {
|
|
271
|
+
const idx = state.history.indexOf(stepId);
|
|
272
|
+
if (idx === -1 || idx === state.history.length - 1) return state;
|
|
273
|
+
return { history: state.history.slice(0, idx + 1) };
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
reset() {
|
|
277
|
+
set(initialState);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function createRuntimeStore(options = {}) {
|
|
282
|
+
const { persistenceMode, schemaId } = options;
|
|
283
|
+
if (persistenceMode === "zustand") {
|
|
284
|
+
const storageKey = schemaId ? `waypoint-runtime-${schemaId}` : "waypoint-runtime";
|
|
285
|
+
return createStore()(
|
|
286
|
+
persist(buildStateCreator(), {
|
|
287
|
+
name: storageKey,
|
|
288
|
+
storage: createJSONStorage(() => {
|
|
289
|
+
if (typeof window === "undefined") {
|
|
290
|
+
return {
|
|
291
|
+
getItem: () => null,
|
|
292
|
+
setItem: () => {
|
|
293
|
+
},
|
|
294
|
+
removeItem: () => {
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return window.localStorage;
|
|
299
|
+
}),
|
|
300
|
+
partialize: (state) => ({
|
|
301
|
+
// Persist the schema id so WaypointRunner can verify the saved data
|
|
302
|
+
// belongs to this schema (guards against stale data from a renamed schema).
|
|
303
|
+
schemaId: state.schema?.id ?? null,
|
|
304
|
+
data: state.data,
|
|
305
|
+
currentStepId: state.currentStepId,
|
|
306
|
+
history: state.history,
|
|
307
|
+
completed: state.completed
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return createStore()(buildStateCreator());
|
|
313
|
+
}
|
|
314
|
+
function hasPersistedState(store, schemaId) {
|
|
315
|
+
const state = store.getState();
|
|
316
|
+
const persistedSchemaId = state["schemaId"];
|
|
317
|
+
return persistedSchemaId === schemaId && state.currentStepId !== null && !state.completed;
|
|
318
|
+
}
|
|
319
|
+
var customValidatorRegistry = {};
|
|
320
|
+
function registerCustomValidator(id, fn) {
|
|
321
|
+
customValidatorRegistry[id] = fn;
|
|
322
|
+
}
|
|
323
|
+
function buildFieldSchema(field) {
|
|
324
|
+
const rules = field.validation ?? [];
|
|
325
|
+
const isRequired = rules.some((r) => r.type === "required");
|
|
326
|
+
const isNumeric = field.type === "number";
|
|
327
|
+
const isCheckbox = field.type === "checkbox";
|
|
328
|
+
const isMultiSelect = field.type === "multiselect";
|
|
329
|
+
if (isCheckbox) {
|
|
330
|
+
const base = z.boolean();
|
|
331
|
+
return isRequired ? base : base.optional();
|
|
332
|
+
}
|
|
333
|
+
if (isMultiSelect) {
|
|
334
|
+
const base = z.array(z.string());
|
|
335
|
+
return isRequired ? base.min(1, rules.find((r) => r.type === "required")?.message ?? "Required") : base.optional();
|
|
336
|
+
}
|
|
337
|
+
if (isNumeric) {
|
|
338
|
+
let numSchema = z.coerce.number({
|
|
339
|
+
invalid_type_error: "Must be a number"
|
|
340
|
+
});
|
|
341
|
+
for (const rule of rules) {
|
|
342
|
+
if (rule.type === "min") {
|
|
343
|
+
const n = Number(rule.value);
|
|
344
|
+
if (!isNaN(n)) numSchema = numSchema.gte(n, rule.message);
|
|
345
|
+
} else if (rule.type === "max") {
|
|
346
|
+
const n = Number(rule.value);
|
|
347
|
+
if (!isNaN(n)) numSchema = numSchema.lte(n, rule.message);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return isRequired ? numSchema : numSchema.optional();
|
|
351
|
+
}
|
|
352
|
+
let strSchema = z.string();
|
|
353
|
+
const refineRules = [];
|
|
354
|
+
for (const rule of rules) {
|
|
355
|
+
switch (rule.type) {
|
|
356
|
+
case "required":
|
|
357
|
+
strSchema = strSchema.min(1, rule.message);
|
|
358
|
+
break;
|
|
359
|
+
case "minLength": {
|
|
360
|
+
const n = Number(rule.value);
|
|
361
|
+
if (!isNaN(n)) strSchema = strSchema.min(n, rule.message);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "maxLength": {
|
|
365
|
+
const n = Number(rule.value);
|
|
366
|
+
if (!isNaN(n)) strSchema = strSchema.max(n, rule.message);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
case "email":
|
|
370
|
+
strSchema = strSchema.email(rule.message);
|
|
371
|
+
break;
|
|
372
|
+
case "url":
|
|
373
|
+
strSchema = strSchema.url(rule.message);
|
|
374
|
+
break;
|
|
375
|
+
case "regex":
|
|
376
|
+
if (rule.value !== void 0 && rule.value !== null) {
|
|
377
|
+
strSchema = strSchema.regex(new RegExp(String(rule.value)), rule.message);
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
case "custom":
|
|
381
|
+
if (rule.customValidatorId && customValidatorRegistry[rule.customValidatorId]) {
|
|
382
|
+
refineRules.push({ id: rule.customValidatorId, message: rule.message });
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
let finalSchema = isRequired ? strSchema : strSchema.optional();
|
|
388
|
+
for (const { id, message } of refineRules) {
|
|
389
|
+
finalSchema = finalSchema.refine(
|
|
390
|
+
(val) => Boolean(customValidatorRegistry[id]?.(val)),
|
|
391
|
+
message
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return finalSchema;
|
|
395
|
+
}
|
|
396
|
+
function buildZodSchema(fields) {
|
|
397
|
+
const shape = {};
|
|
398
|
+
for (const resolvedField of fields) {
|
|
399
|
+
if (!resolvedField.visible) continue;
|
|
400
|
+
shape[resolvedField.definition.id] = buildFieldSchema(resolvedField.definition);
|
|
401
|
+
}
|
|
402
|
+
return z.object(shape);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/validate-schema.ts
|
|
406
|
+
var SUPPORTED_VERSIONS = ["1"];
|
|
407
|
+
var VALID_PERSISTENCE_MODES = /* @__PURE__ */ new Set(["zustand", "backend-step", "backend-manual"]);
|
|
408
|
+
var VALID_EXT_VAR_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "object"]);
|
|
409
|
+
var VALID_CONDITION_OPERATORS = /* @__PURE__ */ new Set([
|
|
410
|
+
"equals",
|
|
411
|
+
"notEquals",
|
|
412
|
+
"greaterThan",
|
|
413
|
+
"greaterThanOrEqual",
|
|
414
|
+
"lessThan",
|
|
415
|
+
"lessThanOrEqual",
|
|
416
|
+
"contains",
|
|
417
|
+
"notContains",
|
|
418
|
+
"in",
|
|
419
|
+
"notIn",
|
|
420
|
+
"exists",
|
|
421
|
+
"notExists",
|
|
422
|
+
"matches"
|
|
423
|
+
]);
|
|
424
|
+
var VALID_VALIDATION_TYPES = /* @__PURE__ */ new Set([
|
|
425
|
+
"required",
|
|
426
|
+
"min",
|
|
427
|
+
"max",
|
|
428
|
+
"minLength",
|
|
429
|
+
"maxLength",
|
|
430
|
+
"email",
|
|
431
|
+
"url",
|
|
432
|
+
"regex",
|
|
433
|
+
"custom"
|
|
434
|
+
]);
|
|
435
|
+
function validateSchema(raw) {
|
|
436
|
+
const errors = [];
|
|
437
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
438
|
+
return { valid: false, errors: ["Root must be a JSON object"] };
|
|
439
|
+
}
|
|
440
|
+
const obj = raw;
|
|
441
|
+
if (!("version" in obj)) {
|
|
442
|
+
errors.push('Missing required field "version"');
|
|
443
|
+
} else if (!SUPPORTED_VERSIONS.includes(obj.version)) {
|
|
444
|
+
errors.push(`Unsupported version "${obj.version}". Supported: ${SUPPORTED_VERSIONS.join(", ")}`);
|
|
445
|
+
}
|
|
446
|
+
if (!("id" in obj) || typeof obj.id !== "string" || !obj.id.trim()) {
|
|
447
|
+
errors.push('Field "id" must be a non-empty string');
|
|
448
|
+
}
|
|
449
|
+
if (!("name" in obj) || typeof obj.name !== "string" || !obj.name.trim()) {
|
|
450
|
+
errors.push('Field "name" must be a non-empty string');
|
|
451
|
+
}
|
|
452
|
+
if (!("steps" in obj) || !Array.isArray(obj.steps)) {
|
|
453
|
+
errors.push('Field "steps" must be an array');
|
|
454
|
+
} else {
|
|
455
|
+
validateSteps(obj.steps, errors);
|
|
456
|
+
}
|
|
457
|
+
if ("persistenceMode" in obj && !VALID_PERSISTENCE_MODES.has(obj.persistenceMode)) {
|
|
458
|
+
errors.push(
|
|
459
|
+
`Invalid "persistenceMode" "${obj.persistenceMode}". Must be one of: ${[...VALID_PERSISTENCE_MODES].join(", ")}`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if ("externalVariables" in obj) {
|
|
463
|
+
if (!Array.isArray(obj.externalVariables)) {
|
|
464
|
+
errors.push('"externalVariables" must be an array');
|
|
465
|
+
} else {
|
|
466
|
+
validateExternalVariables(obj.externalVariables, errors);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if ("customTypes" in obj && !Array.isArray(obj.customTypes)) {
|
|
470
|
+
errors.push('"customTypes" must be an array');
|
|
471
|
+
}
|
|
472
|
+
return { valid: errors.length === 0, errors };
|
|
473
|
+
}
|
|
474
|
+
function validateSteps(steps, errors) {
|
|
475
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
476
|
+
steps.forEach((step, i) => {
|
|
477
|
+
const prefix = `steps[${i}]`;
|
|
478
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
479
|
+
errors.push(`${prefix}: must be an object`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const s = step;
|
|
483
|
+
if (typeof s.id !== "string" || !s.id.trim()) {
|
|
484
|
+
errors.push(`${prefix}: "id" must be a non-empty string`);
|
|
485
|
+
} else if (seenIds.has(s.id)) {
|
|
486
|
+
errors.push(`${prefix}: duplicate step id "${s.id}"`);
|
|
487
|
+
} else {
|
|
488
|
+
seenIds.add(s.id);
|
|
489
|
+
}
|
|
490
|
+
if (typeof s.title !== "string" || !s.title.trim()) {
|
|
491
|
+
errors.push(`${prefix}: "title" must be a non-empty string`);
|
|
492
|
+
}
|
|
493
|
+
if (typeof s.url !== "string" || !s.url.trim()) {
|
|
494
|
+
errors.push(`${prefix}: "url" must be a non-empty string`);
|
|
495
|
+
}
|
|
496
|
+
if (!Array.isArray(s.fields)) {
|
|
497
|
+
errors.push(`${prefix}: "fields" must be an array`);
|
|
498
|
+
} else {
|
|
499
|
+
validateFields(s.fields, s.id, prefix, errors);
|
|
500
|
+
}
|
|
501
|
+
if ("visibleWhen" in s) {
|
|
502
|
+
validateConditionGroup(s.visibleWhen, `${prefix}.visibleWhen`, errors);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function validateFields(fields, stepId, parentPrefix, errors) {
|
|
507
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
508
|
+
fields.forEach((field, i) => {
|
|
509
|
+
const prefix = `${parentPrefix}.fields[${i}]`;
|
|
510
|
+
if (!field || typeof field !== "object" || Array.isArray(field)) {
|
|
511
|
+
errors.push(`${prefix}: must be an object`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const f = field;
|
|
515
|
+
if (typeof f.id !== "string" || !f.id.trim()) {
|
|
516
|
+
errors.push(`${prefix}: "id" must be a non-empty string`);
|
|
517
|
+
} else if (seenIds.has(f.id)) {
|
|
518
|
+
errors.push(`${prefix}: duplicate field id "${f.id}" in step "${stepId}"`);
|
|
519
|
+
} else {
|
|
520
|
+
seenIds.add(f.id);
|
|
521
|
+
}
|
|
522
|
+
if (typeof f.type !== "string" || !f.type.trim()) {
|
|
523
|
+
errors.push(`${prefix}: "type" must be a non-empty string`);
|
|
524
|
+
}
|
|
525
|
+
if (typeof f.label !== "string" || !f.label.trim()) {
|
|
526
|
+
errors.push(`${prefix}: "label" must be a non-empty string`);
|
|
527
|
+
}
|
|
528
|
+
if ("options" in f && !Array.isArray(f.options)) {
|
|
529
|
+
errors.push(`${prefix}: "options" must be an array`);
|
|
530
|
+
}
|
|
531
|
+
if ("validation" in f) {
|
|
532
|
+
if (!Array.isArray(f.validation)) {
|
|
533
|
+
errors.push(`${prefix}: "validation" must be an array`);
|
|
534
|
+
} else {
|
|
535
|
+
validateValidationRules(f.validation, prefix, errors);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if ("dependsOn" in f) {
|
|
539
|
+
if (!Array.isArray(f.dependsOn)) {
|
|
540
|
+
errors.push(`${prefix}: "dependsOn" must be an array`);
|
|
541
|
+
} else {
|
|
542
|
+
for (const dep of f.dependsOn) {
|
|
543
|
+
if (typeof dep !== "string") {
|
|
544
|
+
errors.push(`${prefix}: each "dependsOn" entry must be a string`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if ("visibleWhen" in f) {
|
|
550
|
+
validateConditionGroup(f.visibleWhen, `${prefix}.visibleWhen`, errors);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function validateConditionGroup(group, prefix, errors) {
|
|
555
|
+
if (!group || typeof group !== "object" || Array.isArray(group)) {
|
|
556
|
+
errors.push(`${prefix}: must be an object`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const g = group;
|
|
560
|
+
if (g.combinator !== "and" && g.combinator !== "or") {
|
|
561
|
+
errors.push(`${prefix}: "combinator" must be "and" or "or"`);
|
|
562
|
+
}
|
|
563
|
+
if (!Array.isArray(g.rules)) {
|
|
564
|
+
errors.push(`${prefix}: "rules" must be an array`);
|
|
565
|
+
} else {
|
|
566
|
+
g.rules.forEach((rule, i) => {
|
|
567
|
+
validateConditionRule(rule, `${prefix}.rules[${i}]`, errors);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if ("groups" in g && Array.isArray(g.groups)) {
|
|
571
|
+
g.groups.forEach((nested, i) => {
|
|
572
|
+
validateConditionGroup(nested, `${prefix}.groups[${i}]`, errors);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function validateConditionRule(rule, prefix, errors) {
|
|
577
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
578
|
+
errors.push(`${prefix}: must be an object`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const r = rule;
|
|
582
|
+
if (typeof r.field !== "string" || !r.field.trim()) {
|
|
583
|
+
errors.push(`${prefix}: "field" must be a non-empty string`);
|
|
584
|
+
}
|
|
585
|
+
if (!VALID_CONDITION_OPERATORS.has(r.operator)) {
|
|
586
|
+
errors.push(
|
|
587
|
+
`${prefix}: invalid operator "${r.operator}". Must be one of: ${[...VALID_CONDITION_OPERATORS].join(", ")}`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function validateValidationRules(rules, parentPrefix, errors) {
|
|
592
|
+
rules.forEach((rule, i) => {
|
|
593
|
+
const prefix = `${parentPrefix}.validation[${i}]`;
|
|
594
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
595
|
+
errors.push(`${prefix}: must be an object`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const r = rule;
|
|
599
|
+
if (!VALID_VALIDATION_TYPES.has(r.type)) {
|
|
600
|
+
errors.push(
|
|
601
|
+
`${prefix}: invalid type "${r.type}". Must be one of: ${[...VALID_VALIDATION_TYPES].join(", ")}`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
if (typeof r.message !== "string" || !r.message.trim()) {
|
|
605
|
+
errors.push(`${prefix}: "message" must be a non-empty string`);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function validateExternalVariables(vars, errors) {
|
|
610
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
611
|
+
vars.forEach((v, i) => {
|
|
612
|
+
const prefix = `externalVariables[${i}]`;
|
|
613
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
614
|
+
errors.push(`${prefix}: must be an object`);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const ev = v;
|
|
618
|
+
if (typeof ev.id !== "string" || !ev.id.trim()) {
|
|
619
|
+
errors.push(`${prefix}: "id" must be a non-empty string`);
|
|
620
|
+
} else if (seenIds.has(ev.id)) {
|
|
621
|
+
errors.push(`${prefix}: duplicate external variable id "${ev.id}"`);
|
|
622
|
+
} else {
|
|
623
|
+
seenIds.add(ev.id);
|
|
624
|
+
}
|
|
625
|
+
if (typeof ev.label !== "string" || !ev.label.trim()) {
|
|
626
|
+
errors.push(`${prefix}: "label" must be a non-empty string`);
|
|
627
|
+
}
|
|
628
|
+
if (!VALID_EXT_VAR_TYPES.has(ev.type)) {
|
|
629
|
+
errors.push(
|
|
630
|
+
`${prefix}: invalid type "${ev.type}". Must be one of: ${[...VALID_EXT_VAR_TYPES].join(", ")}`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
if (typeof ev.blocking !== "boolean") {
|
|
634
|
+
errors.push(`${prefix}: "blocking" must be a boolean`);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function assertSchema(raw) {
|
|
639
|
+
const result = validateSchema(raw);
|
|
640
|
+
if (!result.valid) {
|
|
641
|
+
throw new Error(`Invalid WaypointSchema:
|
|
642
|
+
${result.errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/url.ts
|
|
647
|
+
var URLTemplateEngine = class {
|
|
648
|
+
/**
|
|
649
|
+
* Replace all `{{PARAM}}` placeholders in a template with the provided values.
|
|
650
|
+
* Missing params are left as-is.
|
|
651
|
+
*/
|
|
652
|
+
static format(template, params) {
|
|
653
|
+
return template.replace(this.PLACEHOLDER_PATTERN, (match, key) => {
|
|
654
|
+
const value = params[key];
|
|
655
|
+
if (value === void 0 || value === null) {
|
|
656
|
+
console.log(`Waypoint: missing parameter "${key}" in URL: ${template}`);
|
|
657
|
+
return match;
|
|
658
|
+
}
|
|
659
|
+
return String(value);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Extract all placeholder names from a URL template.
|
|
664
|
+
*/
|
|
665
|
+
static extractPlaceholders(template) {
|
|
666
|
+
const matches = template.match(this.PLACEHOLDER_PATTERN);
|
|
667
|
+
return matches ? matches.map((m) => m.replace(/[{}]/g, "")) : [];
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Validate that all placeholders in the template have corresponding params.
|
|
671
|
+
*/
|
|
672
|
+
static validate(template, params) {
|
|
673
|
+
const placeholders = this.extractPlaceholders(template);
|
|
674
|
+
const missingParams = placeholders.filter((key) => params[key] === void 0);
|
|
675
|
+
return { isValid: missingParams.length === 0, missingParams };
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
URLTemplateEngine.PLACEHOLDER_PATTERN = /\{\{([^}]+)\}\}/g;
|
|
679
|
+
function doesPatternMatchURL(pathname, urlPattern) {
|
|
680
|
+
const regexPattern = urlPattern.replace(/\{\{([^}]+)\}\}/g, "([^/]+)").replace(/\//g, "\\/");
|
|
681
|
+
return new RegExp(`^${regexPattern}$`).test(pathname);
|
|
682
|
+
}
|
|
683
|
+
function doesPatternPartiallyMatch(pathname, urlPattern) {
|
|
684
|
+
const patternBase = urlPattern.replace(/\{\{[^}]+\}\}/g, "*").split("/").filter((segment) => segment !== "*" && segment !== "");
|
|
685
|
+
const pathSegments = pathname.split("/").filter((s) => s !== "");
|
|
686
|
+
let patternIndex = 0;
|
|
687
|
+
for (const seg of pathSegments) {
|
|
688
|
+
if (patternIndex < patternBase.length && seg === patternBase[patternIndex]) {
|
|
689
|
+
patternIndex++;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return patternIndex === patternBase.length;
|
|
693
|
+
}
|
|
694
|
+
function extractParamsFromPattern(pathname, urlPattern) {
|
|
695
|
+
const params = {};
|
|
696
|
+
const placeholders = [];
|
|
697
|
+
const regexPattern = urlPattern.replace(
|
|
698
|
+
/\{\{([^}]+)\}\}/g,
|
|
699
|
+
(_match, paramName) => {
|
|
700
|
+
placeholders.push(paramName);
|
|
701
|
+
return "([^/]+)";
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
const regex = new RegExp(`^${regexPattern.replace(/\//g, "\\/")}$`);
|
|
705
|
+
const matches = pathname.match(regex);
|
|
706
|
+
if (matches) {
|
|
707
|
+
for (let i = 1; i < matches.length; i++) {
|
|
708
|
+
const paramName = placeholders[i - 1];
|
|
709
|
+
const paramValue = matches[i];
|
|
710
|
+
if (paramName.includes("_ID") && /^\d+$/.test(paramValue)) {
|
|
711
|
+
params[paramName] = parseInt(paramValue, 10);
|
|
712
|
+
} else {
|
|
713
|
+
params[paramName] = paramValue;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return params;
|
|
718
|
+
}
|
|
719
|
+
function findMatchingStep(pathname, allSteps) {
|
|
720
|
+
for (const step of allSteps) {
|
|
721
|
+
if (doesPatternMatchURL(pathname, step.url)) return step;
|
|
722
|
+
}
|
|
723
|
+
for (const step of allSteps) {
|
|
724
|
+
if (doesPatternPartiallyMatch(pathname, step.url)) return step;
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
function extractURLParamsFromTree(pathname, allSteps) {
|
|
729
|
+
const matching = findMatchingStep(pathname, allSteps);
|
|
730
|
+
if (!matching) return {};
|
|
731
|
+
return extractParamsFromPattern(pathname, matching.url);
|
|
732
|
+
}
|
|
733
|
+
function extractOnlyMissingParams(pathname, allSteps, missingParamNames) {
|
|
734
|
+
if (missingParamNames.length === 0) return {};
|
|
735
|
+
const matching = findMatchingStep(pathname, allSteps);
|
|
736
|
+
if (!matching) return {};
|
|
737
|
+
const all = extractParamsFromPattern(pathname, matching.url);
|
|
738
|
+
const filtered = {};
|
|
739
|
+
for (const name of missingParamNames) {
|
|
740
|
+
if (all[name] !== void 0) filtered[name] = all[name];
|
|
741
|
+
}
|
|
742
|
+
return filtered;
|
|
743
|
+
}
|
|
744
|
+
function mergeContextParams(userParams = {}, pathname, allSteps, targetURL) {
|
|
745
|
+
let urlParams = {};
|
|
746
|
+
if (targetURL) {
|
|
747
|
+
const required = URLTemplateEngine.extractPlaceholders(targetURL);
|
|
748
|
+
const missing = required.filter((p) => userParams[p] === void 0);
|
|
749
|
+
urlParams = extractOnlyMissingParams(pathname, allSteps, missing);
|
|
750
|
+
} else {
|
|
751
|
+
urlParams = extractURLParamsFromTree(pathname, allSteps);
|
|
752
|
+
}
|
|
753
|
+
return { ...urlParams, ...userParams };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export { URLTemplateEngine, assertSchema, buildZodSchema, calculateProgress, calculateProgressFromState, createRuntimeStore, evaluateConditionGroup, extractOnlyMissingParams, extractURLParamsFromTree, findLastValidStep, findMatchingStep, findStepIndex, getCurrentStep, getMissingBlockingVars, getNextStep, getNextStepFromState, getPreviousStep, getPreviousStepFromState, getResolvedTree, hasPersistedState, isVisible, mergeContextParams, registerCustomValidator, resolveFieldValue, resolveTree, validateSchema };
|
|
757
|
+
//# sourceMappingURL=index.js.map
|
|
758
|
+
//# sourceMappingURL=index.js.map
|