diff --git a/eslint.config.js b/eslint.config.js index 60798d6f9c..1f5dce87a3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -216,7 +216,7 @@ export default tsPlugin.config( name: "source/ez", files: ["express-zod-api/src/*.ts"], rules: { - complexity: ["error", 20], + complexity: ["error", 18], "allowed/dependencies": ["error", { packageDir: ezDir }], "no-restricted-syntax": [ "warn", diff --git a/express-zod-api/bench/experiment.bench.ts b/express-zod-api/bench/experiment.bench.ts index e79364dc37..8d7ce7446d 100644 --- a/express-zod-api/bench/experiment.bench.ts +++ b/express-zod-api/bench/experiment.bench.ts @@ -1,14 +1,56 @@ -import * as R from "ramda"; import { bench } from "vitest"; +import type { UploadedFile } from "express-fileupload"; +import { isObjectOfUploadShape } from "../src/upload-schema"; +import { z } from "zod"; -describe("Experiment for unique elements", () => { - const current = ["one", "two"]; +describe("Experiment for upload schema", () => { + const current = () => + z.custom( + (subject) => + typeof subject === "object" && + subject !== null && + "name" in subject && + "encoding" in subject && + "mimetype" in subject && + "data" in subject && + "tempFilePath" in subject && + "truncated" in subject && + "size" in subject && + "md5" in subject && + "mv" in subject && + typeof subject.name === "string" && + typeof subject.encoding === "string" && + typeof subject.mimetype === "string" && + Buffer.isBuffer(subject.data) && + typeof subject.tempFilePath === "string" && + typeof subject.truncated === "boolean" && + typeof subject.size === "number" && + typeof subject.md5 === "string" && + typeof subject.mv === "function", + ); - bench("set", () => { - return void [...new Set(current).add("null")]; + const featured = () => + z.custom( + (subject) => + isObjectOfUploadShape(subject) && + typeof subject.name === "string" && + typeof subject.encoding === "string" && + typeof subject.mimetype === "string" && + Buffer.isBuffer(subject.data) && + typeof subject.tempFilePath === "string" && + typeof subject.truncated === "boolean" && + typeof subject.size === "number" && + typeof subject.md5 === "string" && + typeof subject.mv === "function", + ); + + bench("current", () => { + const one = current(); + one.safeParse({}); }); - bench("R.union", () => { - return void R.union(current, ["null"]); + bench("featured", () => { + const one = featured(); + one.safeParse({}); }); }); diff --git a/express-zod-api/src/upload-schema.ts b/express-zod-api/src/upload-schema.ts index e6aa2c906b..0943b8af17 100644 --- a/express-zod-api/src/upload-schema.ts +++ b/express-zod-api/src/upload-schema.ts @@ -1,23 +1,27 @@ import type { UploadedFile } from "express-fileupload"; import { z } from "zod"; +import { isObject } from "./common-helpers"; export const ezUploadBrand = Symbol("Upload"); +/** @internal */ +export const isObjectOfUploadShape = (subject: unknown) => + isObject(subject) && + "name" in subject && + "encoding" in subject && + "mimetype" in subject && + "data" in subject && + "tempFilePath" in subject && + "truncated" in subject && + "size" in subject && + "md5" in subject && + "mv" in subject; + export const upload = () => z .custom( (subject) => - typeof subject === "object" && - subject !== null && - "name" in subject && - "encoding" in subject && - "mimetype" in subject && - "data" in subject && - "tempFilePath" in subject && - "truncated" in subject && - "size" in subject && - "md5" in subject && - "mv" in subject && + isObjectOfUploadShape(subject) && typeof subject.name === "string" && typeof subject.encoding === "string" && typeof subject.mimetype === "string" && diff --git a/express-zod-api/tests/upload-schema.spec.ts b/express-zod-api/tests/upload-schema.spec.ts index d6c4f671ad..f5c81a5d87 100644 --- a/express-zod-api/tests/upload-schema.spec.ts +++ b/express-zod-api/tests/upload-schema.spec.ts @@ -1,9 +1,60 @@ import { z } from "zod"; import { ez } from "../src"; import { getBrand } from "@express-zod-api/zod-plugin"; -import { ezUploadBrand } from "../src/upload-schema"; +import { ezUploadBrand, isObjectOfUploadShape } from "../src/upload-schema"; describe("ez.upload()", () => { + describe("isObjectOfUploadShape() helper", () => { + test.each([null, undefined, "test", 123, false])( + "should return false for non-object %#", + (subject) => { + expect(isObjectOfUploadShape(subject)).toBe(false); + }, + ); + + test.each([ + "name", + "encoding", + "mimetype", + "data", + "tempFilePath", + "truncated", + "size", + "md5", + "mv", + ] as const)("should return false when missing %s key", (key) => { + const input = { + name: null, + encoding: null, + mimetype: null, + data: null, + tempFilePath: null, + truncated: null, + size: null, + md5: null, + mv: null, + }; + delete input[key]; + expect(isObjectOfUploadShape(input)).toBe(false); + }); + + test("should return true for an object of valid shape", () => { + expect( + isObjectOfUploadShape({ + name: null, + encoding: null, + mimetype: null, + data: null, + tempFilePath: null, + truncated: null, + size: null, + md5: null, + mv: null, + }), + ).toBe(true); + }); + }); + describe("creation", () => { test("should create an instance", () => { const schema = ez.upload();