@finsys/core 1.0.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 +202 -0
- package/README.md +220 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/rhf-generator.d.ts +30 -0
- package/dist/rhf-generator.js +252 -0
- package/dist/rhf-generator.test.d.ts +1 -0
- package/dist/rhf-generator.test.js +568 -0
- package/dist/schema/schema/unified-form.schema.json +266 -0
- package/dist/schema/unified-form.schema.json +266 -0
- package/dist/survey-generator.d.ts +153 -0
- package/dist/survey-generator.js +185 -0
- package/dist/survey-generator.test.d.ts +1 -0
- package/dist/survey-generator.test.js +317 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +41 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +116 -0
- package/dist/validator.d.ts +14 -0
- package/dist/validator.js +34 -0
- package/dist/validator.test.d.ts +1 -0
- package/dist/validator.test.js +127 -0
- package/package.json +73 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolvePageFields, groupFieldsByCategory, applyDynamicTitles } from "./survey-generator.js";
|
|
3
|
+
import { evaluateExpression } from "./utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* Apply visibility-aware validation to a Zod schema
|
|
6
|
+
*/
|
|
7
|
+
function applyVisibilityValidation(baseObject, fields) {
|
|
8
|
+
return baseObject.superRefine((data, ctx) => {
|
|
9
|
+
fields.forEach((field) => {
|
|
10
|
+
const isRequired = field.isRequired !== false && field.required !== false && field.type !== "html";
|
|
11
|
+
if (!isRequired)
|
|
12
|
+
return;
|
|
13
|
+
// Determine Visibility
|
|
14
|
+
let isVisible = field.visible !== false;
|
|
15
|
+
if (isVisible && field.visibleIf) {
|
|
16
|
+
isVisible = evaluateExpression(field.visibleIf, data);
|
|
17
|
+
}
|
|
18
|
+
if (isVisible) {
|
|
19
|
+
const value = data[field.name];
|
|
20
|
+
const isEmpty = value === undefined ||
|
|
21
|
+
value === null ||
|
|
22
|
+
value === "" ||
|
|
23
|
+
(Array.isArray(value) && value.length === 0);
|
|
24
|
+
if (isEmpty) {
|
|
25
|
+
ctx.addIssue({
|
|
26
|
+
code: z.ZodIssueCode.custom,
|
|
27
|
+
message: "This field is required",
|
|
28
|
+
path: [field.name],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function generateRHFSchema(config) {
|
|
36
|
+
// ... (existing logic for collecting fields/building shape)
|
|
37
|
+
const pages = config.pages || [];
|
|
38
|
+
const categories = config.categories || [];
|
|
39
|
+
const fieldsMap = config.fields || {};
|
|
40
|
+
if (pages.length === 0) {
|
|
41
|
+
throw new Error("v2.0.0 templates must include a 'pages' configuration.");
|
|
42
|
+
}
|
|
43
|
+
// 1. Collect all fields from all pages
|
|
44
|
+
const allFields = [];
|
|
45
|
+
for (const page of pages) {
|
|
46
|
+
const pageFields = resolvePageFields(page, fieldsMap);
|
|
47
|
+
allFields.push(...pageFields);
|
|
48
|
+
}
|
|
49
|
+
// Apply dynamic titles
|
|
50
|
+
const mergedFields = allFields.map(applyDynamicTitles);
|
|
51
|
+
// 2. Build Zod Shape & Default Values
|
|
52
|
+
const shape = {};
|
|
53
|
+
const defaultValues = {};
|
|
54
|
+
mergedFields.forEach((field) => {
|
|
55
|
+
let schema = z.string();
|
|
56
|
+
// Track base type before validators/wrapping transform it via .refine()
|
|
57
|
+
// (instanceof checks won't work after .refine() converts ZodString → ZodEffects)
|
|
58
|
+
let baseType = "string";
|
|
59
|
+
// --- Base Type Handling ---
|
|
60
|
+
if (field.inputType === "number" || field.type === "number" || field.type === "slider" || field.type === "range") {
|
|
61
|
+
schema = z.coerce.number();
|
|
62
|
+
baseType = "number";
|
|
63
|
+
defaultValues[field.name] = field.defaultValue !== undefined ? Number(field.defaultValue) : "";
|
|
64
|
+
}
|
|
65
|
+
else if (field.type === "checkbox") {
|
|
66
|
+
schema = z.array(z.string());
|
|
67
|
+
baseType = "array";
|
|
68
|
+
defaultValues[field.name] = [];
|
|
69
|
+
}
|
|
70
|
+
else if (field.type === "file") {
|
|
71
|
+
schema = z.array(z.any());
|
|
72
|
+
baseType = "array";
|
|
73
|
+
defaultValues[field.name] = [];
|
|
74
|
+
}
|
|
75
|
+
else if (field.type === "boolean") {
|
|
76
|
+
schema = z.boolean();
|
|
77
|
+
baseType = "boolean";
|
|
78
|
+
defaultValues[field.name] = field.defaultValue ?? false;
|
|
79
|
+
}
|
|
80
|
+
else if (field.type === "html") {
|
|
81
|
+
schema = z.any().optional();
|
|
82
|
+
baseType = "any";
|
|
83
|
+
defaultValues[field.name] = "";
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (Array.isArray(field.defaultValue)) {
|
|
87
|
+
defaultValues[field.name] = field.defaultValue[0] || "";
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
defaultValues[field.name] = field.defaultValue ?? "";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// --- Validation Constraints ---
|
|
94
|
+
// Min/Max for Numbers
|
|
95
|
+
if (baseType === "number" && schema instanceof z.ZodNumber) {
|
|
96
|
+
let numSchema = schema;
|
|
97
|
+
if (field.min !== undefined)
|
|
98
|
+
numSchema = numSchema.min(Number(field.min), { message: `Minimum value is ${field.min}` });
|
|
99
|
+
if (field.max !== undefined)
|
|
100
|
+
numSchema = numSchema.max(Number(field.max), { message: `Maximum value is ${field.max}` });
|
|
101
|
+
schema = numSchema;
|
|
102
|
+
}
|
|
103
|
+
const isRequired = field.isRequired !== false && field.required !== false && field.type !== "html";
|
|
104
|
+
const isAlwaysVisible = field.visible !== false && !field.visibleIf;
|
|
105
|
+
const isActuallyInput = field.type !== "html";
|
|
106
|
+
// --- Required .min(1) for always-visible fields ---
|
|
107
|
+
// Must be applied BEFORE validators (.refine) which change the schema type.
|
|
108
|
+
if (isRequired && isAlwaysVisible && isActuallyInput) {
|
|
109
|
+
if (baseType === "string" && schema instanceof z.ZodString) {
|
|
110
|
+
schema = schema.min(1, { message: "This field is required" });
|
|
111
|
+
}
|
|
112
|
+
else if (baseType === "array" && schema instanceof z.ZodArray) {
|
|
113
|
+
schema = schema.min(1, { message: "Please select at least one option" });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// --- Enhancements: Validators ---
|
|
117
|
+
// Applied BEFORE optional wrapping so instanceof checks still work on the
|
|
118
|
+
// base type. The .refine() calls here will transform the schema to ZodEffects,
|
|
119
|
+
// which is why we use baseType for subsequent decisions instead of instanceof.
|
|
120
|
+
// Email
|
|
121
|
+
const hasExplicitEmailValidator = field.validators?.some(v => v.type === "email");
|
|
122
|
+
if ((field.inputType === "email" || field.name === "email") && baseType === "string" && schema instanceof z.ZodString && !hasExplicitEmailValidator) {
|
|
123
|
+
schema = schema.refine(val => val === "" || z.string().email().safeParse(val).success, {
|
|
124
|
+
message: "Invalid email address"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// Legacy Validators (Regex / Numeric)
|
|
128
|
+
if (field.validators) {
|
|
129
|
+
field.validators.forEach((v) => {
|
|
130
|
+
if (v.type === "regex" && v.regex) {
|
|
131
|
+
try {
|
|
132
|
+
const regex = new RegExp(v.regex);
|
|
133
|
+
if (baseType === "string") {
|
|
134
|
+
schema = schema.refine((val) => val === "" || regex.test(val), {
|
|
135
|
+
message: v.text || "Invalid format"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
console.warn(`Invalid regex pattern for field ${field.name}:`, v.regex);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (v.type === "email" && baseType === "string") {
|
|
144
|
+
schema = schema.refine((val) => val === "" || z.string().email().safeParse(val).success, {
|
|
145
|
+
message: v.text || "Invalid email"
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (v.type === "numeric" && schema instanceof z.ZodNumber) {
|
|
149
|
+
let numSchema = schema;
|
|
150
|
+
if (v.minValue !== undefined)
|
|
151
|
+
numSchema = numSchema.min(v.minValue, { message: v.text });
|
|
152
|
+
if (v.maxValue !== undefined)
|
|
153
|
+
numSchema = numSchema.max(v.maxValue, { message: v.text });
|
|
154
|
+
schema = numSchema;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// --- Final Wrapper: Optional for conditional/hidden fields ---
|
|
159
|
+
// Uses baseType instead of instanceof since validators may have transformed the schema.
|
|
160
|
+
if (!(isRequired && isAlwaysVisible && isActuallyInput)) {
|
|
161
|
+
if (baseType === "number") {
|
|
162
|
+
schema = schema.optional().or(z.literal("")).or(z.null());
|
|
163
|
+
}
|
|
164
|
+
else if (baseType === "array") {
|
|
165
|
+
schema = schema.optional();
|
|
166
|
+
}
|
|
167
|
+
else if (baseType === "boolean") {
|
|
168
|
+
schema = schema.optional();
|
|
169
|
+
}
|
|
170
|
+
else if (baseType === "string") {
|
|
171
|
+
schema = schema.optional().or(z.literal(""));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
shape[field.name] = schema;
|
|
175
|
+
});
|
|
176
|
+
// 3. Group Fields by Category
|
|
177
|
+
const groupedFields = groupFieldsByCategory(mergedFields, categories);
|
|
178
|
+
// 4. Build Steps (Dynamic Wizard)
|
|
179
|
+
const steps = pages.map((page, idx) => {
|
|
180
|
+
const pageFields = resolvePageFields(page, fieldsMap);
|
|
181
|
+
return {
|
|
182
|
+
id: idx,
|
|
183
|
+
title: page.title || "",
|
|
184
|
+
description: page.description,
|
|
185
|
+
fields: pageFields,
|
|
186
|
+
showCategoryHeadings: page.showCategoryHeadings ?? false,
|
|
187
|
+
layout: page.layout
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
// 5. Build Final Schema with Visibility-Aware Validation
|
|
191
|
+
const baseObject = z.object(shape);
|
|
192
|
+
const zodSchema = applyVisibilityValidation(baseObject, mergedFields);
|
|
193
|
+
return {
|
|
194
|
+
zodSchema,
|
|
195
|
+
baseSchema: baseObject,
|
|
196
|
+
defaultValues,
|
|
197
|
+
fields: mergedFields,
|
|
198
|
+
groupedFields,
|
|
199
|
+
steps,
|
|
200
|
+
displayName: config.displayName || "Application Form",
|
|
201
|
+
categories,
|
|
202
|
+
templateIcon: config.templateIcon
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get Zod schema for a specific step
|
|
207
|
+
*/
|
|
208
|
+
export function getStepSchema(step, fullSchema) {
|
|
209
|
+
const stepFieldNames = step.fields.map(f => f.name);
|
|
210
|
+
const stepShape = {};
|
|
211
|
+
// 1. Determine the base ZodObject
|
|
212
|
+
let baseObj;
|
|
213
|
+
if (fullSchema instanceof z.ZodObject) {
|
|
214
|
+
baseObj = fullSchema;
|
|
215
|
+
}
|
|
216
|
+
else if (fullSchema && typeof fullSchema === "object" && "baseSchema" in fullSchema) {
|
|
217
|
+
baseObj = fullSchema.baseSchema;
|
|
218
|
+
}
|
|
219
|
+
else if (fullSchema && "_def" in fullSchema && fullSchema._def.schema instanceof z.ZodObject) {
|
|
220
|
+
// Basic Unwrap for ZodEffects
|
|
221
|
+
baseObj = fullSchema._def.schema;
|
|
222
|
+
}
|
|
223
|
+
if (!baseObj) {
|
|
224
|
+
console.warn("[RHF Generator] Could not extract ZodObject shape for step validation");
|
|
225
|
+
return z.object({});
|
|
226
|
+
}
|
|
227
|
+
const shape = baseObj.shape;
|
|
228
|
+
const stepFields = [];
|
|
229
|
+
stepFieldNames.forEach(name => {
|
|
230
|
+
if (name in shape) {
|
|
231
|
+
stepShape[name] = shape[name];
|
|
232
|
+
// Keep track of which fields are in this step
|
|
233
|
+
const fieldDef = step.fields.find(f => f.name === name);
|
|
234
|
+
if (fieldDef)
|
|
235
|
+
stepFields.push(fieldDef);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
const stepBaseObject = z.object(stepShape);
|
|
239
|
+
return applyVisibilityValidation(stepBaseObject, stepFields);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Extract default values for a specific step
|
|
243
|
+
*/
|
|
244
|
+
export function getStepDefaultValues(step, allDefaults) {
|
|
245
|
+
const stepDefaults = {};
|
|
246
|
+
step.fields.forEach(field => {
|
|
247
|
+
if (field.name in allDefaults) {
|
|
248
|
+
stepDefaults[field.name] = allDefaults[field.name];
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
return stepDefaults;
|
|
252
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|