@temelyos/zod 0.0.0-stage → 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 +15 -0
- package/dist/index.d.ts +501 -0
- package/dist/index.js +993 -0
- package/package.json +46 -3
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
import { ZodArray, ZodCatch, ZodDefault, ZodLazy, ZodLiteral, ZodNonOptional, ZodNullable, ZodOptional, ZodPrefault, ZodReadonly, z } from "zod";
|
|
2
|
+
import { getLogger } from "@temelyos/core/logging/store";
|
|
3
|
+
import { isValidFormat } from "@temelyos/core/format";
|
|
4
|
+
import { getOrSet } from "@temelyos/core/cache";
|
|
5
|
+
import { isMidnight } from "@temelyos/core/date";
|
|
6
|
+
import { isLooseJson } from "@temelyos/core/json";
|
|
7
|
+
import { isObject, isPlainObject, mapValues } from "@temelyos/core/object";
|
|
8
|
+
import { isLanguageCode } from "@temelyos/core/i18n";
|
|
9
|
+
import { isInteger } from "@temelyos/core/number";
|
|
10
|
+
import { isValidPattern } from "@temelyos/core/reg-exp";
|
|
11
|
+
import { SlugPattern } from "@temelyos/core/string";
|
|
12
|
+
import { isSupportedTimezone } from "@temelyos/core/time-zone";
|
|
13
|
+
// src/bounded-number.ts
|
|
14
|
+
/** A number, optionally whole and bounded; a refusal names its bounds as the message's `{0}` and `{1}`. */
|
|
15
|
+
function boundedNumber({ integer = false, max, min }) {
|
|
16
|
+
return z.number({ error: "Enter a number." }).superRefine((value, ctx) => {
|
|
17
|
+
if (integer && !Number.isInteger(value)) ctx.addIssue({
|
|
18
|
+
code: "custom",
|
|
19
|
+
message: "Enter a whole number."
|
|
20
|
+
});
|
|
21
|
+
else if (min !== void 0 && max !== void 0) {
|
|
22
|
+
if (value < min || value > max) ctx.addIssue({
|
|
23
|
+
code: "custom",
|
|
24
|
+
message: "Enter a number from {0} to {1}.",
|
|
25
|
+
params: {
|
|
26
|
+
0: min,
|
|
27
|
+
1: max
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
} else if (min !== void 0) {
|
|
31
|
+
if (value < min) ctx.addIssue({
|
|
32
|
+
code: "custom",
|
|
33
|
+
message: "Enter a number of {0} or more.",
|
|
34
|
+
params: { 0: min }
|
|
35
|
+
});
|
|
36
|
+
} else if (max !== void 0 && value > max) ctx.addIssue({
|
|
37
|
+
code: "custom",
|
|
38
|
+
message: "Enter a number of {0} or less.",
|
|
39
|
+
params: { 0: max }
|
|
40
|
+
});
|
|
41
|
+
}).meta({
|
|
42
|
+
...integer && { type: "integer" },
|
|
43
|
+
...max !== void 0 && { maximum: max },
|
|
44
|
+
...min !== void 0 && { minimum: min }
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// src/codec-parts.ts
|
|
48
|
+
/**
|
|
49
|
+
* The schemas a codec built at run time encodes and decodes by, kept in a Zod registry beside it.
|
|
50
|
+
* Typed `unknown[]`: the registry maps schema-typed metadata through `$replace`, which recurses
|
|
51
|
+
* without end on `ZodType`.
|
|
52
|
+
*/
|
|
53
|
+
const codecPartsRegistry = z.registry();
|
|
54
|
+
/** The schemas {@link withCodecParts} named for a codec; undefined for any other schema. */
|
|
55
|
+
function codecParts(schema) {
|
|
56
|
+
return codecPartsRegistry.get(schema)?.parts;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A codec that builds its schemas at run time (from a model's fields, or a result's columns),
|
|
60
|
+
* with those schemas named, so a reader can check what it encodes and decodes its values by.
|
|
61
|
+
*/
|
|
62
|
+
function withCodecParts(codec, schemas) {
|
|
63
|
+
codecPartsRegistry.add(codec, { parts: schemas });
|
|
64
|
+
return codec;
|
|
65
|
+
}
|
|
66
|
+
// src/currency-code.ts
|
|
67
|
+
/** A lower-case ISO 4217 code: `usd`, never `USD` or `$`. */
|
|
68
|
+
const CurrencyCodePattern = /^[a-z]{3}$/;
|
|
69
|
+
/** What a value that is not a currency code says, unless the caller says otherwise. An `error` property so the i18n extractor keeps the key. */
|
|
70
|
+
const invalidCurrency = { error: "Invalid currency code." };
|
|
71
|
+
/** A field that takes a lower-case ISO 4217 code. `missing` is the caller's, for a field nobody filled in. */
|
|
72
|
+
function currencyCode(missing, invalid = invalidCurrency.error) {
|
|
73
|
+
return z.string().min(1, missing).regex(CurrencyCodePattern, invalid);
|
|
74
|
+
}
|
|
75
|
+
// src/utilities.ts
|
|
76
|
+
const ZOD_WRAPPER_TYPES = [
|
|
77
|
+
"catch",
|
|
78
|
+
"default",
|
|
79
|
+
"lazy",
|
|
80
|
+
"nonoptional",
|
|
81
|
+
"nullable",
|
|
82
|
+
"optional",
|
|
83
|
+
"promise",
|
|
84
|
+
"success"
|
|
85
|
+
];
|
|
86
|
+
function getInnerType(schema) {
|
|
87
|
+
if (schema instanceof ZodOptional || schema instanceof ZodNullable || schema instanceof ZodDefault || schema instanceof ZodCatch) return getInnerType(schema.def.innerType);
|
|
88
|
+
if (schema instanceof ZodLazy) return getInnerType(schema.def.getter());
|
|
89
|
+
if (schema instanceof ZodArray) return getInnerType(schema.def.element);
|
|
90
|
+
return schema;
|
|
91
|
+
}
|
|
92
|
+
function isFieldNullable(schema) {
|
|
93
|
+
let current = schema;
|
|
94
|
+
while (isZodOptional(current) || isZodNullable(current)) {
|
|
95
|
+
if (isZodNullable(current)) return true;
|
|
96
|
+
current = current.def.innerType;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Whether `schema` is declared `.optional()`, looking only through `.optional()` and `.nullable()`: what a form or a
|
|
102
|
+
* write may leave empty. For whether a parsed value may lack the field, through any wrapper, use {@link isOptionalOutput}.
|
|
103
|
+
*/
|
|
104
|
+
function isFieldOptional(schema) {
|
|
105
|
+
let current = schema;
|
|
106
|
+
while (isZodOptional(current) || isZodNullable(current)) {
|
|
107
|
+
if (isZodOptional(current)) return true;
|
|
108
|
+
current = current.def.innerType;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether a value typed by what `schema` outputs may lack the field: optional through any wrapper (lazy, readonly, a
|
|
114
|
+
* union with an optional member), as zod decides it, not filled by a default; and `any` or `unknown`, whose type takes
|
|
115
|
+
* `undefined` already. What a read typed by the schema may lack; for a field declared `.optional()`, {@link isFieldOptional}.
|
|
116
|
+
*/
|
|
117
|
+
function isOptionalOutput(schema) {
|
|
118
|
+
return schema._zod.optout === "optional" || schema.def.type === "any" || schema.def.type === "unknown";
|
|
119
|
+
}
|
|
120
|
+
function isZodAny(obj) {
|
|
121
|
+
return isZodType(obj) && obj.def.type === "any";
|
|
122
|
+
}
|
|
123
|
+
function isZodArray(obj) {
|
|
124
|
+
return isZodType(obj) && obj.def.type === "array";
|
|
125
|
+
}
|
|
126
|
+
function isZodCatch(obj) {
|
|
127
|
+
return isZodType(obj) && obj.def.type === "catch";
|
|
128
|
+
}
|
|
129
|
+
/** A codec, as `z.codec` builds: a pipe that can also run backwards. Read it as its `out`, what it decodes to. */
|
|
130
|
+
function isZodCodec(obj) {
|
|
131
|
+
return isZodPipe(obj) && "reverseTransform" in obj.def;
|
|
132
|
+
}
|
|
133
|
+
function isZodDate(obj) {
|
|
134
|
+
return isZodType(obj) && obj.def.type === "date";
|
|
135
|
+
}
|
|
136
|
+
function isZodDefault(obj) {
|
|
137
|
+
return isZodType(obj) && obj.def.type === "default";
|
|
138
|
+
}
|
|
139
|
+
function isZodEnum(obj) {
|
|
140
|
+
return isZodType(obj) && obj.def.type === "enum";
|
|
141
|
+
}
|
|
142
|
+
function isZodIntersection(obj) {
|
|
143
|
+
return isZodType(obj) && obj.def.type === "intersection";
|
|
144
|
+
}
|
|
145
|
+
function isZodLazy(obj) {
|
|
146
|
+
return isZodType(obj) && obj.def.type === "lazy";
|
|
147
|
+
}
|
|
148
|
+
function isZodNullable(obj) {
|
|
149
|
+
return isZodType(obj) && obj.def.type === "nullable";
|
|
150
|
+
}
|
|
151
|
+
function isZodNumber(obj) {
|
|
152
|
+
return isZodType(obj) && obj.def.type === "number";
|
|
153
|
+
}
|
|
154
|
+
function isZodObject(obj, isLoose) {
|
|
155
|
+
if (!isZodType(obj) || obj.def.type !== "object") return false;
|
|
156
|
+
if (typeof isLoose === "undefined") return true;
|
|
157
|
+
return isLoose === isZodObjectLoose(obj);
|
|
158
|
+
}
|
|
159
|
+
function isZodObjectLoose(obj) {
|
|
160
|
+
return !!isZodUnknown(obj.def.catchall);
|
|
161
|
+
}
|
|
162
|
+
function isZodOptional(obj) {
|
|
163
|
+
return isZodType(obj) && obj.def.type === "optional";
|
|
164
|
+
}
|
|
165
|
+
function isZodPipe(obj) {
|
|
166
|
+
return isZodType(obj) && obj.def.type === "pipe";
|
|
167
|
+
}
|
|
168
|
+
function isZodRecord(obj) {
|
|
169
|
+
return isZodType(obj) && obj.def.type === "record";
|
|
170
|
+
}
|
|
171
|
+
function isZodString(obj) {
|
|
172
|
+
return isZodType(obj) && obj.def.type === "string";
|
|
173
|
+
}
|
|
174
|
+
function isZodTransform(obj) {
|
|
175
|
+
return isZodType(obj) && obj.def.type === "transform";
|
|
176
|
+
}
|
|
177
|
+
function isZodType(obj) {
|
|
178
|
+
return typeof obj === "object" && obj !== null && "def" in obj && "safeParse" in obj && typeof obj.safeParse === "function";
|
|
179
|
+
}
|
|
180
|
+
function isZodUnion(obj) {
|
|
181
|
+
return isZodType(obj) && obj.def.type === "union";
|
|
182
|
+
}
|
|
183
|
+
function isZodUnknown(obj) {
|
|
184
|
+
return isZodType(obj) && obj.def.type === "unknown";
|
|
185
|
+
}
|
|
186
|
+
function isZodWrapper(obj) {
|
|
187
|
+
return isZodType(obj) && ZOD_WRAPPER_TYPES.includes(obj.def.type);
|
|
188
|
+
}
|
|
189
|
+
// src/field-paths.ts
|
|
190
|
+
function collectFieldPaths(node, prefix, out, shouldSkipArrays, seen) {
|
|
191
|
+
if (seen.has(node)) return;
|
|
192
|
+
seen.add(node);
|
|
193
|
+
if (isZodObject(node)) {
|
|
194
|
+
const shape = node.shape;
|
|
195
|
+
for (const key of Object.keys(shape)) {
|
|
196
|
+
const declared = shape[key];
|
|
197
|
+
const child = unwrapField(declared, shouldSkipArrays);
|
|
198
|
+
const path = [...prefix, key];
|
|
199
|
+
out.push({
|
|
200
|
+
declared,
|
|
201
|
+
path: path.join("."),
|
|
202
|
+
schema: child
|
|
203
|
+
});
|
|
204
|
+
collectFieldPaths(child, path, out, shouldSkipArrays, seen);
|
|
205
|
+
}
|
|
206
|
+
} else if (isZodUnion(node)) for (const option of node.def.options) collectFieldPaths(unwrapField(option, shouldSkipArrays), prefix, out, shouldSkipArrays, seen);
|
|
207
|
+
seen.delete(node);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Enumerates the dotted paths of every field reachable by descending `ZodObject`
|
|
211
|
+
* shapes and the branches of `ZodUnion` / `ZodDiscriminatedUnion` (Zod v4 reports
|
|
212
|
+
* both as `def.type === 'union'`). A field declared in more than one union branch
|
|
213
|
+
* is reported once per branch — callers that need uniqueness should dedupe by
|
|
214
|
+
* `path`.
|
|
215
|
+
*
|
|
216
|
+
* Wrapper types (optional/nullable/default/catch/nonoptional/prefault/readonly/lazy) are always unwrapped.
|
|
217
|
+
* Arrays are descended via their element type by default (emitting non-indexed
|
|
218
|
+
* paths like `items.token`, the way `getPathValues` fans out at runtime); pass
|
|
219
|
+
* `{ skipArrays: true }` to stop at arrays instead. Records and other leaves are
|
|
220
|
+
* never descended. Each field is reported with its unwrapped `schema`, which lets
|
|
221
|
+
* callers read per-field metadata (e.g. `sensitive()` markers) without re-walking
|
|
222
|
+
* wrappers, and as its object `declared` it, wrappers kept, which says whether it
|
|
223
|
+
* may be absent (`isOptionalOutput`).
|
|
224
|
+
*
|
|
225
|
+
* The schema-space analogue of `getPathValues` (which extracts values at a path
|
|
226
|
+
* from a runtime object).
|
|
227
|
+
*/
|
|
228
|
+
function getZodFieldPaths(schema, options = {}) {
|
|
229
|
+
const shouldSkipArrays = options.skipArrays ?? false;
|
|
230
|
+
const out = [];
|
|
231
|
+
collectFieldPaths(unwrapField(schema, shouldSkipArrays), [], out, shouldSkipArrays, /* @__PURE__ */ new Set());
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Unwraps optional/nullable/default/catch/nonoptional/prefault/readonly/lazy wrappers. Unlike `getInnerType`,
|
|
236
|
+
* arrays are unwrapped to their element type only when `shouldSkipArrays` is
|
|
237
|
+
* false; when true it stops at arrays, leaving them terminal.
|
|
238
|
+
*
|
|
239
|
+
* A wrapper chain that closes on itself (`const Self = z.lazy(() => Self.array())`
|
|
240
|
+
* unwraps lazy → array → lazy → …) stops at the node that repeats — it has no
|
|
241
|
+
* inner type left to reach. The guard has to live here: `collectFieldPaths`'s own
|
|
242
|
+
* one is only consulted after unwrapping has already returned. `seen` is per
|
|
243
|
+
* top-level call, so it never suppresses an unrelated sibling.
|
|
244
|
+
*/
|
|
245
|
+
function unwrapField(schema, shouldSkipArrays, seen = /* @__PURE__ */ new Set()) {
|
|
246
|
+
if (seen.has(schema)) return schema;
|
|
247
|
+
seen.add(schema);
|
|
248
|
+
if (schema instanceof ZodOptional || schema instanceof ZodNullable || schema instanceof ZodDefault || schema instanceof ZodCatch || schema instanceof ZodNonOptional || schema instanceof ZodPrefault || schema instanceof ZodReadonly) return unwrapField(schema.def.innerType, shouldSkipArrays, seen);
|
|
249
|
+
if (schema instanceof ZodLazy) return unwrapField(schema.def.getter(), shouldSkipArrays, seen);
|
|
250
|
+
if (!shouldSkipArrays && schema instanceof ZodArray) return unwrapField(schema.def.element, shouldSkipArrays, seen);
|
|
251
|
+
return schema;
|
|
252
|
+
}
|
|
253
|
+
// src/field-settings.ts
|
|
254
|
+
const buildFieldSettingsSchema = (defaultType) => z.object({
|
|
255
|
+
default: defaultType.optional(),
|
|
256
|
+
isOptional: z.boolean().optional(),
|
|
257
|
+
isVisible: z.boolean().optional(),
|
|
258
|
+
label: z.string().optional()
|
|
259
|
+
});
|
|
260
|
+
const stringFieldSettingsSchema = buildFieldSettingsSchema(z.string()).extend({
|
|
261
|
+
maxLength: z.union([z.number(), z.object({
|
|
262
|
+
message: z.string(),
|
|
263
|
+
value: z.number()
|
|
264
|
+
})]).optional(),
|
|
265
|
+
minLength: z.union([z.number(), z.object({
|
|
266
|
+
message: z.string(),
|
|
267
|
+
value: z.number()
|
|
268
|
+
})]).optional(),
|
|
269
|
+
regExp: z.union([z.instanceof(RegExp), z.object({
|
|
270
|
+
message: z.string(),
|
|
271
|
+
value: z.instanceof(RegExp)
|
|
272
|
+
})]).optional()
|
|
273
|
+
});
|
|
274
|
+
const numberFieldSettingsSchema = buildFieldSettingsSchema(z.number()).extend({
|
|
275
|
+
max: z.union([z.number(), z.object({
|
|
276
|
+
message: z.string(),
|
|
277
|
+
value: z.number()
|
|
278
|
+
})]).optional(),
|
|
279
|
+
min: z.union([z.number(), z.object({
|
|
280
|
+
message: z.string(),
|
|
281
|
+
value: z.number()
|
|
282
|
+
})]).optional()
|
|
283
|
+
});
|
|
284
|
+
const fileFieldSettingsSchema = buildFieldSettingsSchema(z.instanceof(File)).extend({
|
|
285
|
+
maxBytes: z.union([z.number(), z.object({
|
|
286
|
+
message: z.string(),
|
|
287
|
+
value: z.number()
|
|
288
|
+
})]).optional(),
|
|
289
|
+
minBytes: z.union([z.number(), z.object({
|
|
290
|
+
message: z.string(),
|
|
291
|
+
value: z.number()
|
|
292
|
+
})]).optional()
|
|
293
|
+
});
|
|
294
|
+
const dropdownFieldSettingsSchema = buildFieldSettingsSchema(z.string()).extend({ values: z.union([z.array(z.string()), z.record(z.string(), z.union([z.string(), z.number()]))]).optional() });
|
|
295
|
+
const AnyFieldSettingsSchema = z.union([
|
|
296
|
+
stringFieldSettingsSchema,
|
|
297
|
+
numberFieldSettingsSchema,
|
|
298
|
+
fileFieldSettingsSchema,
|
|
299
|
+
dropdownFieldSettingsSchema
|
|
300
|
+
]);
|
|
301
|
+
function applyFieldSettings(schema, fields) {
|
|
302
|
+
if (!fields) return schema;
|
|
303
|
+
const updatedShape = Object.assign({}, schema.shape);
|
|
304
|
+
for (const fieldName in fields) {
|
|
305
|
+
const fieldSettings = fields[fieldName];
|
|
306
|
+
if (!(fieldName in schema.shape)) continue;
|
|
307
|
+
const fieldSchema = schema.shape[fieldName];
|
|
308
|
+
if (fieldSettings.isVisible === false) {
|
|
309
|
+
delete updatedShape[fieldName];
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const [oldField, wrappers] = getField(fieldSchema);
|
|
313
|
+
let newField = void 0;
|
|
314
|
+
if (isStringFieldSettings(fieldSettings)) {
|
|
315
|
+
if (!isZodString(oldField)) {
|
|
316
|
+
getLogger().warn(`Field ${fieldName} is not a string, but received StringFieldSettings.`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
newField = createStringField(oldField, fieldSettings);
|
|
320
|
+
}
|
|
321
|
+
if (isNumberFieldSettings(fieldSettings)) {
|
|
322
|
+
if (!isZodNumber(oldField)) {
|
|
323
|
+
getLogger().warn(`Field ${fieldName} is not a number, but received NumberFieldSettings.`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
newField = createNumberField(oldField, fieldSettings);
|
|
327
|
+
}
|
|
328
|
+
if (isDropdownFieldSettings(fieldSettings)) {
|
|
329
|
+
if (Array.isArray(fieldSettings.values)) {
|
|
330
|
+
if (!isZodEnum(oldField)) {
|
|
331
|
+
getLogger().warn(`Field ${fieldName} is not a enum, but received DropdownFieldSettings with an Array.`);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
} else if (!isZodEnum(oldField)) {
|
|
335
|
+
getLogger().warn(`Field ${fieldName} is not a native enum, but received DropdownFieldSettings.`);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
newField = createDropdownField(fieldSettings);
|
|
339
|
+
}
|
|
340
|
+
if (!newField) continue;
|
|
341
|
+
newField = applyWrappers(newField, wrappers, fieldSettings);
|
|
342
|
+
updatedShape[fieldName] = newField;
|
|
343
|
+
}
|
|
344
|
+
return z.object(updatedShape);
|
|
345
|
+
}
|
|
346
|
+
function applyWrappers(base, wrappers, settings = {}) {
|
|
347
|
+
let result = base;
|
|
348
|
+
let isOptionalApplied = false;
|
|
349
|
+
let isDefaultApplied = false;
|
|
350
|
+
for (let i = 0; i < wrappers.length; i++) {
|
|
351
|
+
const wrapper = wrappers[i];
|
|
352
|
+
if (isZodDefault(wrapper)) {
|
|
353
|
+
if (settings.default !== void 0) {
|
|
354
|
+
result = result.default(settings.default);
|
|
355
|
+
isDefaultApplied = true;
|
|
356
|
+
} else result = result.default(wrapper.def.defaultValue);
|
|
357
|
+
} else if (isZodOptional(wrapper) && settings.isOptional !== false) {
|
|
358
|
+
result = result.optional();
|
|
359
|
+
isOptionalApplied = true;
|
|
360
|
+
} else if (isZodNullable(wrapper)) result = result.nullable();
|
|
361
|
+
else if (isZodCatch(wrapper)) result = result.catch(wrapper.def.catchValue);
|
|
362
|
+
else if (isZodLazy(wrapper)) {
|
|
363
|
+
const inner = result;
|
|
364
|
+
result = z.lazy(() => inner);
|
|
365
|
+
} else if (isZodPipe(wrapper)) {
|
|
366
|
+
if (isZodTransform(wrapper.out)) result = result.transform(wrapper.out.def.transform);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (settings.isOptional && !isOptionalApplied) result = result.optional();
|
|
370
|
+
if (settings.default !== void 0 && !isDefaultApplied) result = result.default(settings.default);
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
function createDropdownField(settings) {
|
|
374
|
+
if (Array.isArray(settings.values)) return z.enum(settings.values);
|
|
375
|
+
return z.enum(settings.values ?? {});
|
|
376
|
+
}
|
|
377
|
+
function createNumberField(oldField, settings) {
|
|
378
|
+
let field = z.number();
|
|
379
|
+
if (settings.min !== void 0) {
|
|
380
|
+
if (typeof settings.min === "number") {
|
|
381
|
+
const check = getZodCheck(oldField, "greater_than");
|
|
382
|
+
let params;
|
|
383
|
+
if (check?._zod.def.error) params = { error: check._zod.def.error };
|
|
384
|
+
field = field.min(settings.min, params);
|
|
385
|
+
} else field = field.min(settings.min.value, settings.min.message);
|
|
386
|
+
}
|
|
387
|
+
if (settings.max !== void 0) {
|
|
388
|
+
if (typeof settings.max === "number") {
|
|
389
|
+
const check = getZodCheck(oldField, "less_than");
|
|
390
|
+
let params;
|
|
391
|
+
if (check?._zod.def.error) params = { error: check._zod.def.error };
|
|
392
|
+
field = field.max(settings.max, params);
|
|
393
|
+
} else field = field.max(settings.max.value, settings.max.message);
|
|
394
|
+
}
|
|
395
|
+
const carriedChecks = (oldField.def.checks ?? []).filter((check) => {
|
|
396
|
+
const kind = check._zod.def.check;
|
|
397
|
+
if (kind === "greater_than") return settings.min === void 0;
|
|
398
|
+
if (kind === "less_than") return settings.max === void 0;
|
|
399
|
+
return true;
|
|
400
|
+
});
|
|
401
|
+
return carriedChecks.length ? field.check(...carriedChecks) : field;
|
|
402
|
+
}
|
|
403
|
+
function createStringField(oldField, settings) {
|
|
404
|
+
let field = z.string();
|
|
405
|
+
if (settings.minLength !== void 0) {
|
|
406
|
+
if (typeof settings.minLength === "number") {
|
|
407
|
+
const check = getZodCheck(oldField, "min_length");
|
|
408
|
+
let params;
|
|
409
|
+
if (check?._zod.def.error) params = { error: check._zod.def.error };
|
|
410
|
+
field = field.min(settings.minLength, params);
|
|
411
|
+
} else field = field.min(settings.minLength.value, settings.minLength.message);
|
|
412
|
+
}
|
|
413
|
+
if (settings.maxLength !== void 0) {
|
|
414
|
+
if (typeof settings.maxLength === "number") {
|
|
415
|
+
const mlCheck = getZodCheck(oldField, "max_length");
|
|
416
|
+
let params;
|
|
417
|
+
if (mlCheck?._zod.def.error) params = { error: mlCheck._zod.def.error };
|
|
418
|
+
field = field.max(settings.maxLength, params);
|
|
419
|
+
} else field = field.max(settings.maxLength.value, settings.maxLength.message);
|
|
420
|
+
}
|
|
421
|
+
if (settings.regExp !== void 0) {
|
|
422
|
+
if (settings.regExp instanceof RegExp) {
|
|
423
|
+
const regCheck = getZodCheck(oldField, "string_format", "regex");
|
|
424
|
+
let params;
|
|
425
|
+
if (regCheck?._zod.def.error) params = { error: regCheck._zod.def.error };
|
|
426
|
+
field = field.regex(settings.regExp, params);
|
|
427
|
+
} else field = field.regex(settings.regExp.value, settings.regExp.message);
|
|
428
|
+
}
|
|
429
|
+
const carriedChecks = (oldField.def.checks ?? []).filter((check) => {
|
|
430
|
+
const kind = check._zod.def.check;
|
|
431
|
+
if (kind === "min_length") return settings.minLength === void 0;
|
|
432
|
+
if (kind === "max_length") return settings.maxLength === void 0;
|
|
433
|
+
if (kind === "string_format" && check._zod.def.format === "regex") return settings.regExp === void 0;
|
|
434
|
+
return true;
|
|
435
|
+
});
|
|
436
|
+
return carriedChecks.length ? field.check(...carriedChecks) : field;
|
|
437
|
+
}
|
|
438
|
+
function getField(field) {
|
|
439
|
+
const wrappers = [];
|
|
440
|
+
let type = field;
|
|
441
|
+
while (isZodWrapper(type) || isZodPipe(type) && !isZodCodec(type)) {
|
|
442
|
+
if (isZodPipe(type)) {
|
|
443
|
+
wrappers.push(type);
|
|
444
|
+
type = type.in;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
wrappers.push(type);
|
|
448
|
+
type = type.unwrap();
|
|
449
|
+
}
|
|
450
|
+
return [type, wrappers.reverse()];
|
|
451
|
+
}
|
|
452
|
+
function getZodCheck(field, check, format) {
|
|
453
|
+
return field.def.checks?.find((ck) => {
|
|
454
|
+
if (ck._zod.def.check !== check) return false;
|
|
455
|
+
if (check !== "string_format") return true;
|
|
456
|
+
return !format || format === ck._zod.def.format;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function isDropdownFieldSettings(settings) {
|
|
460
|
+
return "values" in settings && !!settings.values;
|
|
461
|
+
}
|
|
462
|
+
function isNumberFieldSettings(settings) {
|
|
463
|
+
return "min" in settings || "max" in settings;
|
|
464
|
+
}
|
|
465
|
+
function isStringFieldSettings(settings) {
|
|
466
|
+
return "minLength" in settings || "maxLength" in settings || "regExp" in settings;
|
|
467
|
+
}
|
|
468
|
+
// src/format-template.ts
|
|
469
|
+
/** Text whose every `{token}` names one of `keys`, with no stray braces. */
|
|
470
|
+
function formatTemplate(keys) {
|
|
471
|
+
return z.string().refine((value) => isValidFormat(value, keys), "Invalid template.");
|
|
472
|
+
}
|
|
473
|
+
// src/hex-string-24.ts
|
|
474
|
+
/**
|
|
475
|
+
* Twenty-four lower-case hex characters: the shape of every id on the wire and in a zod model.
|
|
476
|
+
*
|
|
477
|
+
* Named for the shape rather than for Mongo, because the shape is all a model can see — the
|
|
478
|
+
* Mingo driver mints the same ids without Mongo anywhere near them. Lower case only, which is
|
|
479
|
+
* what every driver here produces, so anything else is a value nothing in the platform minted.
|
|
480
|
+
*/
|
|
481
|
+
const HexString24Pattern = /^[a-f\d]{24}$/;
|
|
482
|
+
/**
|
|
483
|
+
* What a value that is not an id says, unless the caller says otherwise. Generic, because nothing a
|
|
484
|
+
* person picks produces one: only a hand-built request does. An `error` property so the i18n
|
|
485
|
+
* extractor keeps the key.
|
|
486
|
+
*/
|
|
487
|
+
const notAnId = { error: "Invalid id." };
|
|
488
|
+
function hexString24(messages) {
|
|
489
|
+
if (messages === void 0) return z.string().regex(HexString24Pattern);
|
|
490
|
+
const { invalid = notAnId.error, missing } = typeof messages === "string" || messages === false ? { missing: messages } : messages;
|
|
491
|
+
if (missing === false) return z.string({ error: invalid }).refine((value) => value === "" || HexString24Pattern.test(value), { error: invalid }).nullish();
|
|
492
|
+
const error = (issue) => issue.input === void 0 || issue.input === null || issue.input === "" ? missing : invalid;
|
|
493
|
+
return z.string({ error }).regex(HexString24Pattern, { error });
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* An id with nothing said about whose it is.
|
|
497
|
+
*
|
|
498
|
+
* For a stored model, where the field is a shape rather than a question put to a person: a
|
|
499
|
+
* polymorphic subject's `id`, a join's `_id`. A request that a person fills in wants
|
|
500
|
+
* `hexString24(message)` instead, so a mistyped id reads as something other than a regex.
|
|
501
|
+
*/
|
|
502
|
+
const HexString24 = hexString24();
|
|
503
|
+
// src/iso-date.ts
|
|
504
|
+
const isoDateTime = z.iso.datetime({ offset: true });
|
|
505
|
+
const calendarDay = z.iso.date();
|
|
506
|
+
function isDateInput(value) {
|
|
507
|
+
return value instanceof Date ? !Number.isNaN(value.getTime()) : isoDateTime.safeParse(value).success;
|
|
508
|
+
}
|
|
509
|
+
function isDayInput(value) {
|
|
510
|
+
return value instanceof Date ? isMidnight(value, true) : calendarDay.safeParse(value).success;
|
|
511
|
+
}
|
|
512
|
+
/** The issue `z.date()` raises, or `message` in its place. */
|
|
513
|
+
function notADate(value, message) {
|
|
514
|
+
return {
|
|
515
|
+
code: "invalid_type",
|
|
516
|
+
expected: "date",
|
|
517
|
+
input: value,
|
|
518
|
+
...message !== void 0 && { message }
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* The JSON Schema string format of each `isoDate()` input (`date-time`) and each side of an `isoDay()`
|
|
523
|
+
* (`date`), so `toJsonSchema` can tell them from any other `z.custom` or `z.date()`.
|
|
524
|
+
*/
|
|
525
|
+
const dateFormats = z.registry();
|
|
526
|
+
const inputsByMessage = /* @__PURE__ */ new Map();
|
|
527
|
+
const dayInputsByMessage = /* @__PURE__ */ new Map();
|
|
528
|
+
/** What every `isoDay()` decodes to. */
|
|
529
|
+
const dayOutput = z.date();
|
|
530
|
+
dateFormats.add(dayOutput, { format: "date" });
|
|
531
|
+
/**
|
|
532
|
+
* The string format JSON Schema describes a schema with: `date` for a side of an {@link isoDay},
|
|
533
|
+
* `date-time` for an {@link isoDate}'s input; undefined for any other schema.
|
|
534
|
+
*/
|
|
535
|
+
function dateFormat(schema) {
|
|
536
|
+
return dateFormats.get(schema)?.format;
|
|
537
|
+
}
|
|
538
|
+
/** Whether a field holds a date: `z.date()`, an {@link isoDate} or an {@link isoDay}, under any wrappers. */
|
|
539
|
+
function isDateSchema(schema) {
|
|
540
|
+
const field = unwrapField(schema, true);
|
|
541
|
+
return isZodDate(field) || isZodCodec(field) && isZodDate(field.def.out);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* A date field that also takes its ISO date-time string, as a value that came through JSON
|
|
545
|
+
* arrives.
|
|
546
|
+
*
|
|
547
|
+
* Typed `Date` on both sides although it accepts a string at runtime: a form validator is a
|
|
548
|
+
* `StandardSchemaV1<FormValues>`, and `z.input` reading `string | Date` would fail it. The cost
|
|
549
|
+
* is `z.encode`, whose declared result is `Date` but which returns the ISO string, which only
|
|
550
|
+
* matters where a value is about to be serialized.
|
|
551
|
+
*
|
|
552
|
+
* A factory, so each field has its own instance to describe, wrap and default. A `message`
|
|
553
|
+
* replaces the refusal's, as `z.date(message)` does.
|
|
554
|
+
*/
|
|
555
|
+
function isoDate(message) {
|
|
556
|
+
return z.codec(isoDateInput(message), z.date(), {
|
|
557
|
+
decode: (value) => new Date(value),
|
|
558
|
+
encode: (date) => date.toISOString()
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* What an `isoDate()` takes: a `Date`, or the ISO date-time string JSON carries one as. One
|
|
563
|
+
* instance per message.
|
|
564
|
+
*
|
|
565
|
+
* Refuses with the issue `z.date()` raises, so a refusal reads the same either way.
|
|
566
|
+
*/
|
|
567
|
+
function isoDateInput(message) {
|
|
568
|
+
return getOrSet(inputsByMessage, message ?? "", () => {
|
|
569
|
+
const input = z.custom().check((payload) => {
|
|
570
|
+
if (!isDateInput(payload.value)) payload.issues.push(notADate(payload.value, message));
|
|
571
|
+
});
|
|
572
|
+
dateFormats.add(input, { format: "date-time" });
|
|
573
|
+
if (input._zod.parent) dateFormats.add(input._zod.parent, { format: "date-time" });
|
|
574
|
+
return input;
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* A calendar day: `YYYY-MM-DD` on the wire, that day's UTC midnight as a `Date`, and the UTC day
|
|
579
|
+
* again when encoded. Typed `Date` on both sides, as {@link isoDate} is, and marked `dateOnly`,
|
|
580
|
+
* the metadata a date-only field carries.
|
|
581
|
+
*
|
|
582
|
+
* Refuses anything else (a date-time, a day no calendar has, a `Date` with a time of day) with
|
|
583
|
+
* the issue `z.date()` raises, or `message`.
|
|
584
|
+
*/
|
|
585
|
+
function isoDay(message) {
|
|
586
|
+
return z.codec(isoDayInput(message), dayOutput, {
|
|
587
|
+
decode: (value) => value instanceof Date ? value : /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`),
|
|
588
|
+
encode: (date, payload) => {
|
|
589
|
+
if (!isMidnight(date, true)) payload.issues.push(notADate(date, message));
|
|
590
|
+
return date.toISOString().slice(0, 10);
|
|
591
|
+
}
|
|
592
|
+
}).meta({ dateOnly: true });
|
|
593
|
+
}
|
|
594
|
+
/** What an `isoDay()` takes: a `Date` at UTC midnight, or `YYYY-MM-DD`. One instance per message. */
|
|
595
|
+
function isoDayInput(message) {
|
|
596
|
+
return getOrSet(dayInputsByMessage, message ?? "", () => {
|
|
597
|
+
const input = z.custom().check((payload) => {
|
|
598
|
+
if (!isDayInput(payload.value)) payload.issues.push(notADate(payload.value, message));
|
|
599
|
+
});
|
|
600
|
+
dateFormats.add(input, { format: "date" });
|
|
601
|
+
if (input._zod.parent) dateFormats.add(input._zod.parent, { format: "date" });
|
|
602
|
+
return input;
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
// src/json-text.ts
|
|
606
|
+
/** JSON held as text, strict or loose (`parseLooseJson`). A blank value is left to the field's own required rule. */
|
|
607
|
+
const JsonText = z.string().refine((text) => !text.trim() || isLooseJson(text), "Invalid JSON.");
|
|
608
|
+
// src/labelled.ts
|
|
609
|
+
/** The label a field carries, read through the optional/nullable/default wrappers declared around it. */
|
|
610
|
+
function fieldLabel(schema) {
|
|
611
|
+
for (let current = schema; current; current = current.def?.innerType) {
|
|
612
|
+
const meta = z.globalRegistry.get(current);
|
|
613
|
+
if (typeof meta?.title === "string") return {
|
|
614
|
+
...isStringRecord(meta.options) && { options: meta.options },
|
|
615
|
+
...typeof meta.reference === "string" && { reference: meta.reference },
|
|
616
|
+
title: meta.title
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function isStringRecord(value) {
|
|
621
|
+
return isObject(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
622
|
+
}
|
|
623
|
+
/** A field, labelled. The label carries into the JSON Schema; validation is unchanged. */
|
|
624
|
+
function labelled(schema, label) {
|
|
625
|
+
return schema.meta({ ...label });
|
|
626
|
+
}
|
|
627
|
+
// src/language-code.ts
|
|
628
|
+
/**
|
|
629
|
+
* A field that takes an ISO 639-1 language, optionally with its region (`fr`, `fr-CA`), that
|
|
630
|
+
* this runtime can name.
|
|
631
|
+
*
|
|
632
|
+
* The message is the caller's, as `timeZone`'s is: the i18n extractor harvests a plain zod
|
|
633
|
+
* message where it is written. `whenEmpty` is separate for a form that distinguishes a field
|
|
634
|
+
* nobody filled in from one filled in wrongly; left out, one message answers for both.
|
|
635
|
+
*/
|
|
636
|
+
function languageCode(message, whenEmpty = message) {
|
|
637
|
+
return z.string().min(1, whenEmpty).refine(isLanguageCode, message);
|
|
638
|
+
}
|
|
639
|
+
// src/localized-string.ts
|
|
640
|
+
/**
|
|
641
|
+
* A localized value with nothing said about it: any record of language → text, blank included.
|
|
642
|
+
*
|
|
643
|
+
* For a stored model, where a row saved before a rule must still read. A field a person fills
|
|
644
|
+
* in wants `localizedString(messages)` instead, so a blank one is refused in words.
|
|
645
|
+
*/
|
|
646
|
+
const LocalizedString = z.record(z.string(), z.string());
|
|
647
|
+
const LocalizedStringArray = z.record(z.string(), z.array(z.string()));
|
|
648
|
+
/** As {@link LocalizedString}, or a plain string — what a write model takes for a localized field. */
|
|
649
|
+
const LocalizedStringInput = z.union([z.string(), LocalizedString]);
|
|
650
|
+
const LocalizedStringArrayInput = z.union([z.array(z.string()), LocalizedStringArray]);
|
|
651
|
+
/**
|
|
652
|
+
* Whether a localized value has text: a string that isn't blank, or text in at least one
|
|
653
|
+
* language. `undefined`, `null`, `''`, `' '`, `{}` and `{ en: '', fr: ' ' }` have none.
|
|
654
|
+
*/
|
|
655
|
+
function hasLocalizedText(value) {
|
|
656
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
657
|
+
if (!value || typeof value !== "object") return false;
|
|
658
|
+
return Object.values(value).some((text) => typeof text === "string" && text.trim().length > 0);
|
|
659
|
+
}
|
|
660
|
+
/** Whether a localized value is at most `max` characters, as a string or in every language it names. */
|
|
661
|
+
function isLocalizedTextWithin(value, max) {
|
|
662
|
+
if (typeof value === "string") return value.length <= max;
|
|
663
|
+
if (!value || typeof value !== "object") return true;
|
|
664
|
+
return Object.values(value).every((text) => typeof text !== "string" || text.length <= max);
|
|
665
|
+
}
|
|
666
|
+
function withText(shape, messages) {
|
|
667
|
+
if (messages === void 0) return shape(void 0).refine(hasLocalizedText);
|
|
668
|
+
const { invalid, missing } = typeof messages === "string" ? {
|
|
669
|
+
invalid: messages,
|
|
670
|
+
missing: messages
|
|
671
|
+
} : messages;
|
|
672
|
+
if (missing === false) return shape(() => invalid).nullish();
|
|
673
|
+
const error = (issue) => issue.input === void 0 || issue.input === null ? missing : invalid;
|
|
674
|
+
return shape(error).refine(hasLocalizedText, { error: missing });
|
|
675
|
+
}
|
|
676
|
+
/** A locale record whose every error — the record's, or one language's text — says `error`. */
|
|
677
|
+
const record = (error) => z.record(z.string(), z.string({ error }), { error });
|
|
678
|
+
function localizedString(messages) {
|
|
679
|
+
return withText(record, messages);
|
|
680
|
+
}
|
|
681
|
+
function localizedStringInput(messages) {
|
|
682
|
+
return withText((error) => z.union([z.string({ error }), record(error)], { error }), messages);
|
|
683
|
+
}
|
|
684
|
+
const accepts = (schema, value) => schema.safeParse(value).success;
|
|
685
|
+
/**
|
|
686
|
+
* The write-model fields that let a required localized name through blank — for a guard spec to
|
|
687
|
+
* assert is empty.
|
|
688
|
+
*
|
|
689
|
+
* For every exported model `X` with an `AddX` or `UpdateX` beside it: a field `X` requires, and
|
|
690
|
+
* that takes both a string and a locale record, is a required localized name. Its write models
|
|
691
|
+
* must refuse `''` and `{ en: '' }`, which {@link localizedStringInput} does. Returns
|
|
692
|
+
* `UpdateRole.name`-style paths for each one that doesn't. Read from the schemas rather than the
|
|
693
|
+
* source, so it sees what a write actually accepts.
|
|
694
|
+
*/
|
|
695
|
+
function findUncheckedLocalizedFields(models) {
|
|
696
|
+
const unchecked = [];
|
|
697
|
+
for (const [name, model] of Object.entries(models)) {
|
|
698
|
+
if (!(model instanceof z.ZodObject)) continue;
|
|
699
|
+
const shape = model.shape;
|
|
700
|
+
const required = Object.keys(shape).filter((key) => {
|
|
701
|
+
const field = shape[key];
|
|
702
|
+
return !accepts(field, void 0) && accepts(field, "Name") && accepts(field, { en: "Name" });
|
|
703
|
+
});
|
|
704
|
+
if (required.length === 0) continue;
|
|
705
|
+
for (const writeName of [`Add${name}`, `Update${name}`]) {
|
|
706
|
+
const write = models[writeName];
|
|
707
|
+
if (!(write instanceof z.ZodObject)) continue;
|
|
708
|
+
const writeShape = write.shape;
|
|
709
|
+
for (const key of required) {
|
|
710
|
+
const field = writeShape[key];
|
|
711
|
+
if (field && (accepts(field, "") || accepts(field, { en: "" }))) unchecked.push(`${writeName}.${key}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return unchecked.sort();
|
|
716
|
+
}
|
|
717
|
+
// src/merge.ts
|
|
718
|
+
/**
|
|
719
|
+
* Adds a Zod type to an existing ZodUnion.
|
|
720
|
+
*/
|
|
721
|
+
function addToUnion(union, object) {
|
|
722
|
+
return z.union([object, ...union.options]);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Deeply merges two ZodRawShape objects into one.
|
|
726
|
+
*/
|
|
727
|
+
function mergeShapes(shapeA, shapeB) {
|
|
728
|
+
const mergedShape = { ...shapeA };
|
|
729
|
+
for (const key in shapeB) if (key in mergedShape) {
|
|
730
|
+
const valueA = mergedShape[key];
|
|
731
|
+
const valueB = shapeB[key];
|
|
732
|
+
if (isZodObject(valueA) && isZodObject(valueB)) {
|
|
733
|
+
const shapeInnerA = valueA.shape;
|
|
734
|
+
const shapeInnerB = valueB.shape;
|
|
735
|
+
mergedShape[key] = z.object(mergeShapes(shapeInnerA, shapeInnerB));
|
|
736
|
+
} else mergedShape[key] = z.union([valueA, valueB]);
|
|
737
|
+
} else mergedShape[key] = shapeB[key];
|
|
738
|
+
return mergedShape;
|
|
739
|
+
}
|
|
740
|
+
// src/nullish.ts
|
|
741
|
+
function nullish(fields) {
|
|
742
|
+
return z.object(mapValues(fields.shape, (field) => field.nullish()));
|
|
743
|
+
}
|
|
744
|
+
// src/positional-paths.ts
|
|
745
|
+
/** What `segment` names under `node`, looking through arrays the way Mongo's dotted paths do. */
|
|
746
|
+
function childrenOf(node, segment) {
|
|
747
|
+
if (isZodArray(node)) return flatten(node.def.element).flatMap((element) => childrenOf(element, segment));
|
|
748
|
+
if (isZodObject(node)) return node.shape[segment] ? flatten(node.shape[segment]) : [];
|
|
749
|
+
if (isZodRecord(node) && isZodType(node.def.valueType)) return flatten(node.def.valueType);
|
|
750
|
+
return [];
|
|
751
|
+
}
|
|
752
|
+
function flatten(node) {
|
|
753
|
+
const unwrapped = unwrapField(node, true);
|
|
754
|
+
return isZodUnion(unwrapped) ? unwrapped.options.flatMap(flatten) : [unwrapped];
|
|
755
|
+
}
|
|
756
|
+
/** Nodes a path can end at but not pass through: what lies past them can't be named from the schema. */
|
|
757
|
+
const opaqueTypes = /* @__PURE__ */ new Set([
|
|
758
|
+
"any",
|
|
759
|
+
"custom",
|
|
760
|
+
"intersection",
|
|
761
|
+
"pipe",
|
|
762
|
+
"record",
|
|
763
|
+
"transform",
|
|
764
|
+
"tuple",
|
|
765
|
+
"unknown"
|
|
766
|
+
]);
|
|
767
|
+
/** What `segment` names under `node` on the way to a deeper field, or `undefined` where the schema can't be followed. */
|
|
768
|
+
function followedChildren(node, segment) {
|
|
769
|
+
if (opaqueTypes.has(node.def.type)) return void 0;
|
|
770
|
+
if (isZodObject(node)) return node.shape[segment] ? flatten(node.shape[segment]) : [];
|
|
771
|
+
if (!isZodArray(node)) return [];
|
|
772
|
+
const children = flatten(node.def.element).map((element) => followedChildren(element, segment));
|
|
773
|
+
return children.some((found) => found === void 0) ? void 0 : children.flatMap((found) => found ?? []);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Whether {@link toFieldPath} can follow `path` through `schema`: every segment is reached through objects and
|
|
777
|
+
* arrays (and the wrappers and unions around them), with no pipe, transform, intersection, tuple, record or loose
|
|
778
|
+
* (`any`, `unknown`, `custom`) node before the last. The path may end at one.
|
|
779
|
+
*/
|
|
780
|
+
function isFollowablePath(schema, path) {
|
|
781
|
+
let nodes = flatten(schema);
|
|
782
|
+
for (const segment of path.split(".")) {
|
|
783
|
+
const next = [];
|
|
784
|
+
for (const node of nodes) {
|
|
785
|
+
const children = followedChildren(node, segment);
|
|
786
|
+
if (children === void 0) return false;
|
|
787
|
+
next.push(...children);
|
|
788
|
+
}
|
|
789
|
+
if (next.length === 0) return false;
|
|
790
|
+
nodes = next;
|
|
791
|
+
}
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* `path` with `.$[]` after every segment that `schema` types as an array, except the last:
|
|
796
|
+
* `settings.pages.defaultAccessLevels` -> `settings.pages.$[].defaultAccessLevels`. Returns
|
|
797
|
+
* `path` unchanged when no segment before the last is an array.
|
|
798
|
+
*
|
|
799
|
+
* At a union, every arm is followed; a segment is an array when any arm types it as one.
|
|
800
|
+
*/
|
|
801
|
+
function toAllPositionalPath(schema, path) {
|
|
802
|
+
const segments = path.split(".");
|
|
803
|
+
const result = [];
|
|
804
|
+
let nodes = flatten(schema);
|
|
805
|
+
for (const [index, segment] of segments.entries()) {
|
|
806
|
+
const children = nodes.flatMap((node) => {
|
|
807
|
+
const child = isZodObject(node) ? node.shape[segment] : void 0;
|
|
808
|
+
return child ? flatten(child) : [];
|
|
809
|
+
});
|
|
810
|
+
const arrays = children.filter(isZodArray);
|
|
811
|
+
result.push(segment);
|
|
812
|
+
if (arrays.length > 0 && index < segments.length - 1) {
|
|
813
|
+
result.push("$[]");
|
|
814
|
+
nodes = arrays.flatMap((array) => flatten(array.def.element));
|
|
815
|
+
} else nodes = children;
|
|
816
|
+
}
|
|
817
|
+
return result.join(".");
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* The field path `path` names in `schema`, its array positions left out:
|
|
821
|
+
* `lines.0.attachments.$[].title` -> `lines.attachments.title`. A numeric segment is dropped
|
|
822
|
+
* only where `schema` has an array, so a record's key stays: `budgets.2026` is unchanged.
|
|
823
|
+
* `$`, `$[]` and `$[id]` are always positions.
|
|
824
|
+
*/
|
|
825
|
+
function toFieldPath(schema, path) {
|
|
826
|
+
const result = [];
|
|
827
|
+
let nodes = flatten(schema);
|
|
828
|
+
for (const segment of path.split(".")) {
|
|
829
|
+
const arrays = nodes.filter(isZodArray);
|
|
830
|
+
if (segment.startsWith("$") || isInteger(segment) && arrays.length > 0) {
|
|
831
|
+
if (arrays.length > 0) nodes = arrays.flatMap((array) => flatten(array.def.element));
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
result.push(segment);
|
|
835
|
+
nodes = nodes.flatMap((node) => childrenOf(node, segment));
|
|
836
|
+
}
|
|
837
|
+
return result.join(".");
|
|
838
|
+
}
|
|
839
|
+
// src/reg-exp-pattern.ts
|
|
840
|
+
const RegExpPattern = z.string().refine(isValidPattern, "Invalid regular expression.");
|
|
841
|
+
// src/slug.ts
|
|
842
|
+
/** What a value that is not a slug says. An `error` property so the i18n extractor keeps the key. */
|
|
843
|
+
const invalidSlug = { error: "Use lower-case letters, numbers and dashes." };
|
|
844
|
+
/** A key, identifier or channel in {@link SlugPattern}. `missing` is the field's own message for an empty value, and the only one an empty value gets. */
|
|
845
|
+
function slug(missing = invalidSlug.error) {
|
|
846
|
+
return z.string().min(1, {
|
|
847
|
+
abort: true,
|
|
848
|
+
error: missing
|
|
849
|
+
}).regex(SlugPattern, invalidSlug.error);
|
|
850
|
+
}
|
|
851
|
+
/** A slug with nothing said about an empty value beyond what any other wrong value gets. */
|
|
852
|
+
const Slug = slug();
|
|
853
|
+
// src/strip-empty-fields.ts
|
|
854
|
+
function isEmpty(value) {
|
|
855
|
+
if (value === void 0 || value === "") return true;
|
|
856
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
857
|
+
return isPlainObject(value) && Object.keys(value).length === 0;
|
|
858
|
+
}
|
|
859
|
+
function resolveShape(schema, values) {
|
|
860
|
+
if (isZodObject(schema)) return schema.shape;
|
|
861
|
+
const options = schema.def.options;
|
|
862
|
+
if (Array.isArray(options)) for (const option of options) {
|
|
863
|
+
if (!isZodObject(option)) continue;
|
|
864
|
+
const shape = option.shape;
|
|
865
|
+
if (Object.entries(shape).every(([key, fieldSchema]) => !(fieldSchema instanceof ZodLiteral) || fieldSchema.def.values.some((literal) => literal === values[key]))) return shape;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function stripEmptyFields(schema, values) {
|
|
869
|
+
return stripEmptyFieldsRecursive(schema, values, /* @__PURE__ */ new Set());
|
|
870
|
+
}
|
|
871
|
+
function stripEmptyFieldsRecursive(schema, values, seen) {
|
|
872
|
+
const shape = resolveShape(schema, values);
|
|
873
|
+
if (!shape) return values;
|
|
874
|
+
const result = { ...values };
|
|
875
|
+
for (const key of Object.keys(result)) {
|
|
876
|
+
const fieldSchema = shape[key];
|
|
877
|
+
if (!fieldSchema) continue;
|
|
878
|
+
const value = result[key];
|
|
879
|
+
if (isFieldOptional(fieldSchema) && isEmpty(value)) {
|
|
880
|
+
delete result[key];
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
884
|
+
const innerType = getInnerType(fieldSchema);
|
|
885
|
+
if (isZodObject(innerType) && !seen.has(value)) {
|
|
886
|
+
seen.add(value);
|
|
887
|
+
result[key] = stripEmptyFieldsRecursive(innerType, value, seen);
|
|
888
|
+
seen.delete(value);
|
|
889
|
+
if (isFieldOptional(fieldSchema) && isEmpty(result[key])) delete result[key];
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return result;
|
|
894
|
+
}
|
|
895
|
+
// src/strip-to-schema.ts
|
|
896
|
+
function pick(schemas, value) {
|
|
897
|
+
const fields = schemas.flatMap(variantsOf);
|
|
898
|
+
if (Array.isArray(value)) {
|
|
899
|
+
const elements = fields.filter(isZodArray).map((field) => field.element);
|
|
900
|
+
return elements.length === 0 ? value : value.map((item) => pick(elements, item));
|
|
901
|
+
}
|
|
902
|
+
const shapes = fields.flatMap((field) => isZodObject(field) ? [field.shape] : []);
|
|
903
|
+
return shapes.length === 0 || !isPlainObject(value) ? value : pickFields(shapes, value);
|
|
904
|
+
}
|
|
905
|
+
/** The fields of `value` that any of `shapes` declares, each picked by every shape that declares it. */
|
|
906
|
+
function pickFields(shapes, value) {
|
|
907
|
+
const record = value;
|
|
908
|
+
const picked = {};
|
|
909
|
+
for (const key of new Set(shapes.flatMap((shape) => Object.keys(shape)))) if (Object.hasOwn(record, key) && record[key] !== void 0) picked[key] = pick(shapes.flatMap((shape) => Object.hasOwn(shape, key) ? [shape[key]] : []), record[key]);
|
|
910
|
+
return picked;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* `value` cut down to the fields `schema` declares, at every depth: through arrays, optionals, nullables, defaults,
|
|
914
|
+
* catches, lazies and the objects of a union or an intersection, merged. Nothing is parsed, so no checks, codecs or defaults run, and a
|
|
915
|
+
* value that doesn't fit its field is copied as it is. `value` itself is picked by its keys, whatever its prototype.
|
|
916
|
+
*/
|
|
917
|
+
function stripToSchema(schema, value) {
|
|
918
|
+
return pickFields([schema.shape], value);
|
|
919
|
+
}
|
|
920
|
+
/** What `schema` holds a value as, through wrappers and lazies: each option of a union, each side of an intersection, or itself. */
|
|
921
|
+
function variantsOf(schema) {
|
|
922
|
+
const field = unwrapField(schema, true);
|
|
923
|
+
if (isZodIntersection(field)) return [...variantsOf(field.def.left), ...variantsOf(field.def.right)];
|
|
924
|
+
return isZodUnion(field) ? field.options.flatMap(variantsOf) : [field];
|
|
925
|
+
}
|
|
926
|
+
// src/time-zone.ts
|
|
927
|
+
/**
|
|
928
|
+
* A field that takes an IANA zone this runtime can actually format in.
|
|
929
|
+
*
|
|
930
|
+
* The check belongs at the write boundary: an unsupported zone is stored happily and then
|
|
931
|
+
* throws a `RangeError` inside `Intl` on every later read, far from whatever put it there.
|
|
932
|
+
*
|
|
933
|
+
* The message is the CALLER's, as `objectId`'s is, for two reasons. A domain says what it
|
|
934
|
+
* wants in its own words — "Select a valid time zone." on a form, "Invalid time zone." on a
|
|
935
|
+
* public booking — and the i18n extractor harvests a plain zod message where it is written,
|
|
936
|
+
* so a sentence buried in here would reach no dictionary. `whenEmpty` is separate for a form
|
|
937
|
+
* that distinguishes a field nobody filled in from one filled in wrongly; left out, one
|
|
938
|
+
* message answers for both.
|
|
939
|
+
*/
|
|
940
|
+
function timeZone(message, whenEmpty = message) {
|
|
941
|
+
return z.string().min(1, whenEmpty).refine(isSupportedTimezone, message);
|
|
942
|
+
}
|
|
943
|
+
// src/to-json-schema.ts
|
|
944
|
+
/**
|
|
945
|
+
* A JSON Schema document, as {@link toJsonSchema} produces.
|
|
946
|
+
*
|
|
947
|
+
* An object rather than `unknown` because these schemas exist to be READ: an editor
|
|
948
|
+
* walks `properties` to build its field pickers. As `unknown`, every consumer casts
|
|
949
|
+
* before it can index - at exactly the boundary that was supposed to describe the value.
|
|
950
|
+
*
|
|
951
|
+
* Deliberately not modelled more precisely than "a JSON object". JSON Schema is
|
|
952
|
+
* recursive and open-ended, and restating its grammar here would be a second, worse
|
|
953
|
+
* copy of a specification we do not own.
|
|
954
|
+
*/
|
|
955
|
+
const JsonSchemaDocument = z.record(z.string(), z.unknown());
|
|
956
|
+
/**
|
|
957
|
+
* `z.toJSONSchema`, with dates described rather than refused.
|
|
958
|
+
*
|
|
959
|
+
* zod has no JSON Schema for `z.date()` and throws on it by default, so one date field
|
|
960
|
+
* anywhere in a schema fails the whole conversion - which is how one event with a
|
|
961
|
+
* `startAt` blanked the workflow editor, and why every domain model's schema endpoint
|
|
962
|
+
* answered 500. Here a date is `{ type: 'string', format: 'date-time' }`, the node
|
|
963
|
+
* `z.iso.datetime()` produces, so a reader sees the same field either way, and an
|
|
964
|
+
* `isoDate()` reads the same as `z.date()` on either side; an `isoDay()` is a `format: 'date'`
|
|
965
|
+
* string on either side. The value
|
|
966
|
+
* itself is untouched: a schema that takes a `Date` still takes a `Date`.
|
|
967
|
+
*
|
|
968
|
+
* Use this rather than `z.toJSONSchema`; the lint rule `no-restricted-properties`
|
|
969
|
+
* points there.
|
|
970
|
+
*/
|
|
971
|
+
function toJsonSchema(schema, options = {}) {
|
|
972
|
+
const otherwise = options.unrepresentable ?? "throw";
|
|
973
|
+
return z.toJSONSchema(schema, {
|
|
974
|
+
io: options.io,
|
|
975
|
+
unrepresentable: ({ zodSchema }) => {
|
|
976
|
+
const format = dateFormat(zodSchema) ?? (zodSchema._zod.def.type === "date" ? "date-time" : void 0);
|
|
977
|
+
return format ? {
|
|
978
|
+
format,
|
|
979
|
+
type: "string"
|
|
980
|
+
} : otherwise;
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
// src/whole-number.ts
|
|
985
|
+
const NonNegativeInteger = boundedNumber({
|
|
986
|
+
integer: true,
|
|
987
|
+
min: 0
|
|
988
|
+
});
|
|
989
|
+
const PositiveInteger = boundedNumber({
|
|
990
|
+
integer: true,
|
|
991
|
+
min: 1
|
|
992
|
+
});
|
|
993
|
+
export { AnyFieldSettingsSchema, CurrencyCodePattern, HexString24, HexString24Pattern, JsonSchemaDocument, JsonText, LocalizedString, LocalizedStringArray, LocalizedStringArrayInput, LocalizedStringInput, NonNegativeInteger, PositiveInteger, RegExpPattern, Slug, ZOD_WRAPPER_TYPES, addToUnion, applyFieldSettings, boundedNumber, codecParts, currencyCode, fieldLabel, findUncheckedLocalizedFields, formatTemplate, getInnerType, getZodFieldPaths, hasLocalizedText, hexString24, isDateSchema, isFieldNullable, isFieldOptional, isFollowablePath, isLocalizedTextWithin, isOptionalOutput, isZodAny, isZodArray, isZodCatch, isZodCodec, isZodDate, isZodDefault, isZodEnum, isZodIntersection, isZodLazy, isZodNullable, isZodNumber, isZodObject, isZodObjectLoose, isZodOptional, isZodPipe, isZodRecord, isZodString, isZodTransform, isZodType, isZodUnion, isZodUnknown, isZodWrapper, isoDate, isoDay, labelled, languageCode, localizedString, localizedStringInput, mergeShapes, nullish, slug, stripEmptyFields, stripToSchema, timeZone, toAllPositionalPath, toFieldPath, toJsonSchema, unwrapField, withCodecParts };
|