@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.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
- /** Options for select/multiselect/radio fields */
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" → journey data
132
- * - "$ext.varId" → external variable
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
- definition: fieldDef,
114
- visible: isVisible(fieldDef.visibleWhen, data, externalVars),
115
- dependenciesMet: areDependenciesMet(fieldDef.dependsOn, data, externalVars)
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 buildFieldSchema(field) {
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
- 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);
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
- return isRequired ? numSchema : numSchema.optional();
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({ id: rule.customValidatorId, message: rule.message });
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 { id, message } of refineRules) {
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
  }