@temelyos/zod 1.0.0 → 1.0.1

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 (50) hide show
  1. package/dist/bounded-number.d.ts +8 -0
  2. package/dist/bounded-number.js +36 -0
  3. package/dist/codec-parts.d.ts +8 -0
  4. package/dist/codec-parts.js +21 -0
  5. package/dist/currency-code.d.ts +5 -0
  6. package/dist/currency-code.js +11 -0
  7. package/dist/field-paths.d.ts +54 -0
  8. package/dist/field-paths.js +67 -0
  9. package/dist/field-settings.d.ts +119 -0
  10. package/dist/field-settings.js +219 -0
  11. package/dist/format-template.d.ts +3 -0
  12. package/dist/format-template.js +8 -0
  13. package/dist/hex-string-24.d.ts +45 -0
  14. package/dist/hex-string-24.js +32 -0
  15. package/dist/index.d.ts +25 -501
  16. package/dist/index.js +23 -992
  17. package/dist/iso-date.d.ts +25 -0
  18. package/dist/iso-date.js +108 -0
  19. package/dist/json-text.d.ts +3 -0
  20. package/dist/json-text.js +6 -0
  21. package/dist/labelled.d.ts +17 -0
  22. package/dist/labelled.js +22 -0
  23. package/dist/language-code.d.ts +10 -0
  24. package/dist/language-code.js +15 -0
  25. package/dist/localized-string.d.ts +75 -0
  26. package/dist/localized-string.js +80 -0
  27. package/dist/merge.d.ts +14 -0
  28. package/dist/merge.js +26 -0
  29. package/dist/nullish.d.ts +2 -0
  30. package/dist/nullish.js +7 -0
  31. package/dist/positional-paths.d.ts +22 -0
  32. package/dist/positional-paths.js +99 -0
  33. package/dist/reg-exp-pattern.d.ts +2 -0
  34. package/dist/reg-exp-pattern.js +5 -0
  35. package/dist/slug.d.ts +5 -0
  36. package/dist/slug.js +15 -0
  37. package/dist/strip-empty-fields.d.ts +2 -0
  38. package/dist/strip-empty-fields.js +46 -0
  39. package/dist/strip-to-schema.d.ts +7 -0
  40. package/dist/strip-to-schema.js +35 -0
  41. package/dist/time-zone.d.ts +15 -0
  42. package/dist/time-zone.js +20 -0
  43. package/dist/to-json-schema.d.ts +40 -0
  44. package/dist/to-json-schema.js +44 -0
  45. package/dist/types.d.ts +4 -0
  46. package/dist/utilities.d.ts +39 -0
  47. package/dist/utilities.js +116 -0
  48. package/dist/whole-number.d.ts +2 -0
  49. package/dist/whole-number.js +11 -0
  50. package/package.json +5 -2
