@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.
@@ -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 {};