@svadmin/lite 0.3.27 → 0.6.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.
Files changed (43) hide show
  1. package/README.md +106 -1
  2. package/dist/compatibility.d.ts +23 -0
  3. package/dist/compatibility.js +95 -0
  4. package/dist/components/LiteForm.svelte +1 -1
  5. package/dist/components/LiteForm.svelte.d.ts +1 -1
  6. package/dist/components/LiteShowField.svelte +32 -22
  7. package/dist/components/LiteShowField.svelte.d.ts +3 -2
  8. package/dist/components/LiteTable.svelte +90 -37
  9. package/dist/components/LiteTable.svelte.d.ts +3 -2
  10. package/dist/components/compatibility/LiteCapabilityBoundary.svelte +51 -0
  11. package/dist/components/compatibility/LiteCapabilityBoundary.svelte.d.ts +13 -0
  12. package/dist/components/compatibility/LiteClipboardFallback.svelte +16 -0
  13. package/dist/components/compatibility/LiteClipboardFallback.svelte.d.ts +8 -0
  14. package/dist/components/compatibility/LiteComputeFallback.svelte +50 -0
  15. package/dist/components/compatibility/LiteComputeFallback.svelte.d.ts +14 -0
  16. package/dist/components/compatibility/LiteDirectoryUpload.svelte +44 -0
  17. package/dist/components/compatibility/LiteDirectoryUpload.svelte.d.ts +11 -0
  18. package/dist/components/compatibility/LiteOrderedList.svelte +47 -0
  19. package/dist/components/compatibility/LiteOrderedList.svelte.d.ts +14 -0
  20. package/dist/components/compatibility/LiteRealtimeStatus.svelte +43 -0
  21. package/dist/components/compatibility/LiteRealtimeStatus.svelte.d.ts +11 -0
  22. package/dist/components/compatibility/LiteVisualFallback.svelte +80 -0
  23. package/dist/components/compatibility/LiteVisualFallback.svelte.d.ts +18 -0
  24. package/dist/components/compatibility/index.d.ts +7 -0
  25. package/dist/components/compatibility/index.js +7 -0
  26. package/dist/components/pages/LiteCreatePage.svelte +15 -6
  27. package/dist/components/pages/LiteCreatePage.svelte.d.ts +1 -1
  28. package/dist/components/pages/LiteEditPage.svelte +18 -9
  29. package/dist/components/pages/LiteEditPage.svelte.d.ts +1 -1
  30. package/dist/components/pages/LiteListPage.svelte +86 -16
  31. package/dist/components/pages/LiteListPage.svelte.d.ts +4 -2
  32. package/dist/components/pages/LiteShowPage.svelte +19 -10
  33. package/dist/components/pages/LiteShowPage.svelte.d.ts +1 -1
  34. package/dist/index.d.ts +7 -3
  35. package/dist/index.js +8 -3
  36. package/dist/lite.css +269 -0
  37. package/dist/schema-generator.d.ts +49 -13
  38. package/dist/schema-generator.js +381 -199
  39. package/dist/server-adapter.d.ts +25 -1
  40. package/dist/server-adapter.js +132 -33
  41. package/dist/value-normalization.d.ts +1 -0
  42. package/dist/value-normalization.js +18 -0
  43. package/package.json +10 -10
@@ -1,19 +1,19 @@
1
1
  /**
2
2
  * @svadmin/lite — Schema Generator
3
3
  *
4
- * Auto-generates Zod schemas from @svadmin/core FieldDefinitions.
5
- * Used by the Lite server actions and compatible with other Zod consumers.
4
+ * Auto-generates TypeBox schemas from @svadmin/core FieldDefinitions.
5
+ * Used by Lite server actions and high-speed JIT form validation.
6
6
  */