@@ -0,0 +1,25 @@
1
+ import { ZodType, z } from "zod";
2
+ /** Whether a field holds a date: `z.date()`, an {@link isoDate} or an {@link isoDay}, under any wrappers. */
3
+ export declare function isDateSchema(schema: ZodType): boolean;
4
+ /**
5
+ * A date field that also takes its ISO date-time string, as a value that came through JSON
6
+ * arrives.
7
+ *
8
+ * Typed `Date` on both sides although it accepts a string at runtime: a form validator is a
9
+ * `StandardSchemaV1<FormValues>`, and `z.input` reading `string | Date` would fail it. The cost
10
+ * is `z.encode`, whose declared result is `Date` but which returns the ISO string, which only
11
+ * matters where a value is about to be serialized.
12
+ *
13
+ * A factory, so each field has its own instance to describe, wrap and default. A `message`
14
+ * replaces the refusal's, as `z.date(message)` does.
15
+ */
16
+ export declare function isoDate(message?: string): z.ZodCodec<z.ZodCustom<Date, Date>, z.ZodDate>;
17
+ /**
18
+ * A calendar day: `YYYY-MM-DD` on the wire, that day's UTC midnight as a `Date`, and the UTC day
19
+ * again when encoded. Typed `Date` on both sides, as {@link isoDate} is, and marked `dateOnly`,
20
+ * the metadata a date-only field carries.
21
+ *
22
+ * Refuses anything else (a date-time, a day no calendar has, a `Date` with a time of day) with
23
+ * the issue `z.date()` raises, or `message`.
24
+ */
25
+ export declare function isoDay(message?: string): z.ZodCodec<z.ZodCustom<Date, Date>, z.ZodDate>;
@@ -0,0 +1,108 @@
1
+ import { unwrapField } from "./field-paths.js";
2
+ import { isZodCodec, isZodDate } from "./utilities.js";
3
+ import { z } from "zod";
4
+ import { getOrSet } from "@temelyos/core/cache";
5
+ import { isMidnight } from "@temelyos/core/date";
6
+ // src/iso-date.ts
7
+ const isoDateTime = z.iso.datetime({ offset: true });
8
+ const calendarDay = z.iso.date();
9
+ function isDateInput(value) {
10
+ return value instanceof Date ? !Number.isNaN(value.getTime()) : isoDateTime.safeParse(value).success;
11
+ }
12
+ function isDayInput(value) {
13
+ return value instanceof Date ? isMidnight(value, true) : calendarDay.safeParse(value).success;
14
+ }
15
+ /** The issue `z.date()` raises, or `message` in its place. */
16
+ function notADate(value, message) {
17
+ return {
18
+ code: "invalid_type",
19
+ expected: "date",
20
+ input: value,
21
+ ...message !== void 0 && { message }
22
+ };
23
+ }
24
+ /**
25
+ * The JSON Schema string format of each `isoDate()` input (`date-time`) and each side of an `isoDay()`
26
+ * (`date`), so `toJsonSchema` can tell them from any other `z.custom` or `z.date()`.
27
+ */
28
+ const dateFormats = z.registry();
29
+ const inputsByMessage = /* @__PURE__ */ new Map();
30
+ const dayInputsByMessage = /* @__PURE__ */ new Map();
31
+ /** What every `isoDay()` decodes to. */
32
+ const dayOutput = z.date();
33
+ dateFormats.add(dayOutput, { format: "date" });
34
+ /**
35
+ * The string format JSON Schema describes a schema with: `date` for a side of an {@link isoDay},
36
+ * `date-time` for an {@link isoDate}'s input; undefined for any other schema.
37
+ */
38
+ function dateFormat(schema) {
39
+ return dateFormats.get(schema)?.format;
40
+ }
41
+ /** Whether a field holds a date: `z.date()`, an {@link isoDate} or an {@link isoDay}, under any wrappers. */
42
+ function isDateSchema(schema) {
43
+ const field = unwrapField(schema, true);
44
+ return isZodDate(field) || isZodCodec(field) && isZodDate(field.def.out);
45
+ }
46
+ /**
47
+ * A date field that also takes its ISO date-time string, as a value that came through JSON
48
+ * arrives.
49
+ *
50
+ * Typed `Date` on both sides although it accepts a string at runtime: a form validator is a
51
+ * `StandardSchemaV1<FormValues>`, and `z.input` reading `string | Date` would fail it. The cost
52
+ * is `z.encode`, whose declared result is `Date` but which returns the ISO string, which only
53
+ * matters where a value is about to be serialized.
54
+ *
55
+ * A factory, so each field has its own instance to describe, wrap and default. A `message`
56
+ * replaces the refusal's, as `z.date(message)` does.
57
+ */
58
+ function isoDate(message) {
59
+ return z.codec(isoDateInput(message), z.date(), {
60
+ decode: (value) => new Date(value),
61
+ encode: (date) => date.toISOString()
62
+ });
63
+ }
64
+ /**
65
+ * What an `isoDate()` takes: a `Date`, or the ISO date-time string JSON carries one as. One
66
+ * instance per message.
67
+ *
68
+ * Refuses with the issue `z.date()` raises, so a refusal reads the same either way.
69
+ */
70
+ function isoDateInput(message) {
71
+ return getOrSet(inputsByMessage, message ?? "", () => {
72
+ const input = z.custom().check((payload) => {
73
+ if (!isDateInput(payload.value)) payload.issues.push(notADate(payload.value, message));
74
+ });
75
+ dateFormats.add(input, { format: "date-time" });
76
+ if (input._zod.parent) dateFormats.add(input._zod.parent, { format: "date-time" });
77
+ return input;
78
+ });
79
+ }
80
+ /**
81
+ * A calendar day: `YYYY-MM-DD` on the wire, that day's UTC midnight as a `Date`, and the UTC day
82
+ * again when encoded. Typed `Date` on both sides, as {@link isoDate} is, and marked `dateOnly`,
83
+ * the metadata a date-only field carries.
84
+ *
85
+ * Refuses anything else (a date-time, a day no calendar has, a `Date` with a time of day) with
86
+ * the issue `z.date()` raises, or `message`.
87
+ */
88
+ function isoDay(message) {
89
+ return z.codec(isoDayInput(message), dayOutput, {
90
+ decode: (value) => value instanceof Date ? value : /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`),
91
+ encode: (date, payload) => {
92
+ if (!isMidnight(date, true)) payload.issues.push(notADate(date, message));
93
+ return date.toISOString().slice(0, 10);
94
+ }
95
+ }).meta({ dateOnly: true });
96
+ }
97
+ /** What an `isoDay()` takes: a `Date` at UTC midnight, or `YYYY-MM-DD`. One instance per message. */
98
+ function isoDayInput(message) {
99
+ return getOrSet(dayInputsByMessage, message ?? "", () => {
100
+ const input = z.custom().check((payload) => {
101
+ if (!isDayInput(payload.value)) payload.issues.push(notADate(payload.value, message));
102
+ });
103
+ dateFormats.add(input, { format: "date" });
104
+ if (input._zod.parent) dateFormats.add(input._zod.parent, { format: "date" });
105
+ return input;
106
+ });
107
+ }
108
+ export { dateFormat, isDateSchema, isoDate, isoDay };
@@ -0,0 +1,3 @@
1
+ import { z } from "zod";
2
+ /** JSON held as text, strict or loose (`parseLooseJson`). A blank value is left to the field's own required rule. */
3
+ export declare const JsonText: z.ZodString;
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+ import { isLooseJson } from "@temelyos/core/json";
3
+ // src/json-text.ts
4
+ /** JSON held as text, strict or loose (`parseLooseJson`). A blank value is left to the field's own required rule. */
5
+ const JsonText = z.string().refine((text) => !text.trim() || isLooseJson(text), "Invalid JSON.");
6
+ export { JsonText };
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ /** The values of an enum field, or of the elements of an array of one. */
3
+ type EnumValue<T extends z.ZodType> = Extract<z.output<T> extends readonly (infer TElement)[] ? TElement : z.output<T>, string>;
4
+ /** How a field is named where the product names fields generically, such as the workflow criteria editor. */
5
+ export interface FieldLabel<TValue extends string = string> {
6
+ /** A dictionary key naming each value of an enum field, by value. */
7
+ options?: Record<TValue, string>;
8
+ /** The kind of record the value is the id of, such as `users`; the editor offers a search of that kind for it. */
9
+ reference?: string;
10
+ /** A dictionary key naming the field. */
11
+ title: string;
12
+ }
13
+ /** The label a field carries, read through the optional/nullable/default wrappers declared around it. */
14
+ export declare function fieldLabel(schema: z.ZodType): FieldLabel | undefined;
15
+ /** A field, labelled. The label carries into the JSON Schema; validation is unchanged. */
16
+ export declare function labelled<T extends z.ZodType>(schema: T, label: FieldLabel<EnumValue<T>>): T;
17
+ export {};
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { isObject } from "@temelyos/core/object";
3
+ // src/labelled.ts
4
+ /** The label a field carries, read through the optional/nullable/default wrappers declared around it. */
5
+ function fieldLabel(schema) {
6
+ for (let current = schema; current; current = current.def?.innerType) {
7
+ const meta = z.globalRegistry.get(current);
8
+ if (typeof meta?.title === "string") return {
9
+ ...isStringRecord(meta.options) && { options: meta.options },
10
+ ...typeof meta.reference === "string" && { reference: meta.reference },
11
+ title: meta.title
12
+ };
13
+ }
14
+ }
15
+ function isStringRecord(value) {
16
+ return isObject(value) && Object.values(value).every((entry) => typeof entry === "string");
17
+ }
18
+ /** A field, labelled. The label carries into the JSON Schema; validation is unchanged. */
19
+ function labelled(schema, label) {
20
+ return schema.meta({ ...label });
21
+ }
22
+ export { fieldLabel, labelled };
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * A field that takes an ISO 639-1 language, optionally with its region (`fr`, `fr-CA`), that
4
+ * this runtime can name.
5
+ *
6
+ * The message is the caller's, as `timeZone`'s is: the i18n extractor harvests a plain zod
7
+ * message where it is written. `whenEmpty` is separate for a form that distinguishes a field
8
+ * nobody filled in from one filled in wrongly; left out, one message answers for both.
9
+ */
10
+ export declare function languageCode(message: string, whenEmpty?: string): z.ZodString;
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { isLanguageCode } from "@temelyos/core/i18n";
3
+ // src/language-code.ts
4
+ /**
5
+ * A field that takes an ISO 639-1 language, optionally with its region (`fr`, `fr-CA`), that
6
+ * this runtime can name.
7
+ *
8
+ * The message is the caller's, as `timeZone`'s is: the i18n extractor harvests a plain zod
9
+ * message where it is written. `whenEmpty` is separate for a form that distinguishes a field
10
+ * nobody filled in from one filled in wrongly; left out, one message answers for both.
11
+ */
12
+ function languageCode(message, whenEmpty = message) {
13
+ return z.string().min(1, whenEmpty).refine(isLanguageCode, message);
14
+ }
15
+ export { languageCode };
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ /** Text in each language, by language code: `{ en: 'Sales', fr: 'Ventes' }`. */
3
+ export type LocalizedString = Record<string, string>;
4
+ export type LocalizedStringArray = Record<string, string[]>;
5
+ /**
6
+ * A localized value with nothing said about it: any record of language → text, blank included.
7
+ *
8
+ * For a stored model, where a row saved before a rule must still read. A field a person fills
9
+ * in wants `localizedString(messages)` instead, so a blank one is refused in words.
10
+ */
11
+ export declare const LocalizedString: z.ZodRecord<z.ZodString, z.ZodString>;
12
+ export declare const LocalizedStringArray: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
13
+ /** As {@link LocalizedString}, or a plain string — what a write model takes for a localized field. */
14
+ export declare const LocalizedStringInput: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
15
+ export declare const LocalizedStringArrayInput: z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>]>;
16
+ export type LocalizedStringArrayInput = z.infer<typeof LocalizedStringArrayInput>;
17
+ export type LocalizedStringInput = z.infer<typeof LocalizedStringInput>;
18
+ /** What a localized field says when it has no text, and when what came is not text at all. */
19
+ export interface LocalizedStringMessages {
20
+ invalid: string;
21
+ /** `false` when no text is an answer: absent, `null` and blank then all read as unset. */
22
+ missing: false | string;
23
+ }
24
+ /**
25
+ * Whether a localized value has text: a string that isn't blank, or text in at least one
26
+ * language. `undefined`, `null`, `''`, `' '`, `{}` and `{ en: '', fr: ' ' }` have none.
27
+ */
28
+ export declare function hasLocalizedText(value: unknown): boolean;
29
+ /** Whether a localized value is at most `max` characters, as a string or in every language it names. */
30
+ export declare function isLocalizedTextWithin(value: unknown, max: number): boolean;
31
+ /**
32
+ * A form's localized field — a record, one entry per language — that needs text in some language.
33
+ *
34
+ * The messages are the CALLER's, as `hexString24`'s are: "Name is required." belongs to the form
35
+ * asking for a name.
36
+ *
37
+ * One message answers for every case. `{ missing, invalid }` tells them apart: `missing` for no
38
+ * text at all — absent, `null`, or blank in every language — and `invalid` for a value that is
39
+ * there but is not text.
40
+ *
41
+ * `{ missing: false, invalid }` is the field that may be left empty: absent, `null` and blank
42
+ * pass through as they came, so the repository can tell a clear from a field left alone.
43
+ */
44
+ export declare function localizedString(messages?: string | {
45
+ invalid: string;
46
+ missing: string;
47
+ }): typeof LocalizedString;
48
+ export declare function localizedString(messages: {
49
+ invalid: string;
50
+ missing: false;
51
+ }): z.ZodOptional<z.ZodNullable<typeof LocalizedString>>;
52
+ /**
53
+ * A model's localized field — a record or a plain string — that needs text in some language, with
54
+ * {@link localizedString}'s forms and messages; on a `partial()` update model, whenever it is sent.
55
+ * The refine guards the endpoint only: repository writes skip refines.
56
+ */
57
+ export declare function localizedStringInput(messages?: string | {
58
+ invalid: string;
59
+ missing: string;
60
+ }): typeof LocalizedStringInput;
61
+ export declare function localizedStringInput(messages: {
62
+ invalid: string;
63
+ missing: false;
64
+ }): z.ZodOptional<z.ZodNullable<typeof LocalizedStringInput>>;
65
+ /**
66
+ * The write-model fields that let a required localized name through blank — for a guard spec to
67
+ * assert is empty.
68
+ *
69
+ * For every exported model `X` with an `AddX` or `UpdateX` beside it: a field `X` requires, and
70
+ * that takes both a string and a locale record, is a required localized name. Its write models
71
+ * must refuse `''` and `{ en: '' }`, which {@link localizedStringInput} does. Returns
72
+ * `UpdateRole.name`-style paths for each one that doesn't. Read from the schemas rather than the
73
+ * source, so it sees what a write actually accepts.
74
+ */
75
+ export declare function findUncheckedLocalizedFields(models: Record<string, unknown>): string[];
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ // src/localized-string.ts
3
+ /**
4
+ * A localized value with nothing said about it: any record of language → text, blank included.
5
+ *
6
+ * For a stored model, where a row saved before a rule must still read. A field a person fills
7
+ * in wants `localizedString(messages)` instead, so a blank one is refused in words.
8
+ */
9
+ const LocalizedString = z.record(z.string(), z.string());
10
+ const LocalizedStringArray = z.record(z.string(), z.array(z.string()));
11
+ /** As {@link LocalizedString}, or a plain string — what a write model takes for a localized field. */
12
+ const LocalizedStringInput = z.union([z.string(), LocalizedString]);
13
+ const LocalizedStringArrayInput = z.union([z.array(z.string()), LocalizedStringArray]);
14
+ /**
15
+ * Whether a localized value has text: a string that isn't blank, or text in at least one
16
+ * language. `undefined`, `null`, `''`, `' '`, `{}` and `{ en: '', fr: ' ' }` have none.
17
+ */
18
+ function hasLocalizedText(value) {
19
+ if (typeof value === "string") return value.trim().length > 0;
20
+ if (!value || typeof value !== "object") return false;
21
+ return Object.values(value).some((text) => typeof text === "string" && text.trim().length > 0);
22
+ }
23
+ /** Whether a localized value is at most `max` characters, as a string or in every language it names. */
24
+ function isLocalizedTextWithin(value, max) {
25
+ if (typeof value === "string") return value.length <= max;
26
+ if (!value || typeof value !== "object") return true;
27
+ return Object.values(value).every((text) => typeof text !== "string" || text.length <= max);
28
+ }
29
+ function withText(shape, messages) {
30
+ if (messages === void 0) return shape(void 0).refine(hasLocalizedText);
31
+ const { invalid, missing } = typeof messages === "string" ? {
32
+ invalid: messages,
33
+ missing: messages
34
+ } : messages;
35
+ if (missing === false) return shape(() => invalid).nullish();
36
+ const error = (issue) => issue.input === void 0 || issue.input === null ? missing : invalid;
37
+ return shape(error).refine(hasLocalizedText, { error: missing });
38
+ }
39
+ /** A locale record whose every error — the record's, or one language's text — says `error`. */
40
+ const record = (error) => z.record(z.string(), z.string({ error }), { error });
41
+ function localizedString(messages) {
42
+ return withText(record, messages);
43
+ }
44
+ function localizedStringInput(messages) {
45
+ return withText((error) => z.union([z.string({ error }), record(error)], { error }), messages);
46
+ }
47
+ const accepts = (schema, value) => schema.safeParse(value).success;
48
+ /**
49
+ * The write-model fields that let a required localized name through blank — for a guard spec to
50
+ * assert is empty.
51
+ *
52
+ * For every exported model `X` with an `AddX` or `UpdateX` beside it: a field `X` requires, and
53
+ * that takes both a string and a locale record, is a required localized name. Its write models
54
+ * must refuse `''` and `{ en: '' }`, which {@link localizedStringInput} does. Returns
55
+ * `UpdateRole.name`-style paths for each one that doesn't. Read from the schemas rather than the
56
+ * source, so it sees what a write actually accepts.
57
+ */
58
+ function findUncheckedLocalizedFields(models) {
59
+ const unchecked = [];
60
+ for (const [name, model] of Object.entries(models)) {
61
+ if (!(model instanceof z.ZodObject)) continue;
62
+ const shape = model.shape;
63
+ const required = Object.keys(shape).filter((key) => {
64
+ const field = shape[key];
65
+ return !accepts(field, void 0) && accepts(field, "Name") && accepts(field, { en: "Name" });
66
+ });
67
+ if (required.length === 0) continue;
68
+ for (const writeName of [`Add${name}`, `Update${name}`]) {
69
+ const write = models[writeName];
70
+ if (!(write instanceof z.ZodObject)) continue;
71
+ const writeShape = write.shape;
72
+ for (const key of required) {
73
+ const field = writeShape[key];
74
+ if (field && (accepts(field, "") || accepts(field, { en: "" }))) unchecked.push(`${writeName}.${key}`);
75
+ }
76
+ }
77
+ }
78
+ return unchecked.sort();
79
+ }
80
+ export { LocalizedString, LocalizedStringArray, LocalizedStringArrayInput, LocalizedStringInput, findUncheckedLocalizedFields, hasLocalizedText, isLocalizedTextWithin, localizedString, localizedStringInput };
@@ -0,0 +1,14 @@
1
+ import { ZodObject, ZodRawShape, ZodType, ZodUnion } from "zod";
2
+ /**
3
+ * Recursively merges two ZodRawShape objects into a single shape,
4
+ * combining overlapping keys into unions where applicable.
5
+ */
6
+ export type ZodMergeShapes<T extends ZodRawShape, U extends ZodRawShape> = { [K in keyof T | keyof U]: K extends keyof T & keyof U ? T[K] extends ZodObject<ZodRawShape> ? U[K] extends ZodObject<ZodRawShape> ? ZodObject<ZodMergeShapes<T[K]['shape'], U[K]['shape']>> : T[K] | U[K] : T[K] | U[K] : K extends keyof T ? T[K] : K extends keyof U ? U[K] : never; };
7
+ /**
8
+ * Adds a Zod type to an existing ZodUnion.
9
+ */
10
+ export declare function addToUnion<TUnion extends ZodUnion, TAny extends ZodType>(union: TUnion, object: TAny): ZodUnion;
11
+ /**
12
+ * Deeply merges two ZodRawShape objects into one.
13
+ */
14
+ export declare function mergeShapes<T extends ZodRawShape, U extends ZodRawShape>(shapeA: T, shapeB: U): ZodMergeShapes<T, U>;
package/dist/merge.js ADDED
@@ -0,0 +1,26 @@
1
+ import { isZodObject } from "./utilities.js";
2
+ import { z } from "zod";
3
+ // src/merge.ts
4
+ /**
5
+ * Adds a Zod type to an existing ZodUnion.
6
+ */
7
+ function addToUnion(union, object) {
8
+ return z.union([object, ...union.options]);
9
+ }
10
+ /**
11
+ * Deeply merges two ZodRawShape objects into one.
12
+ */
13
+ function mergeShapes(shapeA, shapeB) {
14
+ const mergedShape = { ...shapeA };
15
+ for (const key in shapeB) if (key in mergedShape) {
16
+ const valueA = mergedShape[key];
17
+ const valueB = shapeB[key];
18
+ if (isZodObject(valueA) && isZodObject(valueB)) {
19
+ const shapeInnerA = valueA.shape;
20
+ const shapeInnerB = valueB.shape;
21
+ mergedShape[key] = z.object(mergeShapes(shapeInnerA, shapeInnerB));
22
+ } else mergedShape[key] = z.union([valueA, valueB]);
23
+ } else mergedShape[key] = shapeB[key];
24
+ return mergedShape;
25
+ }
26
+ export { addToUnion, mergeShapes };
@@ -0,0 +1,2 @@
1
+ import { z } from "zod";
2
+ export declare function nullish<TShape extends Record<string, z.ZodType>>(fields: z.ZodObject<TShape>): z.ZodObject<{ -readonly [P in keyof { [TKey in keyof TShape]: z.ZodOptional<z.ZodNullable<TShape[TKey]>>; }]: { [TKey in keyof TShape]: z.ZodOptional<z.ZodNullable<TShape[TKey]>>; }[P]; }, z.core.$strip>;
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ import { mapValues } from "@temelyos/core/object";
3
+ // src/nullish.ts
4
+ function nullish(fields) {
5
+ return z.object(mapValues(fields.shape, (field) => field.nullish()));
6
+ }
7
+ export { nullish };
@@ -0,0 +1,22 @@
1
+ import { ZodType } from "zod";
2
+ /**
3
+ * Whether {@link toFieldPath} can follow `path` through `schema`: every segment is reached through objects and
4
+ * arrays (and the wrappers and unions around them), with no pipe, transform, intersection, tuple, record or loose
5
+ * (`any`, `unknown`, `custom`) node before the last. The path may end at one.
6
+ */
7
+ export declare function isFollowablePath(schema: ZodType, path: string): boolean;
8
+ /**
9
+ * `path` with `.$[]` after every segment that `schema` types as an array, except the last:
10
+ * `settings.pages.defaultAccessLevels` -> `settings.pages.$[].defaultAccessLevels`. Returns
11
+ * `path` unchanged when no segment before the last is an array.
12
+ *
13
+ * At a union, every arm is followed; a segment is an array when any arm types it as one.
14
+ */
15
+ export declare function toAllPositionalPath(schema: ZodType, path: string): string;
16
+ /**
17
+ * The field path `path` names in `schema`, its array positions left out:
18
+ * `lines.0.attachments.$[].title` -> `lines.attachments.title`. A numeric segment is dropped
19
+ * only where `schema` has an array, so a record's key stays: `budgets.2026` is unchanged.
20
+ * `$`, `$[]` and `$[id]` are always positions.
21
+ */
22
+ export declare function toFieldPath(schema: ZodType, path: string): string;
@@ -0,0 +1,99 @@
1
+ import { unwrapField } from "./field-paths.js";
2
+ import { isZodArray, isZodObject, isZodRecord, isZodType, isZodUnion } from "./utilities.js";
3
+ import { isInteger } from "@temelyos/core/number";
4
+ // src/positional-paths.ts
5
+ /** What `segment` names under `node`, looking through arrays the way Mongo's dotted paths do. */
6
+ function childrenOf(node, segment) {
7
+ if (isZodArray(node)) return flatten(node.def.element).flatMap((element) => childrenOf(element, segment));
8
+ if (isZodObject(node)) return node.shape[segment] ? flatten(node.shape[segment]) : [];
9
+ if (isZodRecord(node) && isZodType(node.def.valueType)) return flatten(node.def.valueType);
10
+ return [];
11
+ }
12
+ function flatten(node) {
13
+ const unwrapped = unwrapField(node, true);
14
+ return isZodUnion(unwrapped) ? unwrapped.options.flatMap(flatten) : [unwrapped];
15
+ }
16
+ /** Nodes a path can end at but not pass through: what lies past them can't be named from the schema. */
17
+ const opaqueTypes = /* @__PURE__ */ new Set([
18
+ "any",
19
+ "custom",
20
+ "intersection",
21
+ "pipe",
22
+ "record",
23
+ "transform",
24
+ "tuple",
25
+ "unknown"
26
+ ]);
27
+ /** What `segment` names under `node` on the way to a deeper field, or `undefined` where the schema can't be followed. */
28
+ function followedChildren(node, segment) {
29
+ if (opaqueTypes.has(node.def.type)) return void 0;
30
+ if (isZodObject(node)) return node.shape[segment] ? flatten(node.shape[segment]) : [];
31
+ if (!isZodArray(node)) return [];
32
+ const children = flatten(node.def.element).map((element) => followedChildren(element, segment));
33
+ return children.some((found) => found === void 0) ? void 0 : children.flatMap((found) => found ?? []);
34
+ }
35
+ /**
36
+ * Whether {@link toFieldPath} can follow `path` through `schema`: every segment is reached through objects and
37
+ * arrays (and the wrappers and unions around them), with no pipe, transform, intersection, tuple, record or loose
38
+ * (`any`, `unknown`, `custom`) node before the last. The path may end at one.
39
+ */
40
+ function isFollowablePath(schema, path) {
41
+ let nodes = flatten(schema);
42
+ for (const segment of path.split(".")) {
43
+ const next = [];
44
+ for (const node of nodes) {
45
+ const children = followedChildren(node, segment);
46
+ if (children === void 0) return false;
47
+ next.push(...children);
48
+ }
49
+ if (next.length === 0) return false;
50
+ nodes = next;
51
+ }
52
+ return true;
53
+ }
54
+ /**
55
+ * `path` with `.$[]` after every segment that `schema` types as an array, except the last:
56
+ * `settings.pages.defaultAccessLevels` -> `settings.pages.$[].defaultAccessLevels`. Returns
57
+ * `path` unchanged when no segment before the last is an array.
58
+ *
59
+ * At a union, every arm is followed; a segment is an array when any arm types it as one.
60
+ */
61
+ function toAllPositionalPath(schema, path) {
62
+ const segments = path.split(".");
63
+ const result = [];
64
+ let nodes = flatten(schema);
65
+ for (const [index, segment] of segments.entries()) {
66
+ const children = nodes.flatMap((node) => {
67
+ const child = isZodObject(node) ? node.shape[segment] : void 0;
68
+ return child ? flatten(child) : [];
69
+ });
70
+ const arrays = children.filter(isZodArray);
71
+ result.push(segment);
72
+ if (arrays.length > 0 && index < segments.length - 1) {
73
+ result.push("$[]");
74
+ nodes = arrays.flatMap((array) => flatten(array.def.element));
75
+ } else nodes = children;
76
+ }
77
+ return result.join(".");
78
+ }
79
+ /**
80
+ * The field path `path` names in `schema`, its array positions left out:
81
+ * `lines.0.attachments.$[].title` -> `lines.attachments.title`. A numeric segment is dropped
82
+ * only where `schema` has an array, so a record's key stays: `budgets.2026` is unchanged.
83
+ * `$`, `$[]` and `$[id]` are always positions.
84
+ */
85
+ function toFieldPath(schema, path) {
86
+ const result = [];
87
+ let nodes = flatten(schema);
88
+ for (const segment of path.split(".")) {
89
+ const arrays = nodes.filter(isZodArray);
90
+ if (segment.startsWith("$") || isInteger(segment) && arrays.length > 0) {
91
+ if (arrays.length > 0) nodes = arrays.flatMap((array) => flatten(array.def.element));
92
+ continue;
93
+ }
94
+ result.push(segment);
95
+ nodes = nodes.flatMap((node) => childrenOf(node, segment));
96
+ }
97
+ return result.join(".");
98
+ }
99
+ export { isFollowablePath, toAllPositionalPath, toFieldPath };
@@ -0,0 +1,2 @@
1
+ import { z } from "zod";
2
+ export declare const RegExpPattern: z.ZodString;
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ import { isValidPattern } from "@temelyos/core/reg-exp";
3
+ // src/reg-exp-pattern.ts
4
+ const RegExpPattern = z.string().refine(isValidPattern, "Invalid regular expression.");
5
+ export { RegExpPattern };
package/dist/slug.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ /** 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. */
3
+ export declare function slug(missing?: string): z.ZodString;
4
+ /** A slug with nothing said about an empty value beyond what any other wrong value gets. */
5
+ export declare const Slug: z.ZodString;
package/dist/slug.js ADDED
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { SlugPattern } from "@temelyos/core/string";
3
+ // src/slug.ts
4
+ /** What a value that is not a slug says. An `error` property so the i18n extractor keeps the key. */
5
+ const invalidSlug = { error: "Use lower-case letters, numbers and dashes." };
6
+ /** 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. */
7
+ function slug(missing = invalidSlug.error) {
8
+ return z.string().min(1, {
9
+ abort: true,
10
+ error: missing
11
+ }).regex(SlugPattern, invalidSlug.error);
12
+ }
13
+ /** A slug with nothing said about an empty value beyond what any other wrong value gets. */
14
+ const Slug = slug();
15
+ export { Slug, slug };
@@ -0,0 +1,2 @@
1
+ import { ZodType, z } from "zod";
2
+ export declare function stripEmptyFields<T extends ZodType>(schema: T, values: z.input<T>): z.infer<T>;
@@ -0,0 +1,46 @@
1
+ import { getInnerType, isFieldOptional, isZodObject } from "./utilities.js";
2
+ import { ZodLiteral } from "zod";
3
+ import { isPlainObject } from "@temelyos/core/object";
4
+ // src/strip-empty-fields.ts
5
+ function isEmpty(value) {
6
+ if (value === void 0 || value === "") return true;
7
+ if (Array.isArray(value)) return value.length === 0;
8
+ return isPlainObject(value) && Object.keys(value).length === 0;
9
+ }
10
+ function resolveShape(schema, values) {
11
+ if (isZodObject(schema)) return schema.shape;
12
+ const options = schema.def.options;
13
+ if (Array.isArray(options)) for (const option of options) {
14
+ if (!isZodObject(option)) continue;
15
+ const shape = option.shape;
16
+ if (Object.entries(shape).every(([key, fieldSchema]) => !(fieldSchema instanceof ZodLiteral) || fieldSchema.def.values.some((literal) => literal === values[key]))) return shape;
17
+ }
18
+ }
19
+ function stripEmptyFields(schema, values) {
20
+ return stripEmptyFieldsRecursive(schema, values, /* @__PURE__ */ new Set());
21
+ }
22
+ function stripEmptyFieldsRecursive(schema, values, seen) {
23
+ const shape = resolveShape(schema, values);
24
+ if (!shape) return values;
25
+ const result = { ...values };
26
+ for (const key of Object.keys(result)) {
27
+ const fieldSchema = shape[key];
28
+ if (!fieldSchema) continue;
29
+ const value = result[key];
30
+ if (isFieldOptional(fieldSchema) && isEmpty(value)) {
31
+ delete result[key];
32
+ continue;
33
+ }
34
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
35
+ const innerType = getInnerType(fieldSchema);
36
+ if (isZodObject(innerType) && !seen.has(value)) {
37
+ seen.add(value);
38
+ result[key] = stripEmptyFieldsRecursive(innerType, value, seen);
39
+ seen.delete(value);
40
+ if (isFieldOptional(fieldSchema) && isEmpty(result[key])) delete result[key];
41
+ }
42
+ }
43
+ }
44
+ return result;
45
+ }
46
+ export { stripEmptyFields };
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * `value` cut down to the fields `schema` declares, at every depth: through arrays, optionals, nullables, defaults,
4
+ * catches, lazies and the objects of a union or an intersection, merged. Nothing is parsed, so no checks, codecs or defaults run, and a
5
+ * value that doesn't fit its field is copied as it is. `value` itself is picked by its keys, whatever its prototype.
6
+ */
7
+ export declare function stripToSchema<T extends z.ZodObject>(schema: T, value: object): z.output<T>;