From 60500c6c0780c4528d72dfd2bacf690ce75a2011 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Wed, 10 Jan 2024 22:03:02 +0100 Subject: [PATCH] Proprietary schemas using native zod methods and Metadata (#1442) I was dreaming about it. Unfortunately `zod` does not provide any way to make a custom or branded or third-party schema that can be identified as one programmatically. Developer of `zod` also does not want to make any method to store metadata in schemas, instead, recommends to wrap schemas in some other structures, which is not suitable for the purposes of `express-zod-api`. There are many small inconvenient things in making custom schema classes, that I'd like to replace into native methods, and use `withMeta` wrapper for storing proprietary identifier, so the generators and walkers could still handle it. Related issues: ``` https://github.com/colinhacks/zod/issues/1718 https://github.com/colinhacks/zod/issues/2413 https://github.com/colinhacks/zod/issues/273 https://github.com/colinhacks/zod/issues/71 https://github.com/colinhacks/zod/issues/37 ``` PR I've been waiting for months to merged (programmatically distinguishable branding): ``` https://github.com/colinhacks/zod/pull/2860 ``` --- CHANGELOG.md | 11 ++ README.md | 6 +- example/example.documentation.yaml | 2 +- example/factories.ts | 2 +- package.json | 2 +- src/common-helpers.ts | 10 +- src/date-in-schema.ts | 79 ++-------- src/date-out-schema.ts | 53 ++----- src/documentation-helpers.ts | 42 +++-- src/file-schema.ts | 146 +++++------------- src/index.ts | 6 +- src/integration.ts | 6 +- src/metadata.ts | 18 +++ src/proprietary-schemas.ts | 23 +-- src/raw-schema.ts | 11 ++ src/schema-helpers.ts | 28 ++-- src/schema-walker.ts | 20 +-- src/upload-schema.ts | 72 ++++----- src/zts.ts | 31 ++-- .../documentation-helpers.spec.ts.snap | 14 ++ .../__snapshots__/upload-schema.spec.ts.snap | 4 +- tests/unit/common-helpers.spec.ts | 26 +--- tests/unit/date-in-schema.spec.ts | 24 +-- tests/unit/date-out-schema.spec.ts | 22 +-- tests/unit/documentation-helpers.spec.ts | 27 ++-- tests/unit/file-schema.spec.ts | 115 +++++++------- tests/unit/index.spec.ts | 8 - tests/unit/upload-schema.spec.ts | 21 +-- 28 files changed, 350 insertions(+), 479 deletions(-) create mode 100644 src/raw-schema.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a14cea17a..3711527ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Version 16 +### 16.3.0 + +- Switching to using native `zod` methods for proprietary schemas instead of custom classes (`ez` namespace): + - Each proprietary schema now relies on internal Metadata; + - Validation errors for `ez.file()` are changed slightly; + - The following refinements of `ez.file()` are deprecated and will be removed later: + - ~~`ez.file().string()`~~ — use `ez.file("string")` instead, + - ~~`ez.file().buffer()`~~ — use `ez.file("buffer")` instead, + - ~~`ez.file().base64()`~~ — use `ez.file("base64")` instead, + - ~~`ez.file().binary()`~~ — use `ez.file("binary")` instead. + ### 16.2.2 - Fixed issue #1458 reported by [@elee1766](https://github.com/elee1766): diff --git a/README.md b/README.md index bdf77c158..191dfbef1 100644 --- a/README.md +++ b/README.md @@ -708,14 +708,14 @@ You can find two approaches to `EndpointsFactory` and `ResultHandler` implementa [in this example](https://github.com/RobinTail/express-zod-api/blob/master/example/factories.ts). One of them implements file streaming, in this case the endpoint just has to provide the filename. The response schema generally may be just `z.string()`, but I made more specific `ez.file()` that also supports -`.binary()` and `.base64()` refinements which are reflected in the +`ez.file("binary")` and `ez.file("base64")` variants which are reflected in the [generated documentation](#creating-a-documentation). ```typescript const fileStreamingEndpointsFactory = new EndpointsFactory( createResultHandler({ getPositiveResponse: () => ({ - schema: ez.file().buffer(), + schema: ez.file("buffer"), mimeType: "image/*", }), getNegativeResponse: () => ({ schema: z.string(), mimeType: "text/plain" }), @@ -899,7 +899,7 @@ Some APIs may require an endpoint to be able to accept and process raw data, suc file as an entire body of request. In order to enable this feature you need to set the `rawParser` config feature to `express.raw()`. See also its options [in Express.js documentation](https://expressjs.com/en/4x/api.html#express.raw). The raw data is placed into `request.body.raw` property, having type `Buffer`. Then use the proprietary `ez.raw()` -schema (which is an alias for `z.object({ raw: ez.file().buffer() })`) as the input schema of your endpoint. +schema (which is an alias for `z.object({ raw: ez.file("buffer") })`) as the input schema of your endpoint. ```typescript import express from "express"; diff --git a/example/example.documentation.yaml b/example/example.documentation.yaml index 1c065357a..50e04a93e 100644 --- a/example/example.documentation.yaml +++ b/example/example.documentation.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Example API - version: 16.2.2 + version: 16.3.0-beta1 paths: /v1/user/retrieve: get: diff --git a/example/factories.ts b/example/factories.ts index 8b9a050c2..9f081144e 100644 --- a/example/factories.ts +++ b/example/factories.ts @@ -52,7 +52,7 @@ export const fileStreamingEndpointsFactory = new EndpointsFactory({ config, resultHandler: createResultHandler({ getPositiveResponse: () => ({ - schema: ez.file().buffer(), + schema: ez.file("buffer"), mimeType: "image/*", }), getNegativeResponse: () => ({ diff --git a/package.json b/package.json index 3ba291031..9186dd2e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-zod-api", - "version": "16.2.2", + "version": "16.3.0-beta1", "description": "A Typescript library to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.", "license": "MIT", "repository": { diff --git a/src/common-helpers.ts b/src/common-helpers.ts index c635f567f..ccc6be552 100644 --- a/src/common-helpers.ts +++ b/src/common-helpers.ts @@ -5,13 +5,13 @@ import { xprod } from "ramda"; import { z } from "zod"; import { CommonConfig, InputSource, InputSources } from "./config-type"; import { InputValidationError, OutputValidationError } from "./errors"; -import { ZodFile } from "./file-schema"; import { IOSchema } from "./io-schema"; import { AbstractLogger } from "./logger"; -import { getMeta } from "./metadata"; +import { getMeta, isProprietary } from "./metadata"; import { AuxMethod, Method } from "./method"; import { mimeMultipart } from "./mime"; -import { ZodUpload } from "./upload-schema"; +import { ezRawKind } from "./raw-schema"; +import { ezUploadKind } from "./upload-schema"; export type FlatObject = Record; @@ -237,13 +237,13 @@ export const hasNestedSchema = ({ export const hasUpload = (subject: IOSchema) => hasNestedSchema({ subject, - condition: (schema) => schema instanceof ZodUpload, + condition: (schema) => isProprietary(schema, ezUploadKind), }); export const hasRaw = (subject: IOSchema) => hasNestedSchema({ subject, - condition: (schema) => schema instanceof ZodFile, + condition: (schema) => isProprietary(schema, ezRawKind), maxDepth: 3, }); diff --git a/src/date-in-schema.ts b/src/date-in-schema.ts index 1caba2c62..8a2385a17 100644 --- a/src/date-in-schema.ts +++ b/src/date-in-schema.ts @@ -1,64 +1,15 @@ -import { - INVALID, - ParseInput, - ParseReturnType, - ZodIssueCode, - ZodParsedType, - ZodType, - ZodTypeDef, - addIssueToContext, -} from "zod"; -import { isValidDate } from "./schema-helpers"; - -// simple regex for ISO date, supports the following formats: -// 2021-01-01T00:00:00.000Z -// 2021-01-01T00:00:00.0Z -// 2021-01-01T00:00:00Z -// 2021-01-01T00:00:00 -// 2021-01-01 -export const isoDateRegex = - /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?)?Z?$/; - -const zodDateInKind = "ZodDateIn"; - -export interface ZodDateInDef extends ZodTypeDef { - typeName: typeof zodDateInKind; -} - -export class ZodDateIn extends ZodType { - _parse(input: ParseInput): ParseReturnType { - const { status, ctx } = this._processInputParams(input); - if (ctx.parsedType !== ZodParsedType.string) { - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_type, - expected: ZodParsedType.string, - received: ctx.parsedType, - }); - return INVALID; - } - - if (!isoDateRegex.test(ctx.data as string)) { - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_string, - validation: "regex", - }); - status.dirty(); - } - - const date = new Date(ctx.data); - - if (!isValidDate(date)) { - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_date, - }); - return INVALID; - } - - return { status: status.value, value: date }; - } - - static create = () => - new ZodDateIn({ - typeName: zodDateInKind, - }); -} +import { z } from "zod"; +import { proprietary } from "./metadata"; +import { isValidDate, isoDateRegex } from "./schema-helpers"; + +export const ezDateInKind = "DateIn"; + +export const dateIn = () => + proprietary( + ezDateInKind, + z + .string() + .regex(isoDateRegex) + .transform((str) => new Date(str)) + .pipe(z.date().refine(isValidDate)), + ); diff --git a/src/date-out-schema.ts b/src/date-out-schema.ts index 4715827e2..6b832aab4 100644 --- a/src/date-out-schema.ts +++ b/src/date-out-schema.ts @@ -1,45 +1,14 @@ -import { - INVALID, - ParseInput, - ParseReturnType, - ZodIssueCode, - ZodParsedType, - ZodType, - ZodTypeDef, - addIssueToContext, -} from "zod"; +import { z } from "zod"; +import { proprietary } from "./metadata"; import { isValidDate } from "./schema-helpers"; -const zodDateOutKind = "ZodDateOut"; +export const ezDateOutKind = "DateOut"; -export interface ZodDateOutDef extends ZodTypeDef { - typeName: typeof zodDateOutKind; -} - -export class ZodDateOut extends ZodType { - _parse(input: ParseInput): ParseReturnType { - const { status, ctx } = this._processInputParams(input); - if (ctx.parsedType !== ZodParsedType.date) { - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_type, - expected: ZodParsedType.date, - received: ctx.parsedType, - }); - return INVALID; - } - - if (!isValidDate(ctx.data)) { - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_date, - }); - return INVALID; - } - - return { status: status.value, value: (ctx.data as Date).toISOString() }; - } - - static create = () => - new ZodDateOut({ - typeName: zodDateOutKind, - }); -} +export const dateOut = () => + proprietary( + ezDateOutKind, + z + .date() + .refine(isValidDate) + .transform((date) => date.toISOString()), + ); diff --git a/src/documentation-helpers.ts b/src/documentation-helpers.ts index 4b4741593..7c46f0686 100644 --- a/src/documentation-helpers.ts +++ b/src/documentation-helpers.ts @@ -22,7 +22,6 @@ import { FlatObject, getExamples, hasCoercion, - hasRaw, hasTopLevelTransformingEffect, isCustomHeader, makeCleanId, @@ -30,10 +29,10 @@ import { ucFirst, } from "./common-helpers"; import { InputSource, TagsConfig } from "./config-type"; -import { ZodDateIn, isoDateRegex } from "./date-in-schema"; -import { ZodDateOut } from "./date-out-schema"; +import { ezDateInKind } from "./date-in-schema"; +import { ezDateOutKind } from "./date-out-schema"; import { DocumentationError } from "./errors"; -import { ZodFile } from "./file-schema"; +import { ezFileKind } from "./file-schema"; import { IOSchema } from "./io-schema"; import { LogicalContainer, @@ -42,6 +41,8 @@ import { } from "./logical-container"; import { copyMeta } from "./metadata"; import { Method } from "./method"; +import { RawSchema, ezRawKind } from "./raw-schema"; +import { isoDateRegex } from "./schema-helpers"; import { HandlingRules, HandlingVariant, @@ -49,7 +50,7 @@ import { walkSchema, } from "./schema-walker"; import { Security } from "./security"; -import { ZodUpload } from "./upload-schema"; +import { ezUploadKind } from "./upload-schema"; /* eslint-disable @typescript-eslint/no-use-before-define */ @@ -130,7 +131,7 @@ export const depictAny: Depicter = () => ({ format: "any", }); -export const depictUpload: Depicter = (ctx) => { +export const depictUpload: Depicter = (ctx) => { assert( !ctx.isResponse, new DocumentationError({ @@ -144,11 +145,14 @@ export const depictUpload: Depicter = (ctx) => { }; }; -export const depictFile: Depicter = ({ - schema: { isBinary, isBase64, isBuffer }, -}) => ({ +export const depictFile: Depicter = ({ schema }) => ({ type: "string", - format: isBuffer || isBinary ? "binary" : isBase64 ? "byte" : "file", + format: + schema instanceof z.ZodString + ? schema._def.checks.find((check) => check.kind === "regex") + ? "byte" + : "file" + : "binary", }); export const depictUnion: Depicter> = ({ @@ -245,7 +249,7 @@ export const depictObject: Depicter = ({ * */ export const depictNull: Depicter = () => ({ type: "null" }); -export const depictDateIn: Depicter = (ctx) => { +export const depictDateIn: Depicter = (ctx) => { assert( !ctx.isResponse, new DocumentationError({ @@ -264,7 +268,7 @@ export const depictDateIn: Depicter = (ctx) => { }; }; -export const depictDateOut: Depicter = (ctx) => { +export const depictDateOut: Depicter = (ctx) => { assert( ctx.isResponse, new DocumentationError({ @@ -587,6 +591,9 @@ export const depictLazy: Depicter> = ({ ); }; +export const depictRaw: Depicter = ({ next, schema }) => + next({ schema: schema.shape.raw }); + export const depictExamples = ( schema: z.ZodTypeAny, isResponse: boolean, @@ -744,8 +751,6 @@ export const depicters: HandlingRules< ZodNumber: depictNumber, ZodBigInt: depictBigInt, ZodBoolean: depictBoolean, - ZodDateIn: depictDateIn, - ZodDateOut: depictDateOut, ZodNull: depictNull, ZodArray: depictArray, ZodTuple: depictTuple, @@ -754,8 +759,6 @@ export const depicters: HandlingRules< ZodLiteral: depictLiteral, ZodIntersection: depictIntersection, ZodUnion: depictUnion, - ZodFile: depictFile, - ZodUpload: depictUpload, ZodAny: depictAny, ZodDefault: depictDefault, ZodEnum: depictEnum, @@ -770,6 +773,11 @@ export const depicters: HandlingRules< ZodPipeline: depictPipeline, ZodLazy: depictLazy, ZodReadonly: depictReadonly, + [ezFileKind]: depictFile, + [ezUploadKind]: depictUpload, + [ezDateOutKind]: depictDateOut, + [ezDateInKind]: depictDateIn, + [ezRawKind]: depictRaw, }; export const onEach: Depicter = ({ @@ -1048,7 +1056,7 @@ export const depictRequest = ({ const bodyDepiction = excludeExamplesFromDepiction( excludeParamsFromDepiction( walkSchema({ - schema: hasRaw(schema) ? ZodFile.create().buffer() : schema, + schema, isResponse: false, rules: depicters, onEach, diff --git a/src/file-schema.ts b/src/file-schema.ts index caea311e1..c5a45eac5 100644 --- a/src/file-schema.ts +++ b/src/file-schema.ts @@ -1,110 +1,38 @@ -import { - INVALID, - ParseInput, - ParseReturnType, - ZodIssueCode, - ZodParsedType, - ZodType, - ZodTypeDef, - addIssueToContext, -} from "zod"; -import { ErrMessage, errToObj } from "./schema-helpers"; - -const zodFileKind = "ZodFile"; - -export interface ZodFileDef - extends ZodTypeDef { - typeName: typeof zodFileKind; - type: T; - encoding?: "binary" | "base64"; - message?: string; -} - -const base64Regex = - /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; - -export class ZodFile< - T extends string | Buffer = string | Buffer, -> extends ZodType, T> { - _parse(input: ParseInput): ParseReturnType { - const { status, ctx } = this._processInputParams(input); - - const isParsedString = - ctx.parsedType === ZodParsedType.string && typeof ctx.data === "string"; - - if (this.isString && !isParsedString) { - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_type, - expected: ZodParsedType.string, - received: ctx.parsedType, - }); - return INVALID; - } - - const isParsedBuffer = - ctx.parsedType === ZodParsedType.object && Buffer.isBuffer(ctx.data); - - if (this.isBuffer && !isParsedBuffer) { - addIssueToContext(ctx, { - code: ZodIssueCode.invalid_type, - expected: ZodParsedType.object, - received: ctx.parsedType, - message: "Expected Buffer", - }); - return INVALID; - } - - if (isParsedString && this.isBase64 && !base64Regex.test(ctx.data)) { - addIssueToContext(ctx, { - code: ZodIssueCode.custom, - message: this._def.message || "Does not match base64 encoding", - }); - status.dirty(); - } - - return { status: status.value, value: ctx.data as T }; - } - - string = (message?: ErrMessage) => - new ZodFile({ ...this._def, ...errToObj(message), type: "" }); - - buffer = (message?: ErrMessage) => - new ZodFile({ - ...this._def, - ...errToObj(message), - type: Buffer.from([]), - }); - - binary = (message?: ErrMessage) => - new ZodFile({ - ...this._def, - ...errToObj(message), - encoding: "binary", - }); - - base64 = (message?: ErrMessage) => - new ZodFile({ - ...this._def, - ...errToObj(message), - encoding: "base64", - }); - - get isBinary() { - return this._def.encoding === "binary"; - } - - get isBase64() { - return this._def.encoding === "base64"; - } - - get isString() { - return typeof this._def.type === "string"; - } - - get isBuffer() { - return Buffer.isBuffer(this._def.type); - } - - static create = () => - new ZodFile({ typeName: zodFileKind, type: "" }); +import { z } from "zod"; +import { proprietary } from "./metadata"; +import { base64Regex, bufferSchema } from "./schema-helpers"; + +export const ezFileKind = "File"; + +// @todo remove this in v17 +const wrap = ( + schema: T, +): ReturnType> & Variants => + // eslint-disable-next-line @typescript-eslint/no-use-before-define + Object.entries(variants).reduce( + (agg, [method, handler]) => + Object.defineProperty(agg, method, { get: () => handler }), + proprietary(ezFileKind, schema), + ) as ReturnType> & Variants; + +// @todo remove arguments from the methods in v17 +const variants = { + /** @deprecated use ez.file("buffer") instead */ + buffer: ({}: string | object = {}) => wrap(bufferSchema), + /** @deprecated use ez.file("string") instead */ + string: ({}: string | object = {}) => wrap(z.string()), + /** @deprecated use ez.file("binary") instead */ + binary: ({}: string | object = {}) => wrap(bufferSchema.or(z.string())), + /** @deprecated use ez.file("base64") instead */ + base64: ({}: string | object = {}) => + wrap(z.string().regex(base64Regex, "Does not match base64 encoding")), +}; + +type Variants = typeof variants; +type Variant = keyof Variants; + +export function file(): ReturnType; +export function file(variant: K): ReturnType; +export function file(variant?: K) { + return variants[variant || "string"](); } diff --git a/src/index.ts b/src/index.ts index 747e23019..c8a143655 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,7 @@ export { withMeta } from "./metadata"; export { testEndpoint } from "./testing"; export { Integration } from "./integration"; -export * as ez from "./proprietary-schemas"; +export { ez } from "./proprietary-schemas"; // Issues 952, 1182, 1269: Insufficient exports for consumer's declaration export type { MockOverrides } from "./testing"; @@ -42,10 +42,6 @@ export type { FlatObject } from "./common-helpers"; export type { Method } from "./method"; export type { IOSchema } from "./io-schema"; export type { Metadata } from "./metadata"; -export type { ZodDateInDef } from "./date-in-schema"; -export type { ZodDateOutDef } from "./date-out-schema"; -export type { ZodFileDef } from "./file-schema"; -export type { ZodUploadDef } from "./upload-schema"; export type { CommonConfig, AppConfig, ServerConfig } from "./config-type"; export type { MiddlewareDefinition } from "./middleware"; export type { ResultHandlerDefinition } from "./result-handler"; diff --git a/src/integration.ts b/src/integration.ts index 875f04d89..b623ea7b8 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -1,6 +1,5 @@ import ts from "typescript"; import { z } from "zod"; -import { ZodFile } from "./file-schema"; import { emptyHeading, emptyTail, @@ -27,7 +26,7 @@ import { protectedReadonlyModifier, spacingMiddle, } from "./integration-helpers"; -import { defaultSerializer, hasRaw, makeCleanId } from "./common-helpers"; +import { defaultSerializer, makeCleanId } from "./common-helpers"; import { Method, methods } from "./method"; import { mimeJson } from "./mime"; import { loadPeer } from "./peer-helpers"; @@ -155,11 +154,10 @@ export class Integration { makeAlias: this.makeAlias.bind(this), optionalPropStyle, }; - const inputSchema = endpoint.getSchema("input"); const inputId = makeCleanId(method, path, "input"); const input = zodToTs({ ...commons, - schema: hasRaw(inputSchema) ? ZodFile.create().buffer() : inputSchema, + schema: endpoint.getSchema("input"), isResponse: false, }); const positiveResponseId = splitResponse diff --git a/src/metadata.ts b/src/metadata.ts index e5e1da523..1ac269dff 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -1,8 +1,14 @@ import { combinations } from "./common-helpers"; import { z } from "zod"; import { clone, mergeDeepRight } from "ramda"; +import { ProprietaryKind } from "./proprietary-schemas"; export interface Metadata { + /** + * @todo if the following PR merged, use native branding instead: + * @link https://github.com/colinhacks/zod/pull/2860 + * */ + kind?: ProprietaryKind; examples: z.input[]; } @@ -65,3 +71,15 @@ export const copyMeta = ( ); return result; }; + +export const proprietary = ( + kind: ProprietaryKind, + subject: T, +) => { + const schema = withMeta(subject); + schema._def[metaProp].kind = kind; + return schema; +}; + +export const isProprietary = (schema: z.ZodTypeAny, kind: ProprietaryKind) => + getMeta(schema, "kind") === kind; diff --git a/src/proprietary-schemas.ts b/src/proprietary-schemas.ts index 0e0f10681..6ea9f2256 100644 --- a/src/proprietary-schemas.ts +++ b/src/proprietary-schemas.ts @@ -1,13 +1,14 @@ -import { z } from "zod"; -import { ZodDateIn } from "./date-in-schema"; -import { ZodDateOut } from "./date-out-schema"; -import { ZodFile } from "./file-schema"; -import { ZodUpload } from "./upload-schema"; +import { dateIn, ezDateInKind } from "./date-in-schema"; +import { dateOut, ezDateOutKind } from "./date-out-schema"; +import { ezFileKind, file } from "./file-schema"; +import { ezRawKind, raw } from "./raw-schema"; +import { ezUploadKind, upload } from "./upload-schema"; -export const file = ZodFile.create; -export const upload = ZodUpload.create; -export const dateIn = ZodDateIn.create; -export const dateOut = ZodDateOut.create; +export const ez = { dateIn, dateOut, file, upload, raw }; -/** Shorthand for z.object({ raw: ez.file().buffer() }) */ -export const raw = () => z.object({ raw: ZodFile.create().buffer() }); +export type ProprietaryKind = + | typeof ezFileKind + | typeof ezDateInKind + | typeof ezDateOutKind + | typeof ezUploadKind + | typeof ezRawKind; diff --git a/src/raw-schema.ts b/src/raw-schema.ts new file mode 100644 index 000000000..4ad7e5c63 --- /dev/null +++ b/src/raw-schema.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; +import { file } from "./file-schema"; +import { proprietary } from "./metadata"; + +export const ezRawKind = "Raw"; + +/** Shorthand for z.object({ raw: ez.file("buffer") }) */ +export const raw = () => + proprietary(ezRawKind, z.object({ raw: file("buffer") })); + +export type RawSchema = ReturnType; diff --git a/src/schema-helpers.ts b/src/schema-helpers.ts index 27962acc7..61fc12b4f 100644 --- a/src/schema-helpers.ts +++ b/src/schema-helpers.ts @@ -1,17 +1,21 @@ import { z } from "zod"; -// obtaining the private helper type from Zod -export type ErrMessage = Exclude< - Parameters[0], - undefined ->; - -// the copy of the private Zod errorUtil.errToObj -export const errToObj = (message: ErrMessage | undefined) => - typeof message === "string" ? { message } : message || {}; - export const isValidDate = (date: Date): boolean => !isNaN(date.getTime()); -export const bufferSchema = z.custom((subject) => - Buffer.isBuffer(subject), +export const bufferSchema = z.custom( + (subject) => Buffer.isBuffer(subject), + { message: "Expected Buffer" }, ); + +export const base64Regex = + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; + +/** + * @example 2021-01-01T00:00:00.000Z + * @example 2021-01-01T00:00:00.0Z + * @example 2021-01-01T00:00:00Z + * @example 2021-01-01T00:00:00 + * @example 2021-01-01 + */ +export const isoDateRegex = + /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?)?Z?$/; diff --git a/src/schema-walker.ts b/src/schema-walker.ts index 32d2c121f..1c9b3a5a0 100644 --- a/src/schema-walker.ts +++ b/src/schema-walker.ts @@ -1,9 +1,7 @@ import { z } from "zod"; import type { FlatObject } from "./common-helpers"; -import type { ZodDateInDef } from "./date-in-schema"; -import type { ZodDateOutDef } from "./date-out-schema"; -import type { ZodFileDef } from "./file-schema"; -import type { ZodUploadDef } from "./upload-schema"; +import { getMeta } from "./metadata"; +import { ProprietaryKind } from "./proprietary-schemas"; export type HandlingVariant = "last" | "regular" | "each"; @@ -33,15 +31,9 @@ export type SchemaHandler< Variant extends HandlingVariant = "regular", > = (params: SchemaHandlingProps) => U; -export type ProprietaryKinds = - | ZodFileDef["typeName"] - | ZodUploadDef["typeName"] - | ZodDateInDef["typeName"] - | ZodDateOutDef["typeName"]; - export type HandlingRules = Partial< Record< - z.ZodFirstPartyTypeKind | ProprietaryKinds, + z.ZodFirstPartyTypeKind | ProprietaryKind, SchemaHandler // keeping "any" here in order to avoid excessive complexity > >; @@ -58,10 +50,8 @@ export const walkSchema = ({ rules: HandlingRules; onMissing: SchemaHandler; }): U => { - const handler = - "typeName" in schema._def - ? rules[schema._def.typeName as keyof typeof rules] - : undefined; + const kind = getMeta(schema, "kind") || schema._def.typeName; + const handler = kind ? rules[kind as keyof typeof rules] : undefined; const ctx = rest as unknown as Context; const next: SchemaHandler = (params) => walkSchema({ ...params, ...ctx, onEach, rules: rules, onMissing }); diff --git a/src/upload-schema.ts b/src/upload-schema.ts index 92622e2ed..3bc6db3e7 100644 --- a/src/upload-schema.ts +++ b/src/upload-schema.ts @@ -1,50 +1,30 @@ import type { UploadedFile } from "express-fileupload"; -import { - INVALID, - OK, - ParseInput, - ParseReturnType, - ZodIssueCode, - ZodType, - ZodTypeDef, - addIssueToContext, - z, -} from "zod"; +import { z } from "zod"; +import { proprietary } from "./metadata"; import { bufferSchema } from "./schema-helpers"; -const zodUploadKind = "ZodUpload"; +export const ezUploadKind = "Upload"; -export interface ZodUploadDef extends ZodTypeDef { - typeName: typeof zodUploadKind; -} - -const uploadedFileSchema = z.object({ - name: z.string(), - encoding: z.string(), - mimetype: z.string(), - data: bufferSchema, - tempFilePath: z.string(), - truncated: z.boolean(), - size: z.number(), - md5: z.string(), - mv: z.function(), -}); - -export class ZodUpload extends ZodType { - override _parse(input: ParseInput): ParseReturnType { - if (uploadedFileSchema.safeParse(input.data).success) { - return OK(input.data); - } - const ctx = this._getOrReturnCtx(input); - addIssueToContext(ctx, { - code: ZodIssueCode.custom, - message: `Expected file upload, received ${ctx.parsedType}`, - }); - return INVALID; - } - - static create = () => - new ZodUpload({ - typeName: zodUploadKind, - }); -} +export const upload = () => + proprietary( + ezUploadKind, + z.custom( + (subject) => + z + .object({ + name: z.string(), + encoding: z.string(), + mimetype: z.string(), + data: bufferSchema, + tempFilePath: z.string(), + truncated: z.boolean(), + size: z.number(), + md5: z.string(), + mv: z.function(), + }) + .safeParse(subject).success, + (input) => ({ + message: `Expected file upload, received ${typeof input}`, + }), + ), + ); diff --git a/src/zts.ts b/src/zts.ts index 651d464cd..890089ac5 100644 --- a/src/zts.ts +++ b/src/zts.ts @@ -1,7 +1,10 @@ import ts from "typescript"; import { z } from "zod"; import { hasCoercion, tryToTransform } from "./common-helpers"; -import { ZodFile } from "./file-schema"; +import { ezDateInKind } from "./date-in-schema"; +import { ezDateOutKind } from "./date-out-schema"; +import { ezFileKind } from "./file-schema"; +import { RawSchema, ezRawKind } from "./raw-schema"; import { HandlingRules, walkSchema } from "./schema-walker"; import { LiteralType, @@ -208,18 +211,28 @@ const onLazy: Producer> = ({ ); }; -const onFile: Producer = ({ schema: { isBuffer } }) => - isBuffer - ? f.createTypeReferenceNode("Buffer") - : f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); +const onFile: Producer = ({ schema }) => { + const stringType = f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); + const bufferType = f.createTypeReferenceNode("Buffer"); + const unionType = f.createUnionTypeNode([stringType, bufferType]); + return schema instanceof z.ZodString + ? stringType + : schema instanceof z.ZodUnion + ? unionType + : bufferType; +}; + +const onRaw: Producer = ({ next, schema }) => + next({ schema: schema.shape.raw }); const producers: HandlingRules = { ZodString: onPrimitive(ts.SyntaxKind.StringKeyword), ZodNumber: onPrimitive(ts.SyntaxKind.NumberKeyword), ZodBigInt: onPrimitive(ts.SyntaxKind.BigIntKeyword), ZodBoolean: onPrimitive(ts.SyntaxKind.BooleanKeyword), - ZodDateIn: onPrimitive(ts.SyntaxKind.StringKeyword), - ZodDateOut: onPrimitive(ts.SyntaxKind.StringKeyword), + ZodAny: onPrimitive(ts.SyntaxKind.AnyKeyword), + [ezDateInKind]: onPrimitive(ts.SyntaxKind.StringKeyword), + [ezDateOutKind]: onPrimitive(ts.SyntaxKind.StringKeyword), ZodNull: onNull, ZodArray: onArray, ZodTuple: onTuple, @@ -228,8 +241,6 @@ const producers: HandlingRules = { ZodLiteral: onLiteral, ZodIntersection: onIntersection, ZodUnion: onSomeUnion, - ZodFile: onFile, - ZodAny: onPrimitive(ts.SyntaxKind.AnyKeyword), ZodDefault: onDefault, ZodEnum: onEnum, ZodNativeEnum: onNativeEnum, @@ -242,6 +253,8 @@ const producers: HandlingRules = { ZodPipeline: onPipeline, ZodLazy: onLazy, ZodReadonly: onReadonly, + [ezFileKind]: onFile, + [ezRawKind]: onRaw, }; export const zodToTs = ({ diff --git a/tests/unit/__snapshots__/documentation-helpers.spec.ts.snap b/tests/unit/__snapshots__/documentation-helpers.spec.ts.snap index ecc30761f..5bbd343cc 100644 --- a/tests/unit/__snapshots__/documentation-helpers.spec.ts.snap +++ b/tests/unit/__snapshots__/documentation-helpers.spec.ts.snap @@ -263,6 +263,20 @@ exports[`Documentation helpers > depictFile() > should set type:string and forma } `; +exports[`Documentation helpers > depictFile() > should set type:string and format accordingly 3 1`] = ` +{ + "format": "file", + "type": "string", +} +`; + +exports[`Documentation helpers > depictFile() > should set type:string and format accordingly 4 1`] = ` +{ + "format": "binary", + "type": "string", +} +`; + exports[`Documentation helpers > depictIntersection() > should wrap next depicters in allOf property 1`] = ` { "allOf": [ diff --git a/tests/unit/__snapshots__/upload-schema.spec.ts.snap b/tests/unit/__snapshots__/upload-schema.spec.ts.snap index 5fa9b469e..d0a2b9add 100644 --- a/tests/unit/__snapshots__/upload-schema.spec.ts.snap +++ b/tests/unit/__snapshots__/upload-schema.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`ZodUpload > _parse() > should accept UploadedFile 0 1`] = ` +exports[`ez.upload() > parsing > should accept UploadedFile 0 1`] = ` { "data": { "data": { @@ -30,7 +30,7 @@ exports[`ZodUpload > _parse() > should accept UploadedFile 0 1`] = ` } `; -exports[`ZodUpload > _parse() > should accept UploadedFile 1 1`] = ` +exports[`ez.upload() > parsing > should accept UploadedFile 1 1`] = ` { "data": { "data": { diff --git a/tests/unit/common-helpers.spec.ts b/tests/unit/common-helpers.spec.ts index 9e73ce882..fd34cd75a 100644 --- a/tests/unit/common-helpers.spec.ts +++ b/tests/unit/common-helpers.spec.ts @@ -19,7 +19,8 @@ import { import { InputValidationError, ez, withMeta } from "../../src"; import { Request } from "express"; import { z } from "zod"; -import { ZodUpload } from "../../src/upload-schema"; +import { isProprietary } from "../../src/metadata"; +import { ezUploadKind } from "../../src/upload-schema"; import { describe, expect, test } from "vitest"; describe("Common Helpers", () => { @@ -377,13 +378,10 @@ describe("Common Helpers", () => { }); describe("hasNestedSchema()", () => { + const condition = (subject: z.ZodTypeAny) => + isProprietary(subject, ezUploadKind); test("should return true for given argument satisfying condition", () => { - expect( - hasNestedSchema({ - subject: ez.upload(), - condition: (subject) => subject instanceof ZodUpload, - }), - ).toBeTruthy(); + expect(hasNestedSchema({ subject: ez.upload(), condition })).toBeTruthy(); }); test.each([ z.object({ test: ez.upload() }), @@ -396,12 +394,7 @@ describe("Common Helpers", () => { ez.upload().refine(() => true), z.array(ez.upload()), ])("should return true for wrapped needle %#", (subject) => { - expect( - hasNestedSchema({ - subject, - condition: (entry) => entry instanceof ZodUpload, - }), - ).toBeTruthy(); + expect(hasNestedSchema({ subject, condition })).toBeTruthy(); }); test.each([ z.object({}), @@ -410,12 +403,7 @@ describe("Common Helpers", () => { z.boolean().and(z.literal(true)), z.number().or(z.string()), ])("should return false in other cases %#", (subject) => { - expect( - hasNestedSchema({ - subject, - condition: (entry) => entry instanceof ZodUpload, - }), - ).toBeFalsy(); + expect(hasNestedSchema({ subject, condition })).toBeFalsy(); }); }); diff --git a/tests/unit/date-in-schema.spec.ts b/tests/unit/date-in-schema.spec.ts index 26554cdd2..074915405 100644 --- a/tests/unit/date-in-schema.spec.ts +++ b/tests/unit/date-in-schema.spec.ts @@ -1,18 +1,20 @@ -import { ZodDateIn } from "../../src/date-in-schema"; +import { z } from "zod"; +import { getMeta } from "../../src/metadata"; +import { ez } from "../../src"; import { describe, expect, test } from "vitest"; -describe("ZodDateIn", () => { - describe("static::create()", () => { +describe("ez.dateIn()", () => { + describe("creation", () => { test("should create an instance", () => { - const schema = ZodDateIn.create(); - expect(schema).toBeInstanceOf(ZodDateIn); - expect(schema._def.typeName).toEqual("ZodDateIn"); + const schema = ez.dateIn(); + expect(schema).toBeInstanceOf(z.ZodPipeline); + expect(getMeta(schema, "kind")).toEqual("DateIn"); }); }); - describe("_parse()", () => { + describe("parsing", () => { test("should handle wrong parsed type", () => { - const schema = ZodDateIn.create(); + const schema = ez.dateIn(); const result = schema.safeParse(123); expect(result.success).toBeFalsy(); if (!result.success) { @@ -29,7 +31,7 @@ describe("ZodDateIn", () => { }); test("should accept valid date string", () => { - const schema = ZodDateIn.create(); + const schema = ez.dateIn(); const result = schema.safeParse("2022-12-31"); expect(result).toEqual({ success: true, @@ -38,7 +40,7 @@ describe("ZodDateIn", () => { }); test("should handle invalid date", () => { - const schema = ZodDateIn.create(); + const schema = ez.dateIn(); const result = schema.safeParse("2022-01-32"); expect(result.success).toBeFalsy(); if (!result.success) { @@ -53,7 +55,7 @@ describe("ZodDateIn", () => { }); test("should handle invalid format", () => { - const schema = ZodDateIn.create(); + const schema = ez.dateIn(); const result = schema.safeParse("12.01.2021"); expect(result.success).toBeFalsy(); if (!result.success) { diff --git a/tests/unit/date-out-schema.spec.ts b/tests/unit/date-out-schema.spec.ts index c715b79a8..64052b70b 100644 --- a/tests/unit/date-out-schema.spec.ts +++ b/tests/unit/date-out-schema.spec.ts @@ -1,18 +1,20 @@ -import { ZodDateOut } from "../../src/date-out-schema"; +import { z } from "zod"; +import { getMeta } from "../../src/metadata"; +import { ez } from "../../src"; import { describe, expect, test } from "vitest"; -describe("ZodDateOut", () => { - describe("static::create()", () => { +describe("ez.dateOut()", () => { + describe("creation", () => { test("should create an instance", () => { - const schema = ZodDateOut.create(); - expect(schema).toBeInstanceOf(ZodDateOut); - expect(schema._def.typeName).toEqual("ZodDateOut"); + const schema = ez.dateOut(); + expect(schema).toBeInstanceOf(z.ZodEffects); + expect(getMeta(schema, "kind")).toEqual("DateOut"); }); }); - describe("_parse()", () => { + describe("parsing", () => { test("should handle wrong parsed type", () => { - const schema = ZodDateOut.create(); + const schema = ez.dateOut(); const result = schema.safeParse("12.01.2022"); expect(result.success).toBeFalsy(); if (!result.success) { @@ -29,7 +31,7 @@ describe("ZodDateOut", () => { }); test("should accept valid date", () => { - const schema = ZodDateOut.create(); + const schema = ez.dateOut(); const result = schema.safeParse(new Date("2022-12-31")); expect(result).toEqual({ success: true, @@ -38,7 +40,7 @@ describe("ZodDateOut", () => { }); test("should handle invalid date", () => { - const schema = ZodDateOut.create(); + const schema = ez.dateOut(); const result = schema.safeParse(new Date("2022-01-32")); expect(result.success).toBeFalsy(); if (!result.success) { diff --git a/tests/unit/documentation-helpers.spec.ts b/tests/unit/documentation-helpers.spec.ts index 6a35f510d..027eae5d7 100644 --- a/tests/unit/documentation-helpers.spec.ts +++ b/tests/unit/documentation-helpers.spec.ts @@ -318,18 +318,21 @@ describe("Documentation helpers", () => { }); describe("depictFile()", () => { - test.each([ez.file(), ez.file().binary(), ez.file().base64()])( - "should set type:string and format accordingly %#", - (schema) => { - expect( - depictFile({ - schema, - ...responseCtx, - next: makeNext(responseCtx), - }), - ).toMatchSnapshot(); - }, - ); + test.each([ + ez.file(), + ez.file("binary"), + ez.file("base64"), + ez.file("string"), + ez.file("buffer"), + ])("should set type:string and format accordingly %#", (schema) => { + expect( + depictFile({ + schema, + ...responseCtx, + next: makeNext(responseCtx), + }), + ).toMatchSnapshot(); + }); }); describe("depictUnion()", () => { diff --git a/tests/unit/file-schema.spec.ts b/tests/unit/file-schema.spec.ts index fa3d73355..864788295 100644 --- a/tests/unit/file-schema.spec.ts +++ b/tests/unit/file-schema.spec.ts @@ -1,90 +1,78 @@ -import { ZodFile } from "../../src/file-schema"; +import { expectType } from "tsd"; +import { z } from "zod"; +import { getMeta } from "../../src/metadata"; +import { ez } from "../../src"; import { readFile } from "node:fs/promises"; import { describe, expect, test } from "vitest"; -describe("ZodFile", () => { - describe("static::create()", () => { +describe("ez.file()", () => { + describe("creation", () => { test("should create an instance being string by default", () => { - const schema = ZodFile.create(); - expect(schema).toBeInstanceOf(ZodFile); - expect(schema._def.encoding).toBeUndefined(); - expect(schema._def.typeName).toEqual("ZodFile"); - expect(schema.isBinary).toBeFalsy(); - expect(schema.isBase64).toBeFalsy(); - expect(schema.isString).toBeTruthy(); - expect(schema.isBuffer).toBeFalsy(); + const schema = ez.file(); + expect(schema).toBeInstanceOf(z.ZodString); + expect(getMeta(schema, "kind")).toBe("File"); }); - }); - describe(".string()", () => { - test("should create a string file", () => { - const schema = ZodFile.create().string(); - expect(schema).toBeInstanceOf(ZodFile); - expect(schema._def.encoding).toBeUndefined(); - expect(schema.isString).toBeTruthy(); - expect(schema.isBuffer).toBeFalsy(); - }); - }); + test.each([ez.file("string"), ez.file().string("deprecated message")])( + "should create a string file", + (schema) => { + expect(schema).toBeInstanceOf(z.ZodString); + expectType(schema._output); + }, + ); - describe(".buffer()", () => { - test("should create a buffer file", () => { - const schema = ZodFile.create().buffer(); - expect(schema).toBeInstanceOf(ZodFile); - expect(schema._def.encoding).toBeUndefined(); - expect(schema.isBuffer).toBeTruthy(); - expect(schema.isString).toBeFalsy(); - }); - }); + test.each([ez.file("buffer"), ez.file().buffer("deprecated message")])( + "should create a buffer file", + (schema) => { + expect(schema).toBeInstanceOf(z.ZodEffects); + expectType(schema._output); + }, + ); - describe(".binary()", () => { - test("should create a binary file", () => { - const schema = ZodFile.create().binary("test message"); - expect(schema).toBeInstanceOf(ZodFile); - expect(schema.isBinary).toBeTruthy(); - expect(schema._def.encoding).toBe("binary"); - expect(schema._def.message).toBe("test message"); - }); - }); + test.each([ez.file("binary"), ez.file().binary("deprecated message")])( + "should create a binary file", + (schema) => { + expect(schema).toBeInstanceOf(z.ZodUnion); + expectType(schema._output); + }, + ); - describe(".base64()", () => { - test("should create a base64 file", () => { - const schema = ZodFile.create().base64("test message"); - expect(schema).toBeInstanceOf(ZodFile); - expect(schema.isBase64).toBeTruthy(); - expect(schema._def.encoding).toBe("base64"); - expect(schema._def.message).toBe("test message"); - }); + test.each([ez.file("base64"), ez.file().base64("deprecated message")])( + "should create a base64 file", + (schema) => { + expect(schema).toBeInstanceOf(z.ZodString); + expectType(schema._output); + }, + ); }); - describe("_parse()", () => { + describe("parsing", () => { test.each([ { - schema: ZodFile.create(), + schema: ez.file(), subject: 123, + code: "invalid_type", expected: "string", received: "number", message: "Expected string, received number", }, { - schema: ZodFile.create().buffer(), + schema: ez.file("buffer"), subject: "123", - expected: "object", - received: "string", + code: "custom", message: "Expected Buffer", + fatal: true, }, ])( "should invalidate wrong types", - ({ schema, subject, expected, received, message }) => { + ({ schema, subject, ...expectedError }) => { const result = schema.safeParse(subject); expect(result.success).toBeFalsy(); if (!result.success) { expect(result.error.issues).toEqual([ { - code: "invalid_type", - expected, - message, + ...expectedError, path: [], - received, }, ]); } @@ -92,14 +80,15 @@ describe("ZodFile", () => { ); test("should perform additional check for base64 file", () => { - const schema = ZodFile.create().base64(); + const schema = ez.file("base64"); const result = schema.safeParse("~~~~"); expect(result.success).toBeFalsy(); if (!result.success) { expect(result.error.issues).toEqual([ { - code: "custom", + code: "invalid_string", message: "Does not match base64 encoding", + validation: "regex", path: [], }, ]); @@ -107,7 +96,7 @@ describe("ZodFile", () => { }); test("should accept string", () => { - const schema = ZodFile.create(); + const schema = ez.file(); const result = schema.safeParse("some string"); expect(result).toEqual({ success: true, @@ -116,7 +105,7 @@ describe("ZodFile", () => { }); test("should accept Buffer", () => { - const schema = ZodFile.create().buffer(); + const schema = ez.file("buffer"); const subject = Buffer.from("test", "utf-8"); const result = schema.safeParse(subject); expect(result).toEqual({ @@ -126,7 +115,7 @@ describe("ZodFile", () => { }); test("should accept binary read string", async () => { - const schema = ZodFile.create().binary(); + const schema = ez.file("binary"); const data = await readFile("logo.svg", "binary"); const result = schema.safeParse(data); expect(result).toEqual({ @@ -136,7 +125,7 @@ describe("ZodFile", () => { }); test("should accept base64 read string", async () => { - const schema = ZodFile.create().base64(); + const schema = ez.file("base64"); const data = await readFile("logo.svg", "base64"); const result = schema.safeParse(data); expect(result).toEqual({ diff --git a/tests/unit/index.spec.ts b/tests/unit/index.spec.ts index a33f5b6af..b69769362 100644 --- a/tests/unit/index.spec.ts +++ b/tests/unit/index.spec.ts @@ -23,10 +23,6 @@ import { ResultHandlerDefinition, Routing, ServerConfig, - ZodDateInDef, - ZodDateOutDef, - ZodFileDef, - ZodUploadDef, } from "../../src"; import { describe, expect, test, vi } from "vitest"; @@ -55,10 +51,6 @@ describe("Index Entrypoint", () => { expectType({}); expectType({}); expectType>({ examples: [] }); - expectType({ typeName: "ZodDateIn" }); - expectType({ typeName: "ZodDateOut" }); - expectType({ typeName: "ZodFile", type: "" }); - expectType({ typeName: "ZodUpload" }); expectType({ cors: true, logger: { level: "silent" } }); expectType({ app: {} as IRouter, diff --git a/tests/unit/upload-schema.spec.ts b/tests/unit/upload-schema.spec.ts index a6645ce50..034ea45f8 100644 --- a/tests/unit/upload-schema.spec.ts +++ b/tests/unit/upload-schema.spec.ts @@ -1,24 +1,27 @@ -import { ZodUpload } from "../../src/upload-schema"; +import { getMeta } from "../../src/metadata"; +import { z } from "zod"; +import { ez } from "../../src"; import { describe, expect, test, vi } from "vitest"; -describe("ZodUpload", () => { - describe("static::create()", () => { +describe("ez.upload()", () => { + describe("creation", () => { test("should create an instance", () => { - const schema = ZodUpload.create(); - expect(schema).toBeInstanceOf(ZodUpload); - expect(schema._def.typeName).toEqual("ZodUpload"); + const schema = ez.upload(); + expect(schema).toBeInstanceOf(z.ZodEffects); + expect(getMeta(schema, "kind")).toBe("Upload"); }); }); - describe("_parse()", () => { + describe("parsing", () => { test("should handle wrong parsed type", () => { - const schema = ZodUpload.create(); + const schema = ez.upload(); const result = schema.safeParse(123); expect(result.success).toBeFalsy(); if (!result.success) { expect(result.error.issues).toEqual([ { code: "custom", + fatal: true, message: "Expected file upload, received number", path: [], }, @@ -29,7 +32,7 @@ describe("ZodUpload", () => { test.each([vi.fn(async () => {}), vi.fn(() => {})])( "should accept UploadedFile %#", (mv) => { - const schema = ZodUpload.create(); + const schema = ez.upload(); const buffer = Buffer.from("something"); const result = schema.safeParse({ name: "avatar.jpg",