Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions express-zod-api/tests/env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ describe("Environment checks", () => {
expect(schema.meta()).toMatchSnapshot();
});

test("metadata is inheritable since zod 4.3.0", () => {
const parent = z.string().meta({ one: "test" });
const subject = parent.min(1).meta({ two: "another" });
expect(subject.meta()).toHaveProperty("one", "test");
});

test("object shape conveys the keys optionality", () => {
const schema = z.object({
one: z.boolean(),
Expand Down
4 changes: 2 additions & 2 deletions express-zod-api/tests/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ts from "typescript";
import { z } from "zod";
import { globalRegistry, z } from "zod";
import {
EndpointsFactory,
Integration,
Expand Down Expand Up @@ -133,7 +133,7 @@ describe("Integration", () => {
schema: ReturnType<z.ZodType["brand"]>,
{ next },
) => {
schema._zod.bag.brand = undefined;
globalRegistry.remove(schema);
return next(schema);
};
const client = await Integration.create({
Expand Down
2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ catalogs:
prod:
"ramda": "^0.32.0"
peer:
"zod": "^4.1.13"
"zod": "^4.3.4"
dev:
"@types/compression": "^1.8.1"
"@types/express": "^5.0.6"
Expand Down
9 changes: 9 additions & 0 deletions zod-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Version 4

### v4.0.0

- Supported `zod` versions: `^4.3.4`;
- Zod made metadata inheritable, so that `pack()` and `unpack()` are removed;
- Use the `.meta()` method of a schema instead of both helpers;
- Runtime distinguishable brand is now stored along with other metadata.

## Version 3

### v3.0.0
Expand Down
12 changes: 5 additions & 7 deletions zod-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ This module extends Zod functionality when it's imported:
- Adds `.remap()` method to `ZodObject` for renaming object properties:
- Supports a mapping object or an object transforming function as an argument;
- Relies on `R.renameKeys()` from the `ramda` library;
- Alters the `.brand()` method on all Zod schemas by making the assigned brand available in runtime:
- The brand set this way will withstand refinements, unlike the metadata set by `.meta()`.
- Alters the `.brand()` method on all Zod schemas:
- shorthand for `.meta({ "x-brand": ... })` making the brand available in runtime via `getBrand()` helper;

## Requirements

- Zod `^4.1.13`
- Zod `^4.3.4`

## Basic usage

Expand All @@ -29,11 +29,9 @@ import { getBrand } from "@express-zod-api/zod-plugin";
const schema = z.string().example("test").example("another").brand("custom");

getBrand(schema); // "custom"
schema.meta(); // { examples: ["test", "another"] }
schema.meta(); // { examples: ["test", "another"], "x-brand": "custom" }
```

## Helpers

- `getBrand()` — retrieves the brand from the schema that was set by its `.brand()` method;
- `pack()` — returns a cloned schema having inheritable attributes assigned (such as brand);
- `unpack()` — retrieves the attributes from the schema that was set by `pack()` helper.
- `getBrand()` — retrieves the brand from the schema that was set by its `.brand()` method.
2 changes: 1 addition & 1 deletion zod-plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@express-zod-api/zod-plugin",
"version": "3.0.0",
"version": "4.0.0-beta.1",
"license": "MIT",
"description": "Zod plugin for express-zod-api",
"sideEffects": true,
Expand Down
14 changes: 4 additions & 10 deletions zod-plugin/src/brand.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { z } from "zod";
import { pack, unpack } from "./packer";
import { globalRegistry, z } from "zod";

/** The property within schema._zod.bag where we store the brand */
export const brandProperty = "brand" as const;

/** Used by runtime (bound) */
export const setBrand = function (this: z.ZodType, brand?: PropertyKey) {
return pack(this, { [brandProperty]: brand });
};
/** The property we store the brand in */
export const brandProperty = "x-brand" as const;

/**
* @public
* @desc Retrieves the brand from the schema set by its .brand() method.
* */
export const getBrand = (subject: z.core.$ZodType) => {
const { [brandProperty]: brand } = unpack(subject) || {};
const { [brandProperty]: brand } = globalRegistry.get(subject) || {};
if (
typeof brand === "symbol" ||
typeof brand === "string" ||
Expand Down
1 change: 0 additions & 1 deletion zod-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
import "./runtime"; // side effects here
export { pack, unpack } from "./packer";
export { getBrand } from "./brand";
5 changes: 5 additions & 0 deletions zod-plugin/src/meta.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { globalRegistry, type z } from "zod";
import { brandProperty } from "./brand";

export const exampleSetter = function (
this: z.ZodType,
Expand All @@ -16,3 +17,7 @@ export const deprecationSetter = function (this: z.ZodType) {
export const labelSetter = function (this: z.ZodDefault, defaultLabel: string) {
return this.meta({ default: defaultLabel });
};

export const brandSetter = function (this: z.ZodType, brand?: PropertyKey) {
return this.meta({ [brandProperty]: brand });
};
43 changes: 0 additions & 43 deletions zod-plugin/src/packer.ts

This file was deleted.

10 changes: 7 additions & 3 deletions zod-plugin/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { z } from "zod";
import { setBrand } from "./brand";
import { remap } from "./remap";
import { deprecationSetter, exampleSetter, labelSetter } from "./meta";
import {
deprecationSetter,
exampleSetter,
labelSetter,
brandSetter,
} from "./meta";
import { getClasses, getPackages } from "./packages";

// eslint-disable-next-line no-restricted-syntax -- substituted by TSDOWN
Expand All @@ -23,7 +27,7 @@ if (!(pluginFlag in globalThis)) {
["brand" satisfies keyof z.ZodType]: {
set() {}, // this is required to override the existing method
get() {
return setBrand.bind(this) as z.ZodType["brand"];
return brandSetter.bind(this) as z.ZodType["brand"];
},
},
});
Expand Down
32 changes: 9 additions & 23 deletions zod-plugin/tests/brand.spec.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,20 @@
import { z } from "zod";
import { brandProperty, getBrand, setBrand } from "../src/brand";
import * as packer from "../src/packer";
import { z, globalRegistry } from "zod";
import { brandProperty, getBrand } from "../src/brand";

describe("Brand", () => {
describe("brandProperty", () => {
test("should be brand", () => {
expect(brandProperty).toBe("brand");
});
});

describe("setBrand", () => {
const packMock = vi.spyOn(packer, "pack");

afterAll(() => {
packMock.mockRestore();
});

test("calls pack() with given brand", () => {
const schema = z.string();
setBrand.call(schema, "test");
expect(packMock).toHaveBeenCalledWith(schema, { brand: "test" });
expect(brandProperty).toBe("x-brand");
});
});

describe("getBrand", () => {
test.each([{ brand: "test" }, {}, undefined])(
"should take it from bag",
(bag) => {
const mock = { _zod: { bag } };
expect(getBrand(mock as unknown as z.core.$ZodType)).toBe(bag?.brand);
test.each([{ [brandProperty]: "test" }, {}, undefined])(
"should take it from metadata in globalRegistry %#",
(metadata) => {
const subject = z.string();
if (metadata) globalRegistry.add(subject, metadata);
expect(getBrand(subject)).toBe(metadata?.[brandProperty]);
},
);
});
Expand Down
2 changes: 0 additions & 2 deletions zod-plugin/tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ describe("Entrypoint", () => {
test("Exports", () => {
expect(entrypoint).toMatchObject({
getBrand: expect.any(Function),
pack: expect.any(Function),
unpack: expect.any(Function),
});
});
});
43 changes: 0 additions & 43 deletions zod-plugin/tests/packer.spec.ts

This file was deleted.

15 changes: 10 additions & 5 deletions zod-plugin/tests/runtime.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createRequire } from "node:module";
import camelize from "camelize-ts";
import { z as zESM } from "zod";
import { getBrand } from "../src";

const { z: zCJS } = createRequire(import.meta.url)("zod");

Expand Down Expand Up @@ -70,19 +69,25 @@ describe.each<{ variant: string; z: typeof zESM }>([

describe(".brand()", () => {
test("should set the brand", () => {
expect(getBrand(z.string().brand("test"))).toBe("test");
const subject = z.string().brand("test");
expect(subject.meta()).toEqual({ "x-brand": "test" });
});

test("should withstand refinements", () => {
const schema = z.string();
const schemaWithMeta = schema.brand("test");
expect(getBrand(schemaWithMeta)).toBe("test");
expect(getBrand(schemaWithMeta.regex(/@example.com$/))).toBe("test");
expect(schemaWithMeta.meta()).toEqual({ "x-brand": "test" });
expect(schemaWithMeta.regex(/@example.com$/).meta()).toEqual({
"x-brand": "test",
});
});

test("should withstand describing", () => {
const schema = z.string().brand("test").describe("something");
expect(getBrand(schema)).toBe("test");
expect(schema.meta()).toEqual({
"x-brand": "test",
description: "something",
});
});
});

Expand Down