7
- import { z } from 'zod';
8
- import { parseExplicitBoolean } from './value-normalization';
7
+ import { Type } from "@sinclair/typebox";
8
+ import { parseExplicitBoolean } from "./value-normalization";
9
9
  function isNativeFile(value) {
10
- return typeof File !== 'undefined' && value instanceof File;
10
+ return typeof File !== "undefined" && value instanceof File;
11
11
  }
12
12
  function isNonEmptyNativeFile(value) {
13
- return isNativeFile(value) && value.size > 0 && value.name !== '';
13
+ return isNativeFile(value) && value.size > 0 && value.name !== "";
14
14
  }
15
15
  function normalizeSingleUpload(submittedUpload) {
16
- if (submittedUpload === undefined || submittedUpload === null || submittedUpload === '')
16
+ if (submittedUpload === undefined || submittedUpload === null || submittedUpload === "")
17
17
  return undefined;
18
18
  if (isNativeFile(submittedUpload) && !isNonEmptyNativeFile(submittedUpload))
19
19
  return undefined;
@@ -22,7 +22,7 @@ function normalizeSingleUpload(submittedUpload) {
22
22
  function hasRequiredValue(value) {
23
23
  if (value === undefined || value === null)
24
24
  return false;
25
- if (typeof value === 'string')
25
+ if (typeof value === "string")
26
26
  return value.trim().length > 0;
27
27
  if (Array.isArray(value))
28
28
  return value.length > 0;
@@ -30,246 +30,428 @@ function hasRequiredValue(value) {
30
30
  return isNonEmptyNativeFile(value);
31
31
  return true;
32
32
  }
33
- function applyCustomValidation(schema, field) {
34
- if (!field.validate)
35
- return schema;
36
- return schema.superRefine((parsedFieldValue, context) => {
37
- if (parsedFieldValue === undefined || parsedFieldValue === null || parsedFieldValue === '')
38
- return;
39
- const message = field.validate?.(parsedFieldValue);
40
- if (message)
41
- context.addIssue({ code: 'custom', message });
42
- });
43
- }
44
- function numberFieldToZod(field) {
45
- const numberSchema = z.coerce.number({ message: `${field.label} must be a number` });
46
- const targetSchema = field.required ? numberSchema : numberSchema.optional();
47
- return z.preprocess((value) => {
48
- if (value === undefined || value === null)
49
- return undefined;
50
- if (typeof value === 'string' && value.trim() === '')
51
- return undefined;
52
- return value;
53
- }, targetSchema);
54
- }
55
- function booleanFieldToZod(field) {
56
- const booleanSchema = z.boolean();
57
- const targetSchema = field.required ? booleanSchema : booleanSchema.optional();
58
- return z.preprocess((rawBoolean) => {
59
- if (rawBoolean === undefined || rawBoolean === null || rawBoolean === '')
60
- return undefined;
61
- return parseExplicitBoolean(rawBoolean) ?? rawBoolean;
62
- }, targetSchema);
63
- }
64
33
  function restoreOptionValue(field, submittedOption) {
65
34
  const option = field.options?.find((candidate) => String(candidate.value) === String(submittedOption));
66
35
  return option?.value ?? submittedOption;
67
36
  }
68
- function optionValueSchema(field) {
69
- const options = field.options ?? [];
70
- return z.union([z.string(), z.number()]).refine((parsedOption) => options.length === 0
71
- || options.some((option) => Object.is(option.value, parsedOption)), { message: `${field.label} must be one of the options` });
72
- }
73
- function singleOptionFieldToZod(field) {
74
- return z.preprocess((submittedOption) => restoreOptionValue(field, submittedOption), optionValueSchema(field));
75
- }
76
- function multipleOptionFieldToZod(field) {
77
- return z.preprocess((submittedOptions) => Array.isArray(submittedOptions)
78
- ? submittedOptions.map((submittedOption) => restoreOptionValue(field, submittedOption))
79
- : submittedOptions, z.array(optionValueSchema(field)).default([]));
80
- }
81
- function singleFileFieldToZod(field, options) {
82
- const fileSchema = z.custom(isNonEmptyNativeFile, {
83
- message: `${field.label} must be a non-empty file`,
84
- });
85
- const referenceSchema = z.string().trim().min(1, `${field.label} must reference an existing file`);
86
- const uploadSchema = options.allowReference ? z.union([fileSchema, referenceSchema]) : fileSchema;
87
- const targetSchema = options.required ? uploadSchema : uploadSchema.optional();
88
- return z.preprocess(normalizeSingleUpload, targetSchema);
89
- }
90
- function imageFieldSchemas(field) {
91
- const imageSchema = z.union([
92
- z.string().trim().min(1, `${field.label} must reference an image`),
93
- z.custom(isNonEmptyNativeFile, {
94
- message: `${field.label} must reference an image or contain a non-empty file`,
95
- }),
96
- ]);
97
- return {
98
- required: z.preprocess(normalizeSingleUpload, imageSchema),
99
- optional: z.preprocess(normalizeSingleUpload, imageSchema.optional()),
100
- };
101
- }
102
37
  function normalizeImageEntries(submittedImages) {
103
- if (submittedImages === undefined || submittedImages === null || submittedImages === '')
38
+ if (submittedImages === undefined || submittedImages === null || submittedImages === "")
104
39
  return undefined;
105
40
  const entries = Array.isArray(submittedImages) ? submittedImages : [submittedImages];
106
41
  const normalized = entries.flatMap((entry) => {
107
- if (typeof entry !== 'string')
42
+ if (typeof entry !== "string")
108
43
  return [entry];
109
44
  return entry.split(/[\r\n]+/u).map((reference) => reference.trim()).filter(Boolean);
110
45
  }).filter((entry) => !isNativeFile(entry) || isNonEmptyNativeFile(entry));
111
46
  return normalized.length > 0 ? normalized : undefined;
112
47
  }
113
- function imagesFieldSchemas(field) {
114
- const imageEntrySchema = z.union([
115
- z.string().trim().min(1, `${field.label} contains an empty image reference`),
116
- z.custom(isNonEmptyNativeFile, {
117
- message: `${field.label} contains an empty or invalid file`,
118
- }),
119
- ]);
120
- const requiredImages = z.array(imageEntrySchema)
121
- .min(1, `${field.label} must contain at least one image`);
122
- return {
123
- required: z.preprocess(normalizeImageEntries, requiredImages),
124
- optional: z.preprocess(normalizeImageEntries, z.array(imageEntrySchema).optional()),
125
- };
126
- }
127
- /**
128
- * Convert a single FieldDefinition to its corresponding Zod type.
129
- */
130
- function fieldToZod(field, mode, withinArray = false) {
131
- let schema;
48
+ function coerceAndValidateField(field, value, mode, withinArray = false, path = [field.key]) {
49
+ const issues = [];
50
+ let coerced = value;
132
51
  switch (field.type) {
133
- case 'number':
134
- return numberFieldToZod(field);
135
- case 'boolean':
136
- return booleanFieldToZod(field);
137
- case 'email':
138
- schema = z.string().email(`${field.label} must be a valid email`);
52
+ case "number": {
53
+ if (coerced === undefined || coerced === null || coerced === "") {
54
+ coerced = undefined;
55
+ if (field.required) {
56
+ issues.push({ path, message: `${field.label} is required` });
57
+ }
58
+ }
59
+ else if (typeof coerced === "string") {
60
+ const trimmed = coerced.trim();
61
+ if (trimmed === "") {
62
+ coerced = undefined;
63
+ if (field.required) {
64
+ issues.push({ path, message: `${field.label} is required` });
65
+ }
66
+ }
67
+ else {
68
+ const num = Number(trimmed);
69
+ if (Number.isNaN(num)) {
70
+ issues.push({ path, message: `${field.label} must be a number` });
71
+ }
72
+ else {
73
+ coerced = num;
74
+ }
75
+ }
76
+ }
77
+ else if (typeof coerced === "number") {
78
+ if (Number.isNaN(coerced)) {
79
+ issues.push({ path, message: `${field.label} must be a number` });
80
+ }
81
+ }
82
+ else {
83
+ issues.push({ path, message: `${field.label} must be a number` });
84
+ }
139
85
  break;
140
- case 'url':
141
- schema = z.string().url(`${field.label} must be a valid URL`);
86
+ }
87
+ case "boolean": {
88
+ if (coerced === undefined || coerced === null || coerced === "") {
89
+ coerced = field.required ? false : undefined;
90
+ if (field.required && coerced === undefined) {
91
+ issues.push({ path, message: `${field.label} is required` });
92
+ }
93
+ }
94
+ else if (typeof coerced === "boolean") {
95
+ // ok
96
+ }
97
+ else if (typeof coerced === "string") {
98
+ const parsed = parseExplicitBoolean(coerced);
99
+ if (parsed === undefined) {
100
+ issues.push({ path, message: `${field.label} must be a boolean` });
101
+ }
102
+ else {
103
+ coerced = parsed;
104
+ }
105
+ }
106
+ else if (typeof coerced === "number") {
107
+ if (coerced === 1)
108
+ coerced = true;
109
+ else if (coerced === 0)
110
+ coerced = false;
111
+ else
112
+ issues.push({ path, message: `${field.label} must be a boolean` });
113
+ }
114
+ else {
115
+ issues.push({ path, message: `${field.label} must be a boolean` });
116
+ }
142
117
  break;
143
- case 'date':
144
- schema = z.string().refine((v) => !v || !isNaN(Date.parse(v)), { message: `${field.label} must be a valid date` });
118
+ }
119
+ case "email": {
120
+ if (typeof coerced === "string" && coerced.length > 0) {
121
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
122
+ if (!emailRegex.test(coerced)) {
123
+ issues.push({ path, message: `${field.label} must be a valid email` });
124
+ }
125
+ }
145
126
  break;
146
- case 'select':
147
- schema = singleOptionFieldToZod(field);
127
+ }
128
+ case "url": {
129
+ if (typeof coerced === "string" && coerced.length > 0) {
130
+ try {
131
+ new URL(coerced);
132
+ }
133
+ catch {
134
+ issues.push({ path, message: `${field.label} must be a valid URL` });
135
+ }
136
+ }
148
137
  break;
149
- case 'multiselect':
150
- schema = multipleOptionFieldToZod(field);
138
+ }
139
+ case "date": {
140
+ if (typeof coerced === "string" && coerced.length > 0) {
141
+ if (Number.isNaN(Date.parse(coerced))) {
142
+ issues.push({ path, message: `${field.label} must be a valid date` });
143
+ }
144
+ }
151
145
  break;
152
- case 'relation':
153
- schema = field.options?.length ? singleOptionFieldToZod(field) : z.string();
146
+ }
147
+ case "select": {
148
+ if (coerced !== undefined && coerced !== null && coerced !== "") {
149
+ coerced = restoreOptionValue(field, coerced);
150
+ const options = field.options ?? [];
151
+ if (options.length > 0 && !options.some((opt) => Object.is(opt.value, coerced))) {
152
+ issues.push({ path, message: `${field.label} must be one of the options` });
153
+ }
154
+ }
154
155
  break;
155
- case 'array': {
156
- const shape = {};
157
- for (const subField of field.subFields ?? []) {
158
- shape[subField.key] = applyCustomValidation(fieldToZod(subField, mode, true), subField);
156
+ }
157
+ case "multiselect": {
158
+ if (Array.isArray(coerced)) {
159
+ coerced = coerced.map((item) => restoreOptionValue(field, item));
160
+ const options = field.options ?? [];
161
+ if (options.length > 0) {
162
+ for (const item of coerced) {
163
+ if (!options.some((opt) => Object.is(opt.value, item))) {
164
+ issues.push({ path, message: `${field.label} must be one of the options` });
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ else if (coerced === undefined || coerced === null || coerced === "") {
171
+ coerced = [];
159
172
  }
160
- const arraySchema = z.array(z.object(shape));
161
- return field.required
162
- ? arraySchema.min(1, `${field.label} must contain at least one item`)
163
- : arraySchema.optional().or(z.literal(''));
173
+ break;
164
174
  }
165
- case 'tags':
166
- schema = z.union([
167
- z.array(z.string()),
168
- z.string().transform((value) => value ? value.split(',').map((tag) => tag.trim()).filter(Boolean) : []),
169
- ]);
175
+ case "relation": {
176
+ if (field.options?.length && coerced !== undefined && coerced !== null && coerced !== "") {
177
+ coerced = restoreOptionValue(field, coerced);
178
+ if (!field.options.some((opt) => Object.is(opt.value, coerced))) {
179
+ issues.push({ path, message: `${field.label} must be one of the options` });
180
+ }
181
+ }
170
182
  break;
171
- case 'textarea':
172
- case 'richtext':
173
- case 'markdown':
174
- schema = z.string().max(50000, `${field.label} is too long`);
183
+ }
184
+ case "array": {
185
+ if (Array.isArray(coerced)) {
186
+ if (field.required && coerced.length === 0) {
187
+ issues.push({ path, message: `${field.label} must contain at least one item` });
188
+ }
189
+ else {
190
+ const processedRows = [];
191
+ for (let r = 0; r < coerced.length; r++) {
192
+ const row = coerced[r];
193
+ if (typeof row !== "object" || row === null) {
194
+ issues.push({ path: [...path, r], message: "Row must be an object" });
195
+ continue;
196
+ }
197
+ const rowObj = row;
198
+ const processedRow = {};
199
+ for (const subField of field.subFields ?? []) {
200
+ const subRes = coerceAndValidateField(subField, rowObj[subField.key], mode, true, [...path, r, subField.key]);
201
+ issues.push(...subRes.issues);
202
+ if (subRes.value !== undefined) {
203
+ processedRow[subField.key] = subRes.value;
204
+ }
205
+ }
206
+ processedRows.push(processedRow);
207
+ }
208
+ coerced = processedRows;
209
+ }
210
+ }
211
+ else if (coerced === undefined || coerced === null || coerced === "") {
212
+ if (field.required) {
213
+ issues.push({ path, message: `${field.label} must contain at least one item` });
214
+ }
215
+ else {
216
+ coerced = [];
217
+ }
218
+ }
219
+ else {
220
+ issues.push({ path, message: `${field.label} must be an array` });
221
+ }
222
+ break;
223
+ }
224
+ case "tags": {
225
+ if (typeof coerced === "string") {
226
+ coerced = coerced ? coerced.split(",").map((tag) => tag.trim()).filter(Boolean) : [];
227
+ }
228
+ else if (!Array.isArray(coerced)) {
229
+ coerced = [];
230
+ }
231
+ break;
232
+ }
233
+ case "textarea":
234
+ case "richtext":
235
+ case "markdown": {
236
+ if (typeof coerced === "string" && coerced.length > 50000) {
237
+ issues.push({ path, message: `${field.label} is too long` });
238
+ }
175
239
  break;
176
- case 'json':
177
- schema = z.unknown().transform((value, context) => {
178
- if (typeof value !== 'string')
179
- return value;
240
+ }
241
+ case "json": {
242
+ if (typeof coerced === "string") {
180
243
  try {
181
- return JSON.parse(value);
244
+ coerced = JSON.parse(coerced);
182
245
  }
183
246
  catch {
184
- context.addIssue({ code: 'custom', message: `${field.label} must be valid JSON` });
185
- return z.NEVER;
247
+ issues.push({ path, message: `${field.label} must be valid JSON` });
248
+ }
249
+ }
250
+ break;
251
+ }
252
+ case "phone": {
253
+ if (typeof coerced === "string" && coerced.length > 0) {
254
+ if (!/^[+\d\s()-]*$/.test(coerced)) {
255
+ issues.push({ path, message: `${field.label} must be a valid phone number` });
256
+ }
257
+ }
258
+ break;
259
+ }
260
+ case "file": {
261
+ coerced = normalizeSingleUpload(coerced);
262
+ const isFile = isNonEmptyNativeFile(coerced);
263
+ const isStringRef = typeof coerced === "string" && coerced.trim().length > 0;
264
+ const isRequired = field.required === true && (mode === "create" || withinArray);
265
+ const allowRef = withinArray && mode === "edit";
266
+ if (isRequired) {
267
+ if (!isFile && (!allowRef || !isStringRef)) {
268
+ issues.push({ path, message: `${field.label} must be a non-empty file` });
269
+ }
270
+ }
271
+ else if (coerced !== undefined) {
272
+ if (!isFile && (!allowRef || !isStringRef)) {
273
+ issues.push({ path, message: `${field.label} must be a non-empty file` });
186
274
  }
187
- });
275
+ }
188
276
  break;
189
- case 'phone':
190
- schema = z.string().regex(/^[+\d\s()-]*$/, `${field.label} must be a valid phone number`);
277
+ }
278
+ case "image": {
279
+ coerced = normalizeSingleUpload(coerced);
280
+ const isFile = isNonEmptyNativeFile(coerced);
281
+ const isStringRef = typeof coerced === "string" && coerced.trim().length > 0;
282
+ const isRequired = field.required === true;
283
+ if (isRequired) {
284
+ if (mode === "create" || withinArray) {
285
+ if (!isFile && !isStringRef) {
286
+ issues.push({ path, message: `${field.label} must reference an image or contain a non-empty file` });
287
+ }
288
+ }
289
+ else if (mode === "edit") {
290
+ if (coerced !== undefined && !isFile && !isStringRef) {
291
+ issues.push({ path, message: `${field.label} must reference an image or contain a non-empty file` });
292
+ }
293
+ }
294
+ }
191
295
  break;
192
- case 'file':
193
- return singleFileFieldToZod(field, {
194
- required: field.required === true && (mode === 'create' || withinArray),
195
- allowReference: mode === 'edit' && withinArray,
196
- });
197
- case 'image': {
198
- const imageSchemas = imageFieldSchemas(field);
199
- return field.required === true && (mode === 'create' || withinArray)
200
- ? imageSchemas.required
201
- : imageSchemas.optional;
202
296
  }
203
- case 'images': {
204
- const imagesSchemas = imagesFieldSchemas(field);
205
- return field.required === true && (mode === 'create' || withinArray)
206
- ? imagesSchemas.required
207
- : imagesSchemas.optional;
297
+ case "images": {
298
+ coerced = normalizeImageEntries(coerced);
299
+ const isRequired = field.required === true;
300
+ const entries = Array.isArray(coerced) ? coerced : (coerced ? [coerced] : []);
301
+ if (isRequired && (mode === "create" || withinArray)) {
302
+ if (entries.length === 0) {
303
+ issues.push({ path, message: `${field.label} must contain at least one image` });
304
+ }
305
+ }
306
+ break;
208
307
  }
209
308
  default:
210
- schema = z.string();
309
+ break;
211
310
  }
212
- // Enforce meaningful values for required fields, including nested array rows.
213
- if (field.required) {
214
- schema = schema.refine(hasRequiredValue, { message: `${field.label} is required` });
311
+ // Check required
312
+ if (field.type !== "number" && field.type !== "boolean" && field.type !== "array" && field.type !== "file" && field.type !== "image" && field.type !== "images") {
313
+ if (field.required && !hasRequiredValue(coerced)) {
314
+ issues.push({ path, message: `${field.label} is required` });
315
+ }
215
316
  }
216
- else {
217
- schema = schema.optional().or(z.literal(''));
317
+ // Custom validate
318
+ if (field.validate && issues.length === 0 && coerced !== undefined && coerced !== null && coerced !== "") {
319
+ const customMessage = field.validate(coerced);
320
+ if (customMessage) {
321
+ issues.push({ path, message: customMessage });
322
+ }
218
323
  }
219
- return schema;
324
+ return { value: coerced, issues };
220
325
  }
221
326
  /**
222
- * Generate a Zod object schema from a ResourceDefinition's fields.
223
- * Only includes fields that are relevant for form rendering.
224
- *
225
- * @param mode - 'create' | 'edit' to filter fields by showInCreate / showInEdit
327
+ * Generate a TypeBox object schema from a list of FieldDefinitions.
226
328
  */
227
- export function fieldsToZodSchema(fields, mode = 'create') {
329
+ export function fieldsToTypeBoxSchema(fields, mode = "create") {
228
330
  const shape = {};
331
+ const activeFields = [];
229
332
  for (const field of fields) {
230
- // Skip fields not shown in forms
231
333
  if (field.showInForm === false)
232
334
  continue;
233
- if (mode === 'create' && field.showInCreate === false)
335
+ if (mode === "create" && field.showInCreate === false)
234
336
  continue;
235
- if (mode === 'edit' && field.showInEdit === false)
337
+ if (mode === "edit" && field.showInEdit === false)
236
338
  continue;
237
- shape[field.key] = applyCustomValidation(fieldToZod(field, mode), field);
339
+ activeFields.push(field);
340
+ switch (field.type) {
341
+ case "number":
342
+ shape[field.key] = field.required ? Type.Number() : Type.Optional(Type.Number());
343
+ break;
344
+ case "boolean":
345
+ shape[field.key] = field.required ? Type.Boolean() : Type.Optional(Type.Boolean());
346
+ break;
347
+ case "array":
348
+ shape[field.key] = field.required ? Type.Array(Type.Any(), { minItems: 1 }) : Type.Optional(Type.Array(Type.Any()));
349
+ break;
350
+ default:
351
+ shape[field.key] = field.required ? Type.Any() : Type.Optional(Type.Any());
352
+ break;
353
+ }
238
354
  }
239
- return z.object(shape);
355
+ const baseSchema = Type.Object(shape, { additionalProperties: true });
356
+ const safeParse = (values) => {
357
+ if (typeof values !== "object" || values === null) {
358
+ return {
359
+ success: false,
360
+ error: { issues: [{ path: ["_root"], message: "Values must be an object" }] },
361
+ };
362
+ }
363
+ const input = values;
364
+ const resultData = {};
365
+ const allIssues = [];
366
+ for (const field of activeFields) {
367
+ const val = input[field.key];
368
+ const fieldRes = coerceAndValidateField(field, val, mode, false, [field.key]);
369
+ allIssues.push(...fieldRes.issues);
370
+ if (fieldRes.value !== undefined) {
371
+ resultData[field.key] = fieldRes.value;
372
+ }
373
+ }
374
+ if (allIssues.length > 0) {
375
+ return {
376
+ success: false,
377
+ error: { issues: allIssues },
378
+ };
379
+ }
380
+ return {
381
+ success: true,
382
+ data: resultData,
383
+ };
384
+ };
385
+ const parse = (values) => {
386
+ const res = safeParse(values);
387
+ if (!res.success) {
388
+ const err = new Error(res.error?.issues[0]?.message || "Validation failed");
389
+ err.issues = res.error?.issues ?? [];
390
+ throw err;
391
+ }
392
+ return res.data ?? {};
393
+ };
394
+ return Object.assign(baseSchema, {
395
+ parse,
396
+ safeParse,
397
+ Check: (val) => safeParse(val).success,
398
+ "~standard": {
399
+ version: 1,
400
+ vendor: "svadmin",
401
+ validate: (val) => {
402
+ const res = safeParse(val);
403
+ if (res.success) {
404
+ return { value: res.data ?? {} };
405
+ }
406
+ return {
407
+ issues: (res.error?.issues ?? []).map((iss) => ({
408
+ message: iss.message,
409
+ path: iss.path,
410
+ })),
411
+ };
412
+ },
413
+ },
414
+ });
240
415
  }
241
416
  /**
242
- * Generate a Zod schema from a ResourceDefinition.
243
- * Convenience wrapper around fieldsToZodSchema.
417
+ * Generate a TypeBox schema from a ResourceDefinition.
418
+ * Convenience wrapper around fieldsToTypeBoxSchema.
244
419
  */
245
- export function resourceToZodSchema(resource, mode = 'create') {
246
- const primaryKey = resource.primaryKey ?? 'id';
247
- return fieldsToZodSchema(resource.fields.filter((field) => field.key !== primaryKey), mode);
420
+ export function resourceToTypeBoxSchema(resource, mode = "create") {
421
+ const primaryKey = resource.primaryKey ?? "id";
422
+ return fieldsToTypeBoxSchema(resource.fields.filter((field) => field.key !== primaryKey), mode);
248
423
  }
424
+ /**
425
+ * Backward compatibility alias for fieldsToTypeBoxSchema
426
+ */
427
+ export const fieldsToZodSchema = fieldsToTypeBoxSchema;
428
+ /**
429
+ * Backward compatibility alias for resourceToTypeBoxSchema
430
+ */
431
+ export const resourceToZodSchema = resourceToTypeBoxSchema;
249
432
  /**
250
433
  * Determine a conservative HTML input type for server-rendered forms.
251
- * Text fallbacks avoid inconsistent native validation and date widgets.
252
434
  */
253
435
  export function fieldToInputType(field) {
254
436
  switch (field.type) {
255
- case 'number': return 'text';
256
- case 'email': return 'text';
257
- case 'url': return 'text';
258
- case 'phone': return 'tel';
259
- case 'boolean': return 'checkbox';
260
- case 'date': return 'text';
261
- case 'textarea':
262
- case 'richtext':
263
- case 'markdown':
264
- case 'images': return 'textarea';
265
- case 'json': return 'textarea';
266
- case 'select':
267
- case 'multiselect': return 'select';
268
- case 'relation': return field.options?.length ? 'select' : 'text';
269
- case 'array': return 'text';
270
- case 'file': return 'file';
271
- case 'password': return 'password';
272
- default: return 'text';
437
+ case "number": return "text";
438
+ case "email": return "text";
439
+ case "url": return "text";
440
+ case "phone": return "tel";
441
+ case "boolean": return "checkbox";
442
+ case "date": return "text";
443
+ case "textarea":
444
+ case "richtext":
445
+ case "markdown":
446
+ case "images": return "textarea";
447
+ case "json": return "textarea";
448
+ case "select":
449
+ case "multiselect": return "select";
450
+ case "relation": return field.options?.length ? "select" : "text";
451
+ case "array": return "text";
452
+ case "file": return "file";
453
+ case "password": return "password";
454
+ default: return "text";
273
455
  }
274
456
  }
275
457
  /**
@@ -277,11 +459,11 @@ export function fieldToInputType(field) {
277
459
  */
278
460
  export function fieldToPlaceholder(field) {
279
461
  switch (field.type) {
280
- case 'date': return 'YYYY-MM-DD';
281
- case 'email': return 'user@example.com';
282
- case 'url': return 'https://example.com';
283
- case 'phone': return '+1 (555) 000-0000';
284
- case 'number': return '0';
285
- default: return '';
462
+ case "date": return "YYYY-MM-DD";
463
+ case "email": return "user@example.com";
464
+ case "url": return "https://example.com";
465
+ case "phone": return "+1 (555) 000-0000";
466
+ case "number": return "0";
467
+ default: return "";
286
468
  }
287
469
  }