@prairielearn/zod 1.4.2 → 1.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @prairielearn/zod
2
2
 
3
+ ## 1.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 36b46ef: Add `UniqueUidsFromStringSchema` and `DatetimeLocalStringSchema`
8
+
9
+ ### Patch Changes
10
+
11
+ - 70a8029: Upgrade all JavaScript dependencies
12
+
13
+ ## 1.4.3
14
+
15
+ ### Patch Changes
16
+
17
+ - 0425922: Upgrade all JavaScript dependencies
18
+
3
19
  ## 1.4.2
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -14,3 +14,22 @@ BooleanFromCheckboxSchema.parse('true'); // true
14
14
  BooleanFromCheckboxSchema.parse('1'); // true
15
15
  BooleanFromCheckboxSchema.parse('on'); // true
16
16
  ```
17
+
18
+ ### `DatetimeLocalStringSchema`
19
+
20
+ ```ts
21
+ import { DatetimeLocalStringSchema } from '@prairielearn/zod';
22
+
23
+ DatetimeLocalStringSchema.parse('2024-01-15T14:30'); // '2024-01-15T14:30:00'
24
+ DatetimeLocalStringSchema.parse('2024-01-15T14:30:45'); // '2024-01-15T14:30:45'
25
+ ```
26
+
27
+ ### `UniqueUidsFromStringSchema`
28
+
29
+ ```ts
30
+ import { UniqueUidsFromStringSchema } from '@prairielearn/zod';
31
+
32
+ const schema = UniqueUidsFromStringSchema();
33
+ schema.parse('user1@example.com, user2@example.com'); // ['user1@example.com', 'user2@example.com']
34
+ schema.parse('user@example.com user@example.com'); // ['user@example.com'] (deduplicated)
35
+ ```
package/dist/index.d.ts CHANGED
@@ -85,6 +85,15 @@ export declare const IntervalSchema: z.ZodEffects<z.ZodUnion<[z.ZodString, z.Zod
85
85
  * Useful for parsing dates from JSON, which are always strings.
86
86
  */
87
87
  export declare const DateFromISOString: z.ZodEffects<z.ZodEffects<z.ZodUnion<[z.ZodString, z.ZodDate]>, string | Date, string | Date>, Date, string | Date>;
88
+ /**
89
+ * A Zod schema for a datetime-local input value.
90
+ *
91
+ * Accepts a string in the format "YYYY-MM-DDTHH:MM" or "YYYY-MM-DDTHH:MM:SS"
92
+ * as produced by `<input type="datetime-local">` elements.
93
+ *
94
+ * Validates the format and returns it as a string.
95
+ */
96
+ export declare const DatetimeLocalStringSchema: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
88
97
  /**
89
98
  * A Zod schema that coerces a non-empty string to an integer or an empty string to null.
90
99
  * This is useful for form number inputs that are not required but we do not want to
@@ -105,5 +114,13 @@ export declare const ArrayFromStringOrArraySchema: z.ZodEffects<z.ZodUnion<[z.Zo
105
114
  * middleware that parses the submitted data into a string or array.
106
115
  */
107
116
  export declare const ArrayFromCheckboxSchema: z.ZodEffects<z.ZodUnion<[z.ZodUndefined, z.ZodString, z.ZodArray<z.ZodString, "many">]>, string[], string | string[] | undefined>;
117
+ /**
118
+ * Creates a Zod schema that parses a string of UIDs separated by whitespace,
119
+ * commas, or semicolons into an array of unique, trimmed UIDs.
120
+ *
121
+ * @param limit - The maximum number of UIDs allowed. Defaults to 1000.
122
+ * @returns A Zod schema that parses and validates the UID string.
123
+ */
124
+ export declare function UniqueUidsFromStringSchema(limit?: number): z.ZodEffects<z.ZodString, string[], string>;
108
125
  export {};
109
126
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASzC;;GAEG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,UAAU,IAAI,CAAC,GAAG;IAC1C,QAAQ,EAAE,KAAK,CAAC;CACjB,CAAC;AAUF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,mFAKrC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,2CAEiE,CAAC;AAevF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBvB,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,qHAWE,CAAC;AAEjC;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,4EAG1C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,uGAUrC,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,mIAUhC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASzC;;GAEG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,UAAU,IAAI,CAAC,GAAG;IAC1C,QAAQ,EAAE,KAAK,CAAC;CACjB,CAAC;AAUF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,mFAKrC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,2CAEiE,CAAC;AAevF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBvB,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,qHAWE,CAAC;AAEjC;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,yEAmBlC,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,4EAG1C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,uGAUrC,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,mIAUhC,CAAC;AAEL;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,SAAO,+CA0CtD"}
package/dist/index.js CHANGED
@@ -106,6 +106,34 @@ export const DateFromISOString = z
106
106
  message: 'must be a valid ISO date string',
107
107
  })
108
108
  .transform((s) => new Date(s));
109
+ /**
110
+ * A Zod schema for a datetime-local input value.
111
+ *
112
+ * Accepts a string in the format "YYYY-MM-DDTHH:MM" or "YYYY-MM-DDTHH:MM:SS"
113
+ * as produced by `<input type="datetime-local">` elements.
114
+ *
115
+ * Validates the format and returns it as a string.
116
+ */
117
+ export const DatetimeLocalStringSchema = z
118
+ .string()
119
+ .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/, {
120
+ message: 'must be a valid datetime-local string (YYYY-MM-DDTHH:MM or YYYY-MM-DDTHH:MM:SS)',
121
+ })
122
+ // Append `:00` seconds if omitted (Chrome bug workaround).
123
+ // https://stackoverflow.com/questions/19504018/show-seconds-on-input-type-date-local-in-chrome
124
+ // https://issues.chromium.org/issues/41159420
125
+ .transform((s) => (s.length === 16 ? `${s}:00` : s))
126
+ .transform((s, ctx) => {
127
+ const date = new Date(s);
128
+ if (Number.isNaN(date.getTime())) {
129
+ ctx.addIssue({
130
+ code: z.ZodIssueCode.custom,
131
+ message: 'must be a valid date',
132
+ });
133
+ return z.NEVER;
134
+ }
135
+ return s;
136
+ });
109
137
  /**
110
138
  * A Zod schema that coerces a non-empty string to an integer or an empty string to null.
111
139
  * This is useful for form number inputs that are not required but we do not want to
@@ -150,4 +178,47 @@ export const ArrayFromCheckboxSchema = z
150
178
  return [s];
151
179
  }
152
180
  });
181
+ /**
182
+ * Creates a Zod schema that parses a string of UIDs separated by whitespace,
183
+ * commas, or semicolons into an array of unique, trimmed UIDs.
184
+ *
185
+ * @param limit - The maximum number of UIDs allowed. Defaults to 1000.
186
+ * @returns A Zod schema that parses and validates the UID string.
187
+ */
188
+ export function UniqueUidsFromStringSchema(limit = 1000) {
189
+ const emailSchema = z.string().email();
190
+ return z.string().transform((uidsString, ctx) => {
191
+ const uids = new Set(uidsString
192
+ .split(/[\s,;]+/)
193
+ .map((uid) => uid.trim())
194
+ .filter(Boolean));
195
+ if (uids.size > limit) {
196
+ ctx.addIssue({
197
+ code: z.ZodIssueCode.too_big,
198
+ maximum: limit,
199
+ type: 'set',
200
+ inclusive: true,
201
+ message: `Cannot provide more than ${limit} UIDs at a time`,
202
+ });
203
+ return z.NEVER;
204
+ }
205
+ if (uids.size === 0) {
206
+ ctx.addIssue({
207
+ code: z.ZodIssueCode.custom,
208
+ message: 'At least one UID is required',
209
+ });
210
+ return z.NEVER;
211
+ }
212
+ for (const uid of uids) {
213
+ const result = emailSchema.safeParse(uid);
214
+ if (!result.success) {
215
+ ctx.addIssue({
216
+ code: z.ZodIssueCode.custom,
217
+ message: `Invalid UID format: ${uid}`,
218
+ });
219
+ }
220
+ }
221
+ return Array.from(uids);
222
+ });
223
+ }
153
224
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import parsePostgresInterval from 'postgres-interval';\nimport { type ZodTypeAny, z } from 'zod';\n\nconst INTERVAL_MS_PER_SECOND = 1000;\nconst INTERVAL_MS_PER_MINUTE = 60 * INTERVAL_MS_PER_SECOND;\nconst INTERVAL_MS_PER_HOUR = 60 * INTERVAL_MS_PER_MINUTE;\nconst INTERVAL_MS_PER_DAY = 24 * INTERVAL_MS_PER_HOUR;\nconst INTERVAL_MS_PER_MONTH = 30 * INTERVAL_MS_PER_DAY;\nconst INTERVAL_MS_PER_YEAR = 365.25 * INTERVAL_MS_PER_DAY;\n\n/**\n * A schema type on which `.optional()` cannot be called.\n */\ntype NoOptional<S extends ZodTypeAny> = S & {\n optional: never;\n};\n\n/**\n * Wrap any Zod schema so that calling `.optional()` is illegal in TypeScript.\n * Runtime behavior is untouched.\n */\nfunction required<S extends ZodTypeAny>(schema: S): NoOptional<S> {\n return schema as unknown as NoOptional<S>;\n}\n\n/**\n * A Zod schema for a boolean from a single checkbox input in the body\n * parameters from a form. This will return a boolean with a value of `true` if\n * the checkbox is checked (the input is present) and `false` if it is not\n * checked.\n *\n * Note that this will not behave sensibly if `.optional()` is called on the schema,\n * as it will turn a missing checkbox into `undefined` instead of `false`. We use\n * some TypeScript magic to ensure that `.optional()` cannot be called on this schema.\n */\nexport const BooleanFromCheckboxSchema = required(\n z\n .string()\n .optional()\n .transform((s) => !!s),\n);\n\n/**\n * A Zod schema for a PostgreSQL ID.\n *\n * We store IDs as BIGINT in PostgreSQL, which are passed to JavaScript as\n * either strings (if the ID is fetched directly) or numbers (if passed via\n * `to_jsonb()`). This schema coerces the ID to a string to ensure consistent\n * handling.\n *\n * The `refine` step is important to ensure that the thing we've coerced to a\n * string is actually a number. If it's not, we want to fail quickly.\n */\nexport const IdSchema = z\n .string({ coerce: true })\n .refine((val) => /^\\d+$/.test(val), { message: 'ID is not a non-negative integer' });\n\n/**\n * A Zod schema for the objects produced by the `postgres-interval` library.\n */\nconst PostgresIntervalSchema = z.object({\n years: z.number().default(0),\n months: z.number().default(0),\n days: z.number().default(0),\n hours: z.number().default(0),\n minutes: z.number().default(0),\n seconds: z.number().default(0),\n milliseconds: z.number().default(0),\n});\n\n/**\n * A Zod schema for a PostgreSQL interval.\n *\n * This handles three representations of an interval:\n *\n * - A string like \"1 year 2 days\", which is how intervals will be represented\n * if they go through `to_jsonb` in a query.\n * - A {@link PostgresIntervalSchema} object, which is what we'll get if a\n * query directly returns an interval column. The interval will already be\n * parsed by `postgres-interval` by way of `pg-types`.\n * - A number of milliseconds, which is possible if you want to feed the output of IntervalSchema.parse()\n * back through this schema.\n *\n * In all cases, we convert the interval to a number of milliseconds.\n */\nexport const IntervalSchema = z\n .union([z.string(), PostgresIntervalSchema, z.number()])\n .transform((interval) => {\n if (typeof interval === 'string') {\n interval = parsePostgresInterval(interval);\n }\n\n if (typeof interval === 'number') {\n return interval;\n }\n\n // This calculation matches Postgres's behavior when computing the number of\n // milliseconds in an interval with `EXTRACT(epoch from '...'::interval) * 1000`.\n // The noteworthy parts of this conversion are that 1 year = 365.25 days and\n // 1 month = 30 days.\n return (\n interval.years * INTERVAL_MS_PER_YEAR +\n interval.months * INTERVAL_MS_PER_MONTH +\n interval.days * INTERVAL_MS_PER_DAY +\n interval.hours * INTERVAL_MS_PER_HOUR +\n interval.minutes * INTERVAL_MS_PER_MINUTE +\n interval.seconds * INTERVAL_MS_PER_SECOND +\n interval.milliseconds\n );\n });\n\n/**\n * A Zod schema for a date string in ISO format.\n *\n * Accepts either a string or a Date object. If a string is passed, it is\n * validated and parsed as an ISO date string.\n *\n * Useful for parsing dates from JSON, which are always strings.\n */\nexport const DateFromISOString = z\n .union([z.string(), z.date()])\n .refine(\n (s) => {\n const date = new Date(s);\n return !Number.isNaN(date.getTime());\n },\n {\n message: 'must be a valid ISO date string',\n },\n )\n .transform((s) => new Date(s));\n\n/**\n * A Zod schema that coerces a non-empty string to an integer or an empty string to null.\n * This is useful for form number inputs that are not required but we do not want to\n * use an empty string to compute values.\n */\nexport const IntegerFromStringOrEmptySchema = z.preprocess(\n (value) => (value === '' ? null : value),\n z.union([z.null(), z.coerce.number().int()]),\n);\n\n/**\n * A Zod schema for an array of string values from either a string or an array of\n * strings.\n */\nexport const ArrayFromStringOrArraySchema = z\n .union([z.string(), z.array(z.string())])\n .transform((s) => {\n if (s === null) {\n return [];\n } else if (Array.isArray(s)) {\n return s;\n } else {\n return [s];\n }\n });\n\n/**\n * A Zod schema for an array of string values from a set of checkboxes in the\n * body parameters from a form. The form should have checkboxes with the same\n * name attribute, and the value of the checkboxes should be the string values\n * to include in the array. If no checkboxes are checked, this will return an\n * empty array. This behavior relies on the ExpressJS `bodyParser.urlencoded()`\n * middleware that parses the submitted data into a string or array.\n */\nexport const ArrayFromCheckboxSchema = z\n .union([z.undefined(), z.string(), z.array(z.string())])\n .transform((s) => {\n if (s == null) {\n return [];\n } else if (Array.isArray(s)) {\n return s;\n } else {\n return [s];\n }\n });\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import parsePostgresInterval from 'postgres-interval';\nimport { type ZodTypeAny, z } from 'zod';\n\nconst INTERVAL_MS_PER_SECOND = 1000;\nconst INTERVAL_MS_PER_MINUTE = 60 * INTERVAL_MS_PER_SECOND;\nconst INTERVAL_MS_PER_HOUR = 60 * INTERVAL_MS_PER_MINUTE;\nconst INTERVAL_MS_PER_DAY = 24 * INTERVAL_MS_PER_HOUR;\nconst INTERVAL_MS_PER_MONTH = 30 * INTERVAL_MS_PER_DAY;\nconst INTERVAL_MS_PER_YEAR = 365.25 * INTERVAL_MS_PER_DAY;\n\n/**\n * A schema type on which `.optional()` cannot be called.\n */\ntype NoOptional<S extends ZodTypeAny> = S & {\n optional: never;\n};\n\n/**\n * Wrap any Zod schema so that calling `.optional()` is illegal in TypeScript.\n * Runtime behavior is untouched.\n */\nfunction required<S extends ZodTypeAny>(schema: S): NoOptional<S> {\n return schema as unknown as NoOptional<S>;\n}\n\n/**\n * A Zod schema for a boolean from a single checkbox input in the body\n * parameters from a form. This will return a boolean with a value of `true` if\n * the checkbox is checked (the input is present) and `false` if it is not\n * checked.\n *\n * Note that this will not behave sensibly if `.optional()` is called on the schema,\n * as it will turn a missing checkbox into `undefined` instead of `false`. We use\n * some TypeScript magic to ensure that `.optional()` cannot be called on this schema.\n */\nexport const BooleanFromCheckboxSchema = required(\n z\n .string()\n .optional()\n .transform((s) => !!s),\n);\n\n/**\n * A Zod schema for a PostgreSQL ID.\n *\n * We store IDs as BIGINT in PostgreSQL, which are passed to JavaScript as\n * either strings (if the ID is fetched directly) or numbers (if passed via\n * `to_jsonb()`). This schema coerces the ID to a string to ensure consistent\n * handling.\n *\n * The `refine` step is important to ensure that the thing we've coerced to a\n * string is actually a number. If it's not, we want to fail quickly.\n */\nexport const IdSchema = z\n .string({ coerce: true })\n .refine((val) => /^\\d+$/.test(val), { message: 'ID is not a non-negative integer' });\n\n/**\n * A Zod schema for the objects produced by the `postgres-interval` library.\n */\nconst PostgresIntervalSchema = z.object({\n years: z.number().default(0),\n months: z.number().default(0),\n days: z.number().default(0),\n hours: z.number().default(0),\n minutes: z.number().default(0),\n seconds: z.number().default(0),\n milliseconds: z.number().default(0),\n});\n\n/**\n * A Zod schema for a PostgreSQL interval.\n *\n * This handles three representations of an interval:\n *\n * - A string like \"1 year 2 days\", which is how intervals will be represented\n * if they go through `to_jsonb` in a query.\n * - A {@link PostgresIntervalSchema} object, which is what we'll get if a\n * query directly returns an interval column. The interval will already be\n * parsed by `postgres-interval` by way of `pg-types`.\n * - A number of milliseconds, which is possible if you want to feed the output of IntervalSchema.parse()\n * back through this schema.\n *\n * In all cases, we convert the interval to a number of milliseconds.\n */\nexport const IntervalSchema = z\n .union([z.string(), PostgresIntervalSchema, z.number()])\n .transform((interval) => {\n if (typeof interval === 'string') {\n interval = parsePostgresInterval(interval);\n }\n\n if (typeof interval === 'number') {\n return interval;\n }\n\n // This calculation matches Postgres's behavior when computing the number of\n // milliseconds in an interval with `EXTRACT(epoch from '...'::interval) * 1000`.\n // The noteworthy parts of this conversion are that 1 year = 365.25 days and\n // 1 month = 30 days.\n return (\n interval.years * INTERVAL_MS_PER_YEAR +\n interval.months * INTERVAL_MS_PER_MONTH +\n interval.days * INTERVAL_MS_PER_DAY +\n interval.hours * INTERVAL_MS_PER_HOUR +\n interval.minutes * INTERVAL_MS_PER_MINUTE +\n interval.seconds * INTERVAL_MS_PER_SECOND +\n interval.milliseconds\n );\n });\n\n/**\n * A Zod schema for a date string in ISO format.\n *\n * Accepts either a string or a Date object. If a string is passed, it is\n * validated and parsed as an ISO date string.\n *\n * Useful for parsing dates from JSON, which are always strings.\n */\nexport const DateFromISOString = z\n .union([z.string(), z.date()])\n .refine(\n (s) => {\n const date = new Date(s);\n return !Number.isNaN(date.getTime());\n },\n {\n message: 'must be a valid ISO date string',\n },\n )\n .transform((s) => new Date(s));\n\n/**\n * A Zod schema for a datetime-local input value.\n *\n * Accepts a string in the format \"YYYY-MM-DDTHH:MM\" or \"YYYY-MM-DDTHH:MM:SS\"\n * as produced by `<input type=\"datetime-local\">` elements.\n *\n * Validates the format and returns it as a string.\n */\nexport const DatetimeLocalStringSchema = z\n .string()\n .regex(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2})?$/, {\n message: 'must be a valid datetime-local string (YYYY-MM-DDTHH:MM or YYYY-MM-DDTHH:MM:SS)',\n })\n // Append `:00` seconds if omitted (Chrome bug workaround).\n // https://stackoverflow.com/questions/19504018/show-seconds-on-input-type-date-local-in-chrome\n // https://issues.chromium.org/issues/41159420\n .transform((s) => (s.length === 16 ? `${s}:00` : s))\n .transform((s, ctx) => {\n const date = new Date(s);\n if (Number.isNaN(date.getTime())) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'must be a valid date',\n });\n return z.NEVER;\n }\n return s;\n });\n\n/**\n * A Zod schema that coerces a non-empty string to an integer or an empty string to null.\n * This is useful for form number inputs that are not required but we do not want to\n * use an empty string to compute values.\n */\nexport const IntegerFromStringOrEmptySchema = z.preprocess(\n (value) => (value === '' ? null : value),\n z.union([z.null(), z.coerce.number().int()]),\n);\n\n/**\n * A Zod schema for an array of string values from either a string or an array of\n * strings.\n */\nexport const ArrayFromStringOrArraySchema = z\n .union([z.string(), z.array(z.string())])\n .transform((s) => {\n if (s === null) {\n return [];\n } else if (Array.isArray(s)) {\n return s;\n } else {\n return [s];\n }\n });\n\n/**\n * A Zod schema for an array of string values from a set of checkboxes in the\n * body parameters from a form. The form should have checkboxes with the same\n * name attribute, and the value of the checkboxes should be the string values\n * to include in the array. If no checkboxes are checked, this will return an\n * empty array. This behavior relies on the ExpressJS `bodyParser.urlencoded()`\n * middleware that parses the submitted data into a string or array.\n */\nexport const ArrayFromCheckboxSchema = z\n .union([z.undefined(), z.string(), z.array(z.string())])\n .transform((s) => {\n if (s == null) {\n return [];\n } else if (Array.isArray(s)) {\n return s;\n } else {\n return [s];\n }\n });\n\n/**\n * Creates a Zod schema that parses a string of UIDs separated by whitespace,\n * commas, or semicolons into an array of unique, trimmed UIDs.\n *\n * @param limit - The maximum number of UIDs allowed. Defaults to 1000.\n * @returns A Zod schema that parses and validates the UID string.\n */\nexport function UniqueUidsFromStringSchema(limit = 1000) {\n const emailSchema = z.string().email();\n\n return z.string().transform((uidsString, ctx) => {\n const uids = new Set(\n uidsString\n .split(/[\\s,;]+/)\n .map((uid) => uid.trim())\n .filter(Boolean),\n );\n\n if (uids.size > limit) {\n ctx.addIssue({\n code: z.ZodIssueCode.too_big,\n maximum: limit,\n type: 'set',\n inclusive: true,\n message: `Cannot provide more than ${limit} UIDs at a time`,\n });\n return z.NEVER;\n }\n\n if (uids.size === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'At least one UID is required',\n });\n return z.NEVER;\n }\n\n for (const uid of uids) {\n const result = emailSchema.safeParse(uid);\n if (!result.success) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: `Invalid UID format: ${uid}`,\n });\n }\n }\n\n return Array.from(uids);\n });\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import parsePostgresInterval from 'postgres-interval';
2
2
  import { assert, describe, it } from 'vitest';
3
- import { ArrayFromCheckboxSchema, ArrayFromStringOrArraySchema, BooleanFromCheckboxSchema, IdSchema, IntegerFromStringOrEmptySchema, IntervalSchema, } from './index.js';
3
+ import { ArrayFromCheckboxSchema, ArrayFromStringOrArraySchema, BooleanFromCheckboxSchema, DatetimeLocalStringSchema, IdSchema, IntegerFromStringOrEmptySchema, IntervalSchema, UniqueUidsFromStringSchema, } from './index.js';
4
4
  describe('BooleanFromCheckboxSchema', () => {
5
5
  it('parses a checked checkbox', () => {
6
6
  const result = BooleanFromCheckboxSchema.parse('on');
@@ -79,6 +79,32 @@ describe('IntervalSchema', () => {
79
79
  assert.equal(interval, -37015506789);
80
80
  });
81
81
  });
82
+ describe('DatetimeLocalStringSchema', () => {
83
+ it('parses a valid datetime-local string without seconds', () => {
84
+ const result = DatetimeLocalStringSchema.parse('2024-01-15T14:30');
85
+ assert.equal(result, '2024-01-15T14:30:00');
86
+ });
87
+ it('parses a valid datetime-local string with seconds', () => {
88
+ const result = DatetimeLocalStringSchema.parse('2024-01-15T14:30:45');
89
+ assert.equal(result, '2024-01-15T14:30:45');
90
+ });
91
+ it('rejects an invalid format (missing time)', () => {
92
+ const result = DatetimeLocalStringSchema.safeParse('2024-01-15');
93
+ assert.isFalse(result.success);
94
+ });
95
+ it('rejects an invalid format (ISO with timezone)', () => {
96
+ const result = DatetimeLocalStringSchema.safeParse('2024-01-15T14:30:00Z');
97
+ assert.isFalse(result.success);
98
+ });
99
+ it('rejects an empty string', () => {
100
+ const result = DatetimeLocalStringSchema.safeParse('');
101
+ assert.isFalse(result.success);
102
+ });
103
+ it('rejects an invalid date', () => {
104
+ const result = DatetimeLocalStringSchema.safeParse('2024-13-45T25:99');
105
+ assert.isFalse(result.success);
106
+ });
107
+ });
82
108
  describe('IntegerFromStringOrEmptySchema', () => {
83
109
  it('parses a valid integer string', () => {
84
110
  const result = IntegerFromStringOrEmptySchema.parse('123');
@@ -129,4 +155,44 @@ describe('ArrayFromCheckboxSchema', () => {
129
155
  assert.deepEqual(result, ['a', 'b', 'c']);
130
156
  });
131
157
  });
158
+ describe('UniqueUidsFromStringSchema', () => {
159
+ it('parses a single UID', () => {
160
+ const result = UniqueUidsFromStringSchema(10).parse('user@example.com');
161
+ assert.deepEqual(result, ['user@example.com']);
162
+ });
163
+ it('parses UIDs with mixed separators', () => {
164
+ const result = UniqueUidsFromStringSchema(10).parse('user1@example.com, user2@example.com; user3@example.com');
165
+ assert.deepEqual(result, ['user1@example.com', 'user2@example.com', 'user3@example.com']);
166
+ });
167
+ it('deduplicates UIDs', () => {
168
+ const result = UniqueUidsFromStringSchema(10).parse('user@example.com, user@example.com, user@example.com');
169
+ assert.deepEqual(result, ['user@example.com']);
170
+ });
171
+ it('trims whitespace from UIDs', () => {
172
+ const result = UniqueUidsFromStringSchema(10).parse(' user1@example.com , user2@example.com ');
173
+ assert.deepEqual(result, ['user1@example.com', 'user2@example.com']);
174
+ });
175
+ it('rejects when UIDs exceed limit', () => {
176
+ const result = UniqueUidsFromStringSchema(2).safeParse('a@b.com, b@c.com, c@d.com');
177
+ assert.isFalse(result.success);
178
+ if (!result.success) {
179
+ assert.include(result.error.issues[0].message, 'Cannot provide more than 2 UIDs');
180
+ }
181
+ });
182
+ it('rejects invalid email addresses', () => {
183
+ const result = UniqueUidsFromStringSchema(10).safeParse('invalid-email');
184
+ assert.isFalse(result.success);
185
+ if (!result.success) {
186
+ assert.include(result.error.issues[0].message, 'Invalid UID format: invalid-email');
187
+ }
188
+ });
189
+ it('rejects when any email in list is invalid', () => {
190
+ const result = UniqueUidsFromStringSchema(10).safeParse('user@example.com, not-an-email, not-an-email-2');
191
+ assert.isFalse(result.success);
192
+ if (!result.success) {
193
+ assert.include(result.error.issues[0].message, 'Invalid UID format: not-an-email');
194
+ assert.include(result.error.issues[1].message, 'Invalid UID format: not-an-email-2');
195
+ }
196
+ });
197
+ });
132
198
  //# sourceMappingURL=index.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import parsePostgresInterval from 'postgres-interval';\nimport { assert, describe, it } from 'vitest';\n\nimport {\n ArrayFromCheckboxSchema,\n ArrayFromStringOrArraySchema,\n BooleanFromCheckboxSchema,\n IdSchema,\n IntegerFromStringOrEmptySchema,\n IntervalSchema,\n} from './index.js';\n\ndescribe('BooleanFromCheckboxSchema', () => {\n it('parses a checked checkbox', () => {\n const result = BooleanFromCheckboxSchema.parse('on');\n assert.isTrue(result);\n });\n it('parses an unchecked checkbox', () => {\n const result = BooleanFromCheckboxSchema.parse(undefined);\n assert.isFalse(result);\n });\n it('parses a checkbox with an empty string', () => {\n const result = BooleanFromCheckboxSchema.parse('');\n assert.isFalse(result);\n });\n it('parses a checkbox with a non-empty string', () => {\n const result = BooleanFromCheckboxSchema.parse('checked');\n assert.isTrue(result);\n });\n});\n\ndescribe('IdSchema', () => {\n it('parses a valid id', () => {\n const id = IdSchema.parse('123');\n assert.equal(id, '123');\n });\n\n it('parses a nullable id', () => {\n const id = IdSchema.nullable().parse(null);\n assert.equal(id, null);\n });\n\n it('parses an optional id', () => {\n const id = IdSchema.optional().parse(undefined);\n assert.equal(id, undefined);\n });\n\n it('rejects a negative ID', () => {\n const result = IdSchema.safeParse('-1');\n assert.isFalse(result.success);\n });\n\n it('rejects a non-numeric ID', () => {\n const result = IdSchema.safeParse('abc');\n assert.isFalse(result.success);\n });\n});\n\ndescribe('IntervalSchema', () => {\n it('handles a PostgresInterval object', () => {\n const interval = IntervalSchema.parse(parsePostgresInterval('1 year 2 months 3 days'));\n assert.equal(interval, 37000800000);\n });\n\n it('parses an interval with date', () => {\n const interval = IntervalSchema.parse('1 year 2 months 3 days');\n assert.equal(interval, 37000800000);\n });\n\n it('parses an interval with time', () => {\n const interval = IntervalSchema.parse('04:05:06.7');\n assert.equal(interval, 14706700);\n });\n\n it('parses an interval with microsecond-precision time', () => {\n const interval = IntervalSchema.parse('01:02:03.456789');\n assert.equal(interval, 3723456.789);\n });\n\n it('parses a complex interval', () => {\n const interval = IntervalSchema.parse('1 years 2 mons 3 days 04:05:06.789');\n assert.equal(interval, 37015506789);\n });\n\n it('parses interval with negative months', () => {\n const interval = IntervalSchema.parse('-10 mons 3 days 04:05:06.789');\n assert.equal(interval, -25646093211);\n });\n\n it('parses interval with negative years and months', () => {\n const interval = IntervalSchema.parse('-1 years -2 months 3 days 04:05:06.789');\n assert.equal(interval, -36467693211);\n });\n\n it('parses interval with negative years, months, and days', () => {\n const interval = IntervalSchema.parse('-1 years -2 months -3 days 04:05:06.789');\n assert.equal(interval, -36986093211);\n });\n\n it('parses a negative interval', () => {\n const interval = IntervalSchema.parse('-1 years -2 months -3 days -04:05:06.789');\n assert.equal(interval, -37015506789);\n });\n});\n\ndescribe('IntegerFromStringOrEmptySchema', () => {\n it('parses a valid integer string', () => {\n const result = IntegerFromStringOrEmptySchema.parse('123');\n assert.equal(result, 123);\n });\n\n it('parses an empty string as null', () => {\n const result = IntegerFromStringOrEmptySchema.parse('');\n assert.equal(result, null);\n });\n\n it('rejects a non-integer string', () => {\n const result = IntegerFromStringOrEmptySchema.safeParse('abc');\n assert.isFalse(result.success);\n });\n\n it('rejects a decimal string', () => {\n const result = IntegerFromStringOrEmptySchema.safeParse('123.45');\n assert.isFalse(result.success);\n });\n});\n\ndescribe('ArrayFromStringOrArraySchema', () => {\n it('parses a string to an array', () => {\n const result = ArrayFromStringOrArraySchema.parse('a');\n assert.deepEqual(result, ['a']);\n });\n\n it('parses an array to itself', () => {\n const result = ArrayFromStringOrArraySchema.parse(['a', 'b', 'c']);\n assert.deepEqual(result, ['a', 'b', 'c']);\n });\n\n it('rejects an integer', () => {\n const result = ArrayFromStringOrArraySchema.safeParse(123);\n assert.isFalse(result.success);\n });\n\n it('rejects an object', () => {\n const result = ArrayFromStringOrArraySchema.safeParse({ a: 1 });\n assert.isFalse(result.success);\n });\n});\n\ndescribe('ArrayFromCheckboxSchema', () => {\n it('parses a missing value', () => {\n const result = ArrayFromCheckboxSchema.parse(undefined);\n assert.deepEqual(result, []);\n });\n\n it('parses a single string value', () => {\n const result = ArrayFromCheckboxSchema.parse('a');\n assert.deepEqual(result, ['a']);\n });\n\n it('parses an array of strings', () => {\n const result = ArrayFromCheckboxSchema.parse(['a', 'b', 'c']);\n assert.deepEqual(result, ['a', 'b', 'c']);\n });\n});\n"]}
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import parsePostgresInterval from 'postgres-interval';\nimport { assert, describe, it } from 'vitest';\n\nimport {\n ArrayFromCheckboxSchema,\n ArrayFromStringOrArraySchema,\n BooleanFromCheckboxSchema,\n DatetimeLocalStringSchema,\n IdSchema,\n IntegerFromStringOrEmptySchema,\n IntervalSchema,\n UniqueUidsFromStringSchema,\n} from './index.js';\n\ndescribe('BooleanFromCheckboxSchema', () => {\n it('parses a checked checkbox', () => {\n const result = BooleanFromCheckboxSchema.parse('on');\n assert.isTrue(result);\n });\n it('parses an unchecked checkbox', () => {\n const result = BooleanFromCheckboxSchema.parse(undefined);\n assert.isFalse(result);\n });\n it('parses a checkbox with an empty string', () => {\n const result = BooleanFromCheckboxSchema.parse('');\n assert.isFalse(result);\n });\n it('parses a checkbox with a non-empty string', () => {\n const result = BooleanFromCheckboxSchema.parse('checked');\n assert.isTrue(result);\n });\n});\n\ndescribe('IdSchema', () => {\n it('parses a valid id', () => {\n const id = IdSchema.parse('123');\n assert.equal(id, '123');\n });\n\n it('parses a nullable id', () => {\n const id = IdSchema.nullable().parse(null);\n assert.equal(id, null);\n });\n\n it('parses an optional id', () => {\n const id = IdSchema.optional().parse(undefined);\n assert.equal(id, undefined);\n });\n\n it('rejects a negative ID', () => {\n const result = IdSchema.safeParse('-1');\n assert.isFalse(result.success);\n });\n\n it('rejects a non-numeric ID', () => {\n const result = IdSchema.safeParse('abc');\n assert.isFalse(result.success);\n });\n});\n\ndescribe('IntervalSchema', () => {\n it('handles a PostgresInterval object', () => {\n const interval = IntervalSchema.parse(parsePostgresInterval('1 year 2 months 3 days'));\n assert.equal(interval, 37000800000);\n });\n\n it('parses an interval with date', () => {\n const interval = IntervalSchema.parse('1 year 2 months 3 days');\n assert.equal(interval, 37000800000);\n });\n\n it('parses an interval with time', () => {\n const interval = IntervalSchema.parse('04:05:06.7');\n assert.equal(interval, 14706700);\n });\n\n it('parses an interval with microsecond-precision time', () => {\n const interval = IntervalSchema.parse('01:02:03.456789');\n assert.equal(interval, 3723456.789);\n });\n\n it('parses a complex interval', () => {\n const interval = IntervalSchema.parse('1 years 2 mons 3 days 04:05:06.789');\n assert.equal(interval, 37015506789);\n });\n\n it('parses interval with negative months', () => {\n const interval = IntervalSchema.parse('-10 mons 3 days 04:05:06.789');\n assert.equal(interval, -25646093211);\n });\n\n it('parses interval with negative years and months', () => {\n const interval = IntervalSchema.parse('-1 years -2 months 3 days 04:05:06.789');\n assert.equal(interval, -36467693211);\n });\n\n it('parses interval with negative years, months, and days', () => {\n const interval = IntervalSchema.parse('-1 years -2 months -3 days 04:05:06.789');\n assert.equal(interval, -36986093211);\n });\n\n it('parses a negative interval', () => {\n const interval = IntervalSchema.parse('-1 years -2 months -3 days -04:05:06.789');\n assert.equal(interval, -37015506789);\n });\n});\n\ndescribe('DatetimeLocalStringSchema', () => {\n it('parses a valid datetime-local string without seconds', () => {\n const result = DatetimeLocalStringSchema.parse('2024-01-15T14:30');\n assert.equal(result, '2024-01-15T14:30:00');\n });\n\n it('parses a valid datetime-local string with seconds', () => {\n const result = DatetimeLocalStringSchema.parse('2024-01-15T14:30:45');\n assert.equal(result, '2024-01-15T14:30:45');\n });\n\n it('rejects an invalid format (missing time)', () => {\n const result = DatetimeLocalStringSchema.safeParse('2024-01-15');\n assert.isFalse(result.success);\n });\n\n it('rejects an invalid format (ISO with timezone)', () => {\n const result = DatetimeLocalStringSchema.safeParse('2024-01-15T14:30:00Z');\n assert.isFalse(result.success);\n });\n\n it('rejects an empty string', () => {\n const result = DatetimeLocalStringSchema.safeParse('');\n assert.isFalse(result.success);\n });\n\n it('rejects an invalid date', () => {\n const result = DatetimeLocalStringSchema.safeParse('2024-13-45T25:99');\n assert.isFalse(result.success);\n });\n});\n\ndescribe('IntegerFromStringOrEmptySchema', () => {\n it('parses a valid integer string', () => {\n const result = IntegerFromStringOrEmptySchema.parse('123');\n assert.equal(result, 123);\n });\n\n it('parses an empty string as null', () => {\n const result = IntegerFromStringOrEmptySchema.parse('');\n assert.equal(result, null);\n });\n\n it('rejects a non-integer string', () => {\n const result = IntegerFromStringOrEmptySchema.safeParse('abc');\n assert.isFalse(result.success);\n });\n\n it('rejects a decimal string', () => {\n const result = IntegerFromStringOrEmptySchema.safeParse('123.45');\n assert.isFalse(result.success);\n });\n});\n\ndescribe('ArrayFromStringOrArraySchema', () => {\n it('parses a string to an array', () => {\n const result = ArrayFromStringOrArraySchema.parse('a');\n assert.deepEqual(result, ['a']);\n });\n\n it('parses an array to itself', () => {\n const result = ArrayFromStringOrArraySchema.parse(['a', 'b', 'c']);\n assert.deepEqual(result, ['a', 'b', 'c']);\n });\n\n it('rejects an integer', () => {\n const result = ArrayFromStringOrArraySchema.safeParse(123);\n assert.isFalse(result.success);\n });\n\n it('rejects an object', () => {\n const result = ArrayFromStringOrArraySchema.safeParse({ a: 1 });\n assert.isFalse(result.success);\n });\n});\n\ndescribe('ArrayFromCheckboxSchema', () => {\n it('parses a missing value', () => {\n const result = ArrayFromCheckboxSchema.parse(undefined);\n assert.deepEqual(result, []);\n });\n\n it('parses a single string value', () => {\n const result = ArrayFromCheckboxSchema.parse('a');\n assert.deepEqual(result, ['a']);\n });\n\n it('parses an array of strings', () => {\n const result = ArrayFromCheckboxSchema.parse(['a', 'b', 'c']);\n assert.deepEqual(result, ['a', 'b', 'c']);\n });\n});\n\ndescribe('UniqueUidsFromStringSchema', () => {\n it('parses a single UID', () => {\n const result = UniqueUidsFromStringSchema(10).parse('user@example.com');\n assert.deepEqual(result, ['user@example.com']);\n });\n\n it('parses UIDs with mixed separators', () => {\n const result = UniqueUidsFromStringSchema(10).parse(\n 'user1@example.com, user2@example.com; user3@example.com',\n );\n assert.deepEqual(result, ['user1@example.com', 'user2@example.com', 'user3@example.com']);\n });\n\n it('deduplicates UIDs', () => {\n const result = UniqueUidsFromStringSchema(10).parse(\n 'user@example.com, user@example.com, user@example.com',\n );\n assert.deepEqual(result, ['user@example.com']);\n });\n\n it('trims whitespace from UIDs', () => {\n const result = UniqueUidsFromStringSchema(10).parse(\n ' user1@example.com , user2@example.com ',\n );\n assert.deepEqual(result, ['user1@example.com', 'user2@example.com']);\n });\n\n it('rejects when UIDs exceed limit', () => {\n const result = UniqueUidsFromStringSchema(2).safeParse('a@b.com, b@c.com, c@d.com');\n assert.isFalse(result.success);\n if (!result.success) {\n assert.include(result.error.issues[0].message, 'Cannot provide more than 2 UIDs');\n }\n });\n\n it('rejects invalid email addresses', () => {\n const result = UniqueUidsFromStringSchema(10).safeParse('invalid-email');\n assert.isFalse(result.success);\n if (!result.success) {\n assert.include(result.error.issues[0].message, 'Invalid UID format: invalid-email');\n }\n });\n\n it('rejects when any email in list is invalid', () => {\n const result = UniqueUidsFromStringSchema(10).safeParse(\n 'user@example.com, not-an-email, not-an-email-2',\n );\n assert.isFalse(result.success);\n if (!result.success) {\n assert.include(result.error.issues[0].message, 'Invalid UID format: not-an-email');\n assert.include(result.error.issues[1].message, 'Invalid UID format: not-an-email-2');\n }\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/zod",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,10 +19,10 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@prairielearn/tsconfig": "^0.0.0",
22
- "@types/node": "^22.18.8",
23
- "@vitest/coverage-v8": "^3.2.4",
24
- "tsx": "^4.20.6",
22
+ "@types/node": "^22.19.3",
23
+ "@vitest/coverage-v8": "^4.0.16",
24
+ "tsx": "^4.21.0",
25
25
  "typescript": "^5.9.3",
26
- "vitest": "^3.2.4"
26
+ "vitest": "^4.0.16"
27
27
  }
28
28
  }
package/src/index.test.ts CHANGED
@@ -5,9 +5,11 @@ import {
5
5
  ArrayFromCheckboxSchema,
6
6
  ArrayFromStringOrArraySchema,
7
7
  BooleanFromCheckboxSchema,
8
+ DatetimeLocalStringSchema,
8
9
  IdSchema,
9
10
  IntegerFromStringOrEmptySchema,
10
11
  IntervalSchema,
12
+ UniqueUidsFromStringSchema,
11
13
  } from './index.js';
12
14
 
13
15
  describe('BooleanFromCheckboxSchema', () => {
@@ -103,6 +105,38 @@ describe('IntervalSchema', () => {
103
105
  });
104
106
  });
105
107
 
108
+ describe('DatetimeLocalStringSchema', () => {
109
+ it('parses a valid datetime-local string without seconds', () => {
110
+ const result = DatetimeLocalStringSchema.parse('2024-01-15T14:30');
111
+ assert.equal(result, '2024-01-15T14:30:00');
112
+ });
113
+
114
+ it('parses a valid datetime-local string with seconds', () => {
115
+ const result = DatetimeLocalStringSchema.parse('2024-01-15T14:30:45');
116
+ assert.equal(result, '2024-01-15T14:30:45');
117
+ });
118
+
119
+ it('rejects an invalid format (missing time)', () => {
120
+ const result = DatetimeLocalStringSchema.safeParse('2024-01-15');
121
+ assert.isFalse(result.success);
122
+ });
123
+
124
+ it('rejects an invalid format (ISO with timezone)', () => {
125
+ const result = DatetimeLocalStringSchema.safeParse('2024-01-15T14:30:00Z');
126
+ assert.isFalse(result.success);
127
+ });
128
+
129
+ it('rejects an empty string', () => {
130
+ const result = DatetimeLocalStringSchema.safeParse('');
131
+ assert.isFalse(result.success);
132
+ });
133
+
134
+ it('rejects an invalid date', () => {
135
+ const result = DatetimeLocalStringSchema.safeParse('2024-13-45T25:99');
136
+ assert.isFalse(result.success);
137
+ });
138
+ });
139
+
106
140
  describe('IntegerFromStringOrEmptySchema', () => {
107
141
  it('parses a valid integer string', () => {
108
142
  const result = IntegerFromStringOrEmptySchema.parse('123');
@@ -163,3 +197,58 @@ describe('ArrayFromCheckboxSchema', () => {
163
197
  assert.deepEqual(result, ['a', 'b', 'c']);
164
198
  });
165
199
  });
200
+
201
+ describe('UniqueUidsFromStringSchema', () => {
202
+ it('parses a single UID', () => {
203
+ const result = UniqueUidsFromStringSchema(10).parse('user@example.com');
204
+ assert.deepEqual(result, ['user@example.com']);
205
+ });
206
+
207
+ it('parses UIDs with mixed separators', () => {
208
+ const result = UniqueUidsFromStringSchema(10).parse(
209
+ 'user1@example.com, user2@example.com; user3@example.com',
210
+ );
211
+ assert.deepEqual(result, ['user1@example.com', 'user2@example.com', 'user3@example.com']);
212
+ });
213
+
214
+ it('deduplicates UIDs', () => {
215
+ const result = UniqueUidsFromStringSchema(10).parse(
216
+ 'user@example.com, user@example.com, user@example.com',
217
+ );
218
+ assert.deepEqual(result, ['user@example.com']);
219
+ });
220
+
221
+ it('trims whitespace from UIDs', () => {
222
+ const result = UniqueUidsFromStringSchema(10).parse(
223
+ ' user1@example.com , user2@example.com ',
224
+ );
225
+ assert.deepEqual(result, ['user1@example.com', 'user2@example.com']);
226
+ });
227
+
228
+ it('rejects when UIDs exceed limit', () => {
229
+ const result = UniqueUidsFromStringSchema(2).safeParse('a@b.com, b@c.com, c@d.com');
230
+ assert.isFalse(result.success);
231
+ if (!result.success) {
232
+ assert.include(result.error.issues[0].message, 'Cannot provide more than 2 UIDs');
233
+ }
234
+ });
235
+
236
+ it('rejects invalid email addresses', () => {
237
+ const result = UniqueUidsFromStringSchema(10).safeParse('invalid-email');
238
+ assert.isFalse(result.success);
239
+ if (!result.success) {
240
+ assert.include(result.error.issues[0].message, 'Invalid UID format: invalid-email');
241
+ }
242
+ });
243
+
244
+ it('rejects when any email in list is invalid', () => {
245
+ const result = UniqueUidsFromStringSchema(10).safeParse(
246
+ 'user@example.com, not-an-email, not-an-email-2',
247
+ );
248
+ assert.isFalse(result.success);
249
+ if (!result.success) {
250
+ assert.include(result.error.issues[0].message, 'Invalid UID format: not-an-email');
251
+ assert.include(result.error.issues[1].message, 'Invalid UID format: not-an-email-2');
252
+ }
253
+ });
254
+ });
package/src/index.ts CHANGED
@@ -130,6 +130,35 @@ export const DateFromISOString = z
130
130
  )
131
131
  .transform((s) => new Date(s));
132
132
 
133
+ /**
134
+ * A Zod schema for a datetime-local input value.
135
+ *
136
+ * Accepts a string in the format "YYYY-MM-DDTHH:MM" or "YYYY-MM-DDTHH:MM:SS"
137
+ * as produced by `<input type="datetime-local">` elements.
138
+ *
139
+ * Validates the format and returns it as a string.
140
+ */
141
+ export const DatetimeLocalStringSchema = z
142
+ .string()
143
+ .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/, {
144
+ message: 'must be a valid datetime-local string (YYYY-MM-DDTHH:MM or YYYY-MM-DDTHH:MM:SS)',
145
+ })
146
+ // Append `:00` seconds if omitted (Chrome bug workaround).
147
+ // https://stackoverflow.com/questions/19504018/show-seconds-on-input-type-date-local-in-chrome
148
+ // https://issues.chromium.org/issues/41159420
149
+ .transform((s) => (s.length === 16 ? `${s}:00` : s))
150
+ .transform((s, ctx) => {
151
+ const date = new Date(s);
152
+ if (Number.isNaN(date.getTime())) {
153
+ ctx.addIssue({
154
+ code: z.ZodIssueCode.custom,
155
+ message: 'must be a valid date',
156
+ });
157
+ return z.NEVER;
158
+ }
159
+ return s;
160
+ });
161
+
133
162
  /**
134
163
  * A Zod schema that coerces a non-empty string to an integer or an empty string to null.
135
164
  * This is useful for form number inputs that are not required but we do not want to
@@ -175,3 +204,54 @@ export const ArrayFromCheckboxSchema = z
175
204
  return [s];
176
205
  }
177
206
  });
207
+
208
+ /**
209
+ * Creates a Zod schema that parses a string of UIDs separated by whitespace,
210
+ * commas, or semicolons into an array of unique, trimmed UIDs.
211
+ *
212
+ * @param limit - The maximum number of UIDs allowed. Defaults to 1000.
213
+ * @returns A Zod schema that parses and validates the UID string.
214
+ */
215
+ export function UniqueUidsFromStringSchema(limit = 1000) {
216
+ const emailSchema = z.string().email();
217
+
218
+ return z.string().transform((uidsString, ctx) => {
219
+ const uids = new Set(
220
+ uidsString
221
+ .split(/[\s,;]+/)
222
+ .map((uid) => uid.trim())
223
+ .filter(Boolean),
224
+ );
225
+
226
+ if (uids.size > limit) {
227
+ ctx.addIssue({
228
+ code: z.ZodIssueCode.too_big,
229
+ maximum: limit,
230
+ type: 'set',
231
+ inclusive: true,
232
+ message: `Cannot provide more than ${limit} UIDs at a time`,
233
+ });
234
+ return z.NEVER;
235
+ }
236
+
237
+ if (uids.size === 0) {
238
+ ctx.addIssue({
239
+ code: z.ZodIssueCode.custom,
240
+ message: 'At least one UID is required',
241
+ });
242
+ return z.NEVER;
243
+ }
244
+
245
+ for (const uid of uids) {
246
+ const result = emailSchema.safeParse(uid);
247
+ if (!result.success) {
248
+ ctx.addIssue({
249
+ code: z.ZodIssueCode.custom,
250
+ message: `Invalid UID format: ${uid}`,
251
+ });
252
+ }
253
+ }
254
+
255
+ return Array.from(uids);
256
+ });
257
+ }
package/vitest.config.ts CHANGED
@@ -2,10 +2,10 @@ import { defineConfig } from 'vitest/config';
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
+ dir: `${import.meta.dirname}/src`,
5
6
  coverage: {
6
7
  reporter: ['html', 'text-summary', 'cobertura'],
7
- all: true,
8
- include: ['src/**'],
8
+ include: ['src/**/*.{ts,tsx}'],
9
9
  },
10
10
  },
11
11
  });