@waypointjs/core 0.1.4 → 0.1.6
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/dist/index.cjs +194 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -11
- package/dist/index.d.ts +61 -11
- package/dist/index.js +194 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
* Auto-sufficient tree definition generated by @waypointjs/builder.
|
|
8
8
|
* Versioned to allow non-breaking evolution.
|
|
9
9
|
*/
|
|
10
|
-
type ConditionOperator = "equals" | "notEquals" | "greaterThan" | "greaterThanOrEqual" | "lessThan" | "lessThanOrEqual" | "contains" | "notContains" | "in" | "notIn" | "exists" | "notExists" | "matches";
|
|
10
|
+
type ConditionOperator = "equals" | "notEquals" | "greaterThan" | "greaterThanOrEqual" | "lessThan" | "lessThanOrEqual" | "contains" | "notContains" | "in" | "notIn" | "exists" | "notExists" | "matches" | "inEnum" | "notInEnum";
|
|
11
11
|
/**
|
|
12
12
|
* A single condition rule.
|
|
13
13
|
* `field` is a dot-path: "stepId.fieldId" for journey data, or "$ext.varId" for external variables.
|
|
@@ -27,7 +27,7 @@ interface ConditionGroup {
|
|
|
27
27
|
rules: ConditionRule[];
|
|
28
28
|
groups?: ConditionGroup[];
|
|
29
29
|
}
|
|
30
|
-
type ValidationRuleType = "required" | "min" | "max" | "minLength" | "maxLength" | "email" | "url" | "regex" | "custom";
|
|
30
|
+
type ValidationRuleType = "required" | "min" | "max" | "minLength" | "maxLength" | "email" | "url" | "regex" | "custom" | "equals" | "notEquals" | "greaterThan" | "greaterThanOrEqual" | "lessThan" | "lessThanOrEqual" | "contains" | "notContains" | "matches" | "inEnum" | "notInEnum";
|
|
31
31
|
interface ValidationRule {
|
|
32
32
|
type: ValidationRuleType;
|
|
33
33
|
/** Value for min/max/minLength/maxLength/regex rules */
|
|
@@ -36,6 +36,11 @@ interface ValidationRule {
|
|
|
36
36
|
message: string;
|
|
37
37
|
/** For "custom" type: identifier of the validator function registered at runtime */
|
|
38
38
|
customValidatorId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Cross-field reference — "stepId.fieldId" path.
|
|
41
|
+
* When set, the comparison target is resolved from journey data instead of using `value`.
|
|
42
|
+
*/
|
|
43
|
+
refField?: string;
|
|
39
44
|
}
|
|
40
45
|
type BuiltinFieldType = "text" | "number" | "email" | "password" | "tel" | "url" | "textarea" | "select" | "multiselect" | "checkbox" | "radio" | "date" | "file";
|
|
41
46
|
/** Field type — either a builtin or a custom type id registered at runtime */
|
|
@@ -44,14 +49,29 @@ interface SelectOption {
|
|
|
44
49
|
label: string;
|
|
45
50
|
value: string | number;
|
|
46
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* A dynamic default rule: if the condition matches, use the given value
|
|
54
|
+
* as the field's default. First matching rule wins.
|
|
55
|
+
*/
|
|
56
|
+
interface DynamicDefaultRule {
|
|
57
|
+
when: ConditionGroup;
|
|
58
|
+
value: unknown;
|
|
59
|
+
}
|
|
47
60
|
interface FieldDefinition {
|
|
48
61
|
id: string;
|
|
49
62
|
type: FieldType;
|
|
50
63
|
label: string;
|
|
51
64
|
placeholder?: string;
|
|
52
65
|
defaultValue?: unknown;
|
|
53
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Conditional defaults — evaluated in order, first match wins.
|
|
68
|
+
* Falls back to `defaultValue` if no rule matches.
|
|
69
|
+
*/
|
|
70
|
+
dynamicDefault?: DynamicDefaultRule[];
|
|
71
|
+
/** Options for select/multiselect/radio fields (hardcoded) */
|
|
54
72
|
options?: SelectOption[];
|
|
73
|
+
/** Reference to an external enum — options resolved at runtime from app-provided enums */
|
|
74
|
+
externalEnumId?: string;
|
|
55
75
|
validation?: ValidationRule[];
|
|
56
76
|
/** Controls visibility of this field within its step */
|
|
57
77
|
visibleWhen?: ConditionGroup;
|
|
@@ -68,6 +88,8 @@ interface StepDefinition$1 {
|
|
|
68
88
|
visibleWhen?: ConditionGroup;
|
|
69
89
|
/** Whether this step can be used as a resume point */
|
|
70
90
|
enableResumeFromHere?: boolean;
|
|
91
|
+
/** Whether the user can skip this step without filling required fields */
|
|
92
|
+
skippable?: boolean;
|
|
71
93
|
}
|
|
72
94
|
interface ExternalVariable {
|
|
73
95
|
id: string;
|
|
@@ -82,6 +104,16 @@ interface ExternalVariable {
|
|
|
82
104
|
fieldId?: string;
|
|
83
105
|
}>;
|
|
84
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* An externally-provided list of options for select/multiselect/radio fields.
|
|
109
|
+
* The actual values are injected at runtime (builder prop / runner prop) so
|
|
110
|
+
* the schema only stores a reference (`externalEnumId`).
|
|
111
|
+
*/
|
|
112
|
+
interface ExternalEnum {
|
|
113
|
+
id: string;
|
|
114
|
+
label: string;
|
|
115
|
+
values: SelectOption[];
|
|
116
|
+
}
|
|
85
117
|
/**
|
|
86
118
|
* A custom field type registered in the builder.
|
|
87
119
|
* `metadata` is passed through as-is so the backend can enrich it.
|
|
@@ -128,20 +160,21 @@ type JourneyData = Record<string, Record<string, unknown>>;
|
|
|
128
160
|
type ExternalVars = Record<string, unknown>;
|
|
129
161
|
/**
|
|
130
162
|
* Resolves a field path to its value from the data context.
|
|
131
|
-
* - "stepId.fieldId"
|
|
132
|
-
* - "$ext.varId"
|
|
163
|
+
* - "stepId.fieldId" → journey data
|
|
164
|
+
* - "$ext.varId" → external variable
|
|
165
|
+
* - "$step.stepId.skipped" → whether the step was skipped (boolean)
|
|
133
166
|
*/
|
|
134
|
-
declare function resolveFieldValue(path: string, data: JourneyData, externalVars: ExternalVars): unknown;
|
|
167
|
+
declare function resolveFieldValue(path: string, data: JourneyData, externalVars: ExternalVars, skippedSteps?: string[]): unknown;
|
|
135
168
|
/**
|
|
136
169
|
* Evaluates a condition group against the current data context.
|
|
137
170
|
* Returns true if the group's conditions are satisfied.
|
|
138
171
|
*/
|
|
139
|
-
declare function evaluateConditionGroup(group: ConditionGroup, data: JourneyData, externalVars: ExternalVars): boolean;
|
|
172
|
+
declare function evaluateConditionGroup(group: ConditionGroup, data: JourneyData, externalVars: ExternalVars, externalEnums?: ExternalEnum[], skippedSteps?: string[]): boolean;
|
|
140
173
|
/**
|
|
141
174
|
* Convenience: returns true if no condition is defined (always visible),
|
|
142
175
|
* or if the condition group evaluates to true.
|
|
143
176
|
*/
|
|
144
|
-
declare function isVisible(visibleWhen: ConditionGroup | undefined, data: JourneyData, externalVars: ExternalVars): boolean;
|
|
177
|
+
declare function isVisible(visibleWhen: ConditionGroup | undefined, data: JourneyData, externalVars: ExternalVars, externalEnums?: ExternalEnum[], skippedSteps?: string[]): boolean;
|
|
145
178
|
|
|
146
179
|
/** A field after condition evaluation */
|
|
147
180
|
interface ResolvedField {
|
|
@@ -150,6 +183,17 @@ interface ResolvedField {
|
|
|
150
183
|
visible: boolean;
|
|
151
184
|
/** Whether all dependsOn paths have a non-empty value */
|
|
152
185
|
dependenciesMet: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* Resolved options from an external enum.
|
|
188
|
+
* Set when `definition.externalEnumId` matches an enum in the provided `externalEnums`.
|
|
189
|
+
* Use `field.resolvedOptions ?? field.definition.options` when rendering.
|
|
190
|
+
*/
|
|
191
|
+
resolvedOptions?: SelectOption[];
|
|
192
|
+
/**
|
|
193
|
+
* Resolved dynamic default value (first matching `dynamicDefault` rule).
|
|
194
|
+
* Use `field.resolvedDefaultValue ?? field.definition.defaultValue` for initial form values.
|
|
195
|
+
*/
|
|
196
|
+
resolvedDefaultValue?: unknown;
|
|
153
197
|
}
|
|
154
198
|
/** A step after condition evaluation */
|
|
155
199
|
interface ResolvedStep {
|
|
@@ -177,7 +221,7 @@ interface ResolvedTree {
|
|
|
177
221
|
*
|
|
178
222
|
* This function is pure: same inputs always produce same outputs.
|
|
179
223
|
*/
|
|
180
|
-
declare function resolveTree(schema: WaypointSchema, data: JourneyData, externalVars: ExternalVars): ResolvedTree;
|
|
224
|
+
declare function resolveTree(schema: WaypointSchema, data: JourneyData, externalVars: ExternalVars, externalEnums?: ExternalEnum[], skippedSteps?: string[]): ResolvedTree;
|
|
181
225
|
/**
|
|
182
226
|
* Returns the index of the step with the given id in the resolved (visible) tree.
|
|
183
227
|
* Returns -1 if not found.
|
|
@@ -212,6 +256,8 @@ interface WaypointRuntimeState {
|
|
|
212
256
|
currentStepId: string | null;
|
|
213
257
|
/** Step IDs visited in order */
|
|
214
258
|
history: string[];
|
|
259
|
+
/** Step IDs that have been skipped by the user */
|
|
260
|
+
skippedSteps: string[];
|
|
215
261
|
isSubmitting: boolean;
|
|
216
262
|
/** True once onComplete has been called (all steps validated) */
|
|
217
263
|
completed: boolean;
|
|
@@ -238,6 +284,10 @@ interface WaypointRuntimeActions {
|
|
|
238
284
|
setCurrentStep(stepId: string): void;
|
|
239
285
|
setIsSubmitting(b: boolean): void;
|
|
240
286
|
setCompleted(b: boolean): void;
|
|
287
|
+
/** Marks a step as skipped (user chose to bypass it) */
|
|
288
|
+
skipStep(stepId: string): void;
|
|
289
|
+
/** Removes a step from the skipped list (e.g. when user goes back and fills it) */
|
|
290
|
+
unskipStep(stepId: string): void;
|
|
241
291
|
/**
|
|
242
292
|
* Truncates history to include only steps up to and including stepId.
|
|
243
293
|
* Called before navigating forward so stale steps from a previous path are removed.
|
|
@@ -288,7 +338,7 @@ declare function registerCustomValidator(id: string, fn: (value: unknown) => boo
|
|
|
288
338
|
* - Fields without a `required` validation rule are wrapped in `.optional()`.
|
|
289
339
|
* - Numeric fields use `z.coerce.number()` so string inputs are coerced.
|
|
290
340
|
*/
|
|
291
|
-
declare function buildZodSchema(fields: ResolvedField[]): z.ZodObject<z.ZodRawShape>;
|
|
341
|
+
declare function buildZodSchema(fields: ResolvedField[], externalEnums?: ExternalEnum[], data?: JourneyData): z.ZodObject<z.ZodRawShape>;
|
|
292
342
|
|
|
293
343
|
/**
|
|
294
344
|
* Runtime validation for WaypointSchema JSON.
|
|
@@ -382,4 +432,4 @@ declare function extractURLParamsFromTree(pathname: string, allSteps: StepDefini
|
|
|
382
432
|
declare function extractOnlyMissingParams(pathname: string, allSteps: StepDefinition[], missingParamNames: string[]): WaypointParams;
|
|
383
433
|
declare function mergeContextParams(userParams: WaypointParams | undefined, pathname: string, allSteps: StepDefinition[], targetURL?: string): WaypointParams;
|
|
384
434
|
|
|
385
|
-
export { type BuiltinFieldType, type ConditionGroup, type ConditionOperator, type ConditionRule, type CreateRuntimeStoreOptions, type CustomTypeDefinition, type ExternalVariable, type ExternalVars, type FieldDefinition, type FieldType, type JourneyData, type JourneyState, type JourneyTreeStep, type JourneyTreeType, type PersistenceMode, type ResolvedField, type ResolvedStep, type ResolvedTree, type RuntimeStore, type SchemaValidationResult, type SelectOption, type StepDefinition$1 as StepDefinition, URLTemplateEngine, type ValidationRule, type ValidationRuleType, type WaypointParams, type WaypointRuntimeActions, type WaypointRuntimeState, type WaypointRuntimeStore, type WaypointSchema, 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 };
|
|
435
|
+
export { type BuiltinFieldType, type ConditionGroup, type ConditionOperator, type ConditionRule, type CreateRuntimeStoreOptions, type CustomTypeDefinition, type DynamicDefaultRule, type ExternalEnum, type ExternalVariable, type ExternalVars, type FieldDefinition, type FieldType, type JourneyData, type JourneyState, type JourneyTreeStep, type JourneyTreeType, type PersistenceMode, type ResolvedField, type ResolvedStep, type ResolvedTree, type RuntimeStore, type SchemaValidationResult, type SelectOption, type StepDefinition$1 as StepDefinition, URLTemplateEngine, type ValidationRule, type ValidationRuleType, type WaypointParams, type WaypointRuntimeActions, type WaypointRuntimeState, type WaypointRuntimeStore, type WaypointSchema, 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 };
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,15 @@ import { persist, createJSONStorage } from 'zustand/middleware';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
// src/conditions.ts
|
|
6
|
-
function resolveFieldValue(path, data, externalVars) {
|
|
6
|
+
function resolveFieldValue(path, data, externalVars, skippedSteps) {
|
|
7
7
|
if (path.startsWith("$ext.")) {
|
|
8
8
|
const varId = path.slice(5);
|
|
9
9
|
return externalVars[varId];
|
|
10
10
|
}
|
|
11
|
+
if (path.startsWith("$step.") && path.endsWith(".skipped")) {
|
|
12
|
+
const stepId2 = path.slice(6, -8);
|
|
13
|
+
return skippedSteps?.includes(stepId2) ?? false;
|
|
14
|
+
}
|
|
11
15
|
const dotIndex = path.indexOf(".");
|
|
12
16
|
if (dotIndex === -1) return void 0;
|
|
13
17
|
const stepId = path.slice(0, dotIndex);
|
|
@@ -63,24 +67,29 @@ function evaluateOperator(operator, actual, expected) {
|
|
|
63
67
|
return false;
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
|
-
function evaluateRule(rule, data, externalVars) {
|
|
67
|
-
const actual = resolveFieldValue(rule.field, data, externalVars);
|
|
70
|
+
function evaluateRule(rule, data, externalVars, externalEnums, skippedSteps) {
|
|
71
|
+
const actual = resolveFieldValue(rule.field, data, externalVars, skippedSteps);
|
|
72
|
+
if ((rule.operator === "inEnum" || rule.operator === "notInEnum") && externalEnums) {
|
|
73
|
+
const enumDef = externalEnums.find((e) => e.id === String(rule.value));
|
|
74
|
+
const values = enumDef?.values.map((v) => String(v.value)) ?? [];
|
|
75
|
+
return rule.operator === "inEnum" ? values.includes(String(actual)) : !values.includes(String(actual));
|
|
76
|
+
}
|
|
68
77
|
return evaluateOperator(rule.operator, actual, rule.value);
|
|
69
78
|
}
|
|
70
|
-
function evaluateConditionGroup(group, data, externalVars) {
|
|
79
|
+
function evaluateConditionGroup(group, data, externalVars, externalEnums, skippedSteps) {
|
|
71
80
|
const ruleResults = group.rules.map(
|
|
72
|
-
(rule) => evaluateRule(rule, data, externalVars)
|
|
81
|
+
(rule) => evaluateRule(rule, data, externalVars, externalEnums, skippedSteps)
|
|
73
82
|
);
|
|
74
83
|
const groupResults = (group.groups ?? []).map(
|
|
75
|
-
(subGroup) => evaluateConditionGroup(subGroup, data, externalVars)
|
|
84
|
+
(subGroup) => evaluateConditionGroup(subGroup, data, externalVars, externalEnums, skippedSteps)
|
|
76
85
|
);
|
|
77
86
|
const allResults = [...ruleResults, ...groupResults];
|
|
78
87
|
if (allResults.length === 0) return true;
|
|
79
88
|
return group.combinator === "and" ? allResults.every(Boolean) : allResults.some(Boolean);
|
|
80
89
|
}
|
|
81
|
-
function isVisible(visibleWhen, data, externalVars) {
|
|
90
|
+
function isVisible(visibleWhen, data, externalVars, externalEnums, skippedSteps) {
|
|
82
91
|
if (!visibleWhen) return true;
|
|
83
|
-
return evaluateConditionGroup(visibleWhen, data, externalVars);
|
|
92
|
+
return evaluateConditionGroup(visibleWhen, data, externalVars, externalEnums, skippedSteps);
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
// src/tree-resolver.ts
|
|
@@ -104,16 +113,34 @@ function findMissingBlockingVars(externalVariables, externalVars, visibleSteps)
|
|
|
104
113
|
return value === void 0 || value === null;
|
|
105
114
|
}).map((extVar) => extVar.id);
|
|
106
115
|
}
|
|
107
|
-
function resolveTree(schema, data, externalVars) {
|
|
116
|
+
function resolveTree(schema, data, externalVars, externalEnums, skippedSteps) {
|
|
108
117
|
const visibleSteps = [];
|
|
109
118
|
const hiddenSteps = [];
|
|
110
119
|
for (const stepDef of schema.steps) {
|
|
111
|
-
const stepVisible = isVisible(stepDef.visibleWhen, data, externalVars);
|
|
112
|
-
const resolvedFields = stepDef.fields.map((fieldDef) =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
const stepVisible = isVisible(stepDef.visibleWhen, data, externalVars, externalEnums, skippedSteps);
|
|
121
|
+
const resolvedFields = stepDef.fields.map((fieldDef) => {
|
|
122
|
+
let resolvedOptions;
|
|
123
|
+
if (fieldDef.externalEnumId && externalEnums) {
|
|
124
|
+
const enumDef = externalEnums.find((e) => e.id === fieldDef.externalEnumId);
|
|
125
|
+
resolvedOptions = enumDef?.values;
|
|
126
|
+
}
|
|
127
|
+
let resolvedDefaultValue;
|
|
128
|
+
if (fieldDef.dynamicDefault && fieldDef.dynamicDefault.length > 0) {
|
|
129
|
+
for (const rule of fieldDef.dynamicDefault) {
|
|
130
|
+
if (evaluateConditionGroup(rule.when, data, externalVars, externalEnums, skippedSteps)) {
|
|
131
|
+
resolvedDefaultValue = rule.value;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
definition: fieldDef,
|
|
138
|
+
visible: isVisible(fieldDef.visibleWhen, data, externalVars, externalEnums, skippedSteps),
|
|
139
|
+
dependenciesMet: areDependenciesMet(fieldDef.dependsOn, data, externalVars),
|
|
140
|
+
resolvedOptions,
|
|
141
|
+
resolvedDefaultValue
|
|
142
|
+
};
|
|
143
|
+
});
|
|
117
144
|
const resolvedStep = {
|
|
118
145
|
definition: stepDef,
|
|
119
146
|
visible: stepVisible,
|
|
@@ -178,7 +205,7 @@ function getResolvedTree(state) {
|
|
|
178
205
|
if (!state.schema) {
|
|
179
206
|
return { steps: [], hiddenSteps: [], missingExternalVars: [] };
|
|
180
207
|
}
|
|
181
|
-
return resolveTree(state.schema, state.data, state.externalVars);
|
|
208
|
+
return resolveTree(state.schema, state.data, state.externalVars, void 0, state.skippedSteps);
|
|
182
209
|
}
|
|
183
210
|
function getCurrentStep(state) {
|
|
184
211
|
if (!state.currentStepId) return void 0;
|
|
@@ -210,6 +237,7 @@ var initialState = {
|
|
|
210
237
|
externalVars: {},
|
|
211
238
|
currentStepId: null,
|
|
212
239
|
history: [],
|
|
240
|
+
skippedSteps: [],
|
|
213
241
|
isSubmitting: false,
|
|
214
242
|
completed: false
|
|
215
243
|
};
|
|
@@ -225,6 +253,7 @@ function buildStateCreator() {
|
|
|
225
253
|
externalVars,
|
|
226
254
|
currentStepId: firstStepId,
|
|
227
255
|
history: firstStepId ? [firstStepId] : [],
|
|
256
|
+
skippedSteps: [],
|
|
228
257
|
isSubmitting: false,
|
|
229
258
|
completed: false
|
|
230
259
|
});
|
|
@@ -266,6 +295,16 @@ function buildStateCreator() {
|
|
|
266
295
|
setCompleted(b) {
|
|
267
296
|
set({ completed: b });
|
|
268
297
|
},
|
|
298
|
+
skipStep(stepId) {
|
|
299
|
+
set((state) => ({
|
|
300
|
+
skippedSteps: state.skippedSteps.includes(stepId) ? state.skippedSteps : [...state.skippedSteps, stepId]
|
|
301
|
+
}));
|
|
302
|
+
},
|
|
303
|
+
unskipStep(stepId) {
|
|
304
|
+
set((state) => ({
|
|
305
|
+
skippedSteps: state.skippedSteps.filter((id) => id !== stepId)
|
|
306
|
+
}));
|
|
307
|
+
},
|
|
269
308
|
truncateHistoryAt(stepId) {
|
|
270
309
|
set((state) => {
|
|
271
310
|
const idx = state.history.indexOf(stepId);
|
|
@@ -304,6 +343,7 @@ function createRuntimeStore(options = {}) {
|
|
|
304
343
|
data: state.data,
|
|
305
344
|
currentStepId: state.currentStepId,
|
|
306
345
|
history: state.history,
|
|
346
|
+
skippedSteps: state.skippedSteps,
|
|
307
347
|
completed: state.completed
|
|
308
348
|
})
|
|
309
349
|
})
|
|
@@ -320,7 +360,13 @@ var customValidatorRegistry = {};
|
|
|
320
360
|
function registerCustomValidator(id, fn) {
|
|
321
361
|
customValidatorRegistry[id] = fn;
|
|
322
362
|
}
|
|
323
|
-
function
|
|
363
|
+
function resolveRuleValue(rule, data) {
|
|
364
|
+
if (rule.refField && data) {
|
|
365
|
+
return resolveFieldValue(rule.refField, data, {});
|
|
366
|
+
}
|
|
367
|
+
return rule.value;
|
|
368
|
+
}
|
|
369
|
+
function buildFieldSchema(field, externalEnums, data) {
|
|
324
370
|
const rules = field.validation ?? [];
|
|
325
371
|
const isRequired = rules.some((r) => r.type === "required");
|
|
326
372
|
const isNumeric = field.type === "number";
|
|
@@ -338,16 +384,47 @@ function buildFieldSchema(field) {
|
|
|
338
384
|
let numSchema = z.coerce.number({
|
|
339
385
|
invalid_type_error: "Must be a number"
|
|
340
386
|
});
|
|
387
|
+
const numRefineRules = [];
|
|
341
388
|
for (const rule of rules) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
389
|
+
const rv = resolveRuleValue(rule, data);
|
|
390
|
+
const isRef = !!rule.refField;
|
|
391
|
+
if (rule.type === "min" || rule.type === "greaterThanOrEqual") {
|
|
392
|
+
const n = Number(rv);
|
|
393
|
+
if (!isNaN(n)) {
|
|
394
|
+
if (isRef) numRefineRules.push({ fn: (v) => v >= n, message: rule.message });
|
|
395
|
+
else numSchema = numSchema.gte(n, rule.message);
|
|
396
|
+
}
|
|
397
|
+
} else if (rule.type === "max" || rule.type === "lessThanOrEqual") {
|
|
398
|
+
const n = Number(rv);
|
|
399
|
+
if (!isNaN(n)) {
|
|
400
|
+
if (isRef) numRefineRules.push({ fn: (v) => v <= n, message: rule.message });
|
|
401
|
+
else numSchema = numSchema.lte(n, rule.message);
|
|
402
|
+
}
|
|
403
|
+
} else if (rule.type === "greaterThan") {
|
|
404
|
+
const n = Number(rv);
|
|
405
|
+
if (!isNaN(n)) {
|
|
406
|
+
if (isRef) numRefineRules.push({ fn: (v) => v > n, message: rule.message });
|
|
407
|
+
else numSchema = numSchema.gt(n, rule.message);
|
|
408
|
+
}
|
|
409
|
+
} else if (rule.type === "lessThan") {
|
|
410
|
+
const n = Number(rv);
|
|
411
|
+
if (!isNaN(n)) {
|
|
412
|
+
if (isRef) numRefineRules.push({ fn: (v) => v < n, message: rule.message });
|
|
413
|
+
else numSchema = numSchema.lt(n, rule.message);
|
|
414
|
+
}
|
|
415
|
+
} else if (rule.type === "equals") {
|
|
416
|
+
const n = Number(rv);
|
|
417
|
+
if (!isNaN(n)) numRefineRules.push({ fn: (v) => v === n, message: rule.message });
|
|
418
|
+
} else if (rule.type === "notEquals") {
|
|
419
|
+
const n = Number(rv);
|
|
420
|
+
if (!isNaN(n)) numRefineRules.push({ fn: (v) => v !== n, message: rule.message });
|
|
348
421
|
}
|
|
349
422
|
}
|
|
350
|
-
|
|
423
|
+
let numFinal = isRequired ? numSchema : numSchema.optional();
|
|
424
|
+
for (const { fn, message } of numRefineRules) {
|
|
425
|
+
numFinal = numFinal.refine((v) => v == null || fn(v), message);
|
|
426
|
+
}
|
|
427
|
+
return numFinal;
|
|
351
428
|
}
|
|
352
429
|
let strSchema = z.string();
|
|
353
430
|
const refineRules = [];
|
|
@@ -377,27 +454,112 @@ function buildFieldSchema(field) {
|
|
|
377
454
|
strSchema = strSchema.regex(new RegExp(String(rule.value)), rule.message);
|
|
378
455
|
}
|
|
379
456
|
break;
|
|
457
|
+
case "equals": {
|
|
458
|
+
const rv = resolveRuleValue(rule, data);
|
|
459
|
+
if (rv !== void 0) {
|
|
460
|
+
const eq = String(rv);
|
|
461
|
+
refineRules.push({ fn: (v) => String(v) === eq, message: rule.message });
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case "notEquals": {
|
|
466
|
+
const rv = resolveRuleValue(rule, data);
|
|
467
|
+
if (rv !== void 0) {
|
|
468
|
+
const neq = String(rv);
|
|
469
|
+
refineRules.push({ fn: (v) => String(v) !== neq, message: rule.message });
|
|
470
|
+
}
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
case "greaterThan": {
|
|
474
|
+
const rv = resolveRuleValue(rule, data);
|
|
475
|
+
if (rv !== void 0) {
|
|
476
|
+
const gt = Number(rv);
|
|
477
|
+
refineRules.push({ fn: (v) => Number(v) > gt, message: rule.message });
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
case "greaterThanOrEqual": {
|
|
482
|
+
const rv = resolveRuleValue(rule, data);
|
|
483
|
+
if (rv !== void 0) {
|
|
484
|
+
const gte = Number(rv);
|
|
485
|
+
refineRules.push({ fn: (v) => Number(v) >= gte, message: rule.message });
|
|
486
|
+
}
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case "lessThan": {
|
|
490
|
+
const rv = resolveRuleValue(rule, data);
|
|
491
|
+
if (rv !== void 0) {
|
|
492
|
+
const lt = Number(rv);
|
|
493
|
+
refineRules.push({ fn: (v) => Number(v) < lt, message: rule.message });
|
|
494
|
+
}
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case "lessThanOrEqual": {
|
|
498
|
+
const rv = resolveRuleValue(rule, data);
|
|
499
|
+
if (rv !== void 0) {
|
|
500
|
+
const lte = Number(rv);
|
|
501
|
+
refineRules.push({ fn: (v) => Number(v) <= lte, message: rule.message });
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
case "contains": {
|
|
506
|
+
const rv = resolveRuleValue(rule, data);
|
|
507
|
+
if (rv !== void 0) {
|
|
508
|
+
const sub = String(rv);
|
|
509
|
+
refineRules.push({ fn: (v) => String(v).includes(sub), message: rule.message });
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case "notContains": {
|
|
514
|
+
const rv = resolveRuleValue(rule, data);
|
|
515
|
+
if (rv !== void 0) {
|
|
516
|
+
const nsub = String(rv);
|
|
517
|
+
refineRules.push({ fn: (v) => !String(v).includes(nsub), message: rule.message });
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case "matches":
|
|
522
|
+
if (rule.value !== void 0 && rule.value !== null) {
|
|
523
|
+
const rx = new RegExp(String(rule.value));
|
|
524
|
+
refineRules.push({ fn: (v) => rx.test(String(v)), message: rule.message });
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
527
|
+
case "inEnum":
|
|
528
|
+
if (rule.value && externalEnums) {
|
|
529
|
+
const enumDef = externalEnums.find((e) => e.id === String(rule.value));
|
|
530
|
+
if (enumDef) {
|
|
531
|
+
const values = enumDef.values.map((v) => String(v.value));
|
|
532
|
+
refineRules.push({ fn: (v) => values.includes(String(v)), message: rule.message });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
break;
|
|
536
|
+
case "notInEnum":
|
|
537
|
+
if (rule.value && externalEnums) {
|
|
538
|
+
const enumDef = externalEnums.find((e) => e.id === String(rule.value));
|
|
539
|
+
if (enumDef) {
|
|
540
|
+
const values = enumDef.values.map((v) => String(v.value));
|
|
541
|
+
refineRules.push({ fn: (v) => !values.includes(String(v)), message: rule.message });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
break;
|
|
380
545
|
case "custom":
|
|
381
546
|
if (rule.customValidatorId && customValidatorRegistry[rule.customValidatorId]) {
|
|
382
|
-
refineRules.push({
|
|
547
|
+
refineRules.push({ fn: (v) => Boolean(customValidatorRegistry[rule.customValidatorId]?.(v)), message: rule.message });
|
|
383
548
|
}
|
|
384
549
|
break;
|
|
385
550
|
}
|
|
386
551
|
}
|
|
387
552
|
let finalSchema = isRequired ? strSchema : strSchema.optional();
|
|
388
|
-
for (const {
|
|
389
|
-
finalSchema = finalSchema.refine(
|
|
390
|
-
(val) => Boolean(customValidatorRegistry[id]?.(val)),
|
|
391
|
-
message
|
|
392
|
-
);
|
|
553
|
+
for (const { fn, message } of refineRules) {
|
|
554
|
+
finalSchema = finalSchema.refine(fn, message);
|
|
393
555
|
}
|
|
394
556
|
return finalSchema;
|
|
395
557
|
}
|
|
396
|
-
function buildZodSchema(fields) {
|
|
558
|
+
function buildZodSchema(fields, externalEnums, data) {
|
|
397
559
|
const shape = {};
|
|
398
560
|
for (const resolvedField of fields) {
|
|
399
561
|
if (!resolvedField.visible) continue;
|
|
400
|
-
shape[resolvedField.definition.id] = buildFieldSchema(resolvedField.definition);
|
|
562
|
+
shape[resolvedField.definition.id] = buildFieldSchema(resolvedField.definition, externalEnums, data);
|
|
401
563
|
}
|
|
402
564
|
return z.object(shape);
|
|
403
565
|
}
|