Skip to content

Commit

Permalink
Split extended zod into separate package @nest-zod/z (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
BenLorantfy authored Oct 17, 2024
1 parent 4e3564b commit 4f65783
Show file tree
Hide file tree
Showing 57 changed files with 1,086 additions and 383 deletions.
23 changes: 17 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,35 @@ jobs:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- run: pnpm test
- run: pnpm build
- name: Build z
run: cd packages/z && pnpm build
- name: Test z
run: cd packages/z && pnpm test
- name: Test nestjs-zod
run: cd packages/nestjs-zod && pnpm test
- name: Build nestjs-zod
run: cd packages/nestjs-zod && pnpm build
- name: Build example app
run: cd packages/example && pnpm run build
- name: Test example app
run: cd packages/example && pnpm run test:e2e

- name: Extract version
id: version
uses: olegtarasov/[email protected]
uses: olegtarasov/[email protected].3
with:
tagRegex: 'v(.*)'

- name: Set version from release
uses: reedyuk/npm-version@1.0.1
uses: reedyuk/npm-version@1.1.1
with:
version: ${{ steps.version.outputs.tag }}
package: 'packages/nestjs-zod'

- name: Create NPM config
run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
run: cd packages/nestjs-zod && npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish to NPM
run: npm publish
run: cd packages/nestjs-zod && npm publish
16 changes: 13 additions & 3 deletions .github/workflows/test-and-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: ['main']
pull_request:
branches: ['main', 'release/alpha', 'release/beta', 'release/next']
branches: ['main']

jobs:
test-and-build:
Expand All @@ -21,5 +21,15 @@ jobs:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- run: cd packages/nestjs-zod && pnpm test
- run: cd packages/nestjs-zod &&pnpm build
- name: Build z
run: cd packages/z && pnpm build
- name: Test z
run: cd packages/z && pnpm test
- name: Test nestjs-zod
run: cd packages/nestjs-zod && pnpm test
- name: Build nestjs-zod
run: cd packages/nestjs-zod && pnpm build
- name: Build example app
run: cd packages/example && pnpm run build
- name: Test example app
run: cd packages/example && pnpm run test:e2e
22 changes: 22 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Migration

## From version 3.x to 4.x

### `nestjs-zod/z` is now `@nest-zod/z`
The extended zod api was moved out of the main package to a separate package. This requires a slight change to the import path:
```diff
- import { z } from 'nestjs-zod/z'
+ import { z } from '@nest-zod/z'
```
Additionally, `@nest-zod/z` is deprecated and will not be supported soon. This is because the way `@nest-zod/z` extends `zod` is brittle and breaks in patch versions of zod. If you still want to use the functionality of `password` and `dateString`, you can implement the same logic using [refine()](https://zod.dev/?id=refine)

> [!CAUTION]
> It is highly recommended to move towards importing `zod` directly, instead of `@nest-zod/z`
### `nestjs-zod/frontend` is removed
The same exports are now available in `@nest-zod/z/frontend` (see details about `@nest-zod/z` above). This requires a slight change to the import path:
```diff
- import { isNestJsZodIssue } from 'nestjs-zod/frontend'
+ import { isNestJsZodIssue } from '@nest-zod/z/frontend'
```
`@nest-zod/z/frontend` is also deprecated and will not be supported soon, as explained above.
65 changes: 32 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
- `@nestjs/swagger` integration using the patch
- `zodToOpenAPI` - generate highly accurate Swagger Schema
- Zod DTOs can be used in any `@nestjs/swagger` decorator
- Extended Zod schemas for NestJS (`nestjs-zod/z`)
- Extended Zod schemas for NestJS (`@nest-zod/z`)
- `dateString` for dates (supports casting to `Date`)
- `password` for passwords (more complex string rules + OpenAPI conversion)
- Customization - change exception format easily
Expand All @@ -64,7 +64,6 @@ All peer dependencies are marked as optional for better client side usage, but y

## Navigation

- [Writing Zod schemas](#writing-zod-schemas)
- [Creating DTO from Zod schema](#creating-dto-from-zod-schema)
- [Using DTO](#using-dto)
- [Using ZodValidationPipe](#using-zodvalidationpipe)
Expand All @@ -87,32 +86,11 @@ All peer dependencies are marked as optional for better client side usage, but y
- [Writing more Swagger-compatible schemas](#writing-more-swagger-compatible-schemas)
- [Using zodToOpenAPI](#using-zodtoopenapi)

## Writing Zod schemas

Extended Zod and Swagger integration are bound to the internal API, so even the patch updates can cause errors.

For that reason, `nestjs-zod` uses specific `zod` version inside and re-exports it under `/z` scope:

```ts
import { z, ZodString, ZodError } from 'nestjs-zod/z'

const CredentialsSchema = z.object({
username: z.string(),
password: z.string(),
})
```

Zod's classes and types are re-exported too, but under `/z` scope for more clarity:

```ts
import { ZodString, ZodError, ZodIssue } from 'nestjs-zod/z'
```

## Creating DTO from Zod schema

```ts
import { createZodDto } from 'nestjs-zod'
import { z } from 'nestjs-zod/z'
import { z } from 'zod'

const CredentialsSchema = z.object({
username: z.string(),
Expand Down Expand Up @@ -378,10 +356,16 @@ In the above example, despite the `userService.findOne` method returns `password

## Extended Zod

As you learned in [Writing Zod Schemas](#writing-zod-schemas) section, `nestjs-zod` provides a special version of Zod. It helps you to validate the user input more accurately by using our custom schemas and methods.
> [!CAUTION]
> `@nest-zod/z` is deprecated and will not be supported soon. It is recommended to use `zod` directly. See [MIGRATION.md](./MIGRATION.md) for more information.
`@nest-zod/z` provides a special version of Zod. It helps you to validate the user input more accurately by using our custom schemas and methods.

### ZodDateString

> [!CAUTION]
> `@nest-zod/z` is deprecated and will not be supported soon. It is recommended to use `zod` directly. See [MIGRATION.md](./MIGRATION.md) for more information.
In HTTP, we always accept Dates as strings. But default Zod only has validations for full date-time strings. `ZodDateString` was created to address this issue.

```ts
Expand Down Expand Up @@ -458,6 +442,9 @@ Errors:

### ZodPassword

> [!CAUTION]
> `@nest-zod/z` is deprecated and will not be supported soon. It is recommended to use `zod` directly. See [MIGRATION.md](./MIGRATION.md) for more information.
`ZodPassword` is a string-like type, just like the `ZodDateString`. As you might have guessed, it's intended to help you with password schemas definition.

Also, `ZodPassword` has a more accurate OpenAPI conversion, comparing to regular `.string()`: it has `password` format and generated RegExp string for `pattern`.
Expand Down Expand Up @@ -496,6 +483,9 @@ Errors:

### Json Schema

> [!CAUTION]
> `@nest-zod/z` is deprecated and will not be supported soon. It is recommended to use `zod` directly. See [MIGRATION.md](./MIGRATION.md) for more information.
> Created for `nestjs-zod-prisma`
```ts
Expand All @@ -504,6 +494,9 @@ z.json()

### "from" function

> [!CAUTION]
> `@nest-zod/z` is deprecated and will not be supported soon. It is recommended to use `zod` directly. See [MIGRATION.md](./MIGRATION.md) for more information.
> Created for custom schemas in `nestjs-zod-prisma`
Just returns the same Schema
Expand All @@ -514,6 +507,9 @@ z.from(MySchema)

### Extended Zod Errors

> [!CAUTION]
> `@nest-zod/z` is deprecated and will not be supported soon. It is recommended to use `zod` directly. See [MIGRATION.md](./MIGRATION.md) for more information.
Currently, we use `custom` error code due to some Zod limitations (`errorMap` priorities)

Therefore, the error details is located inside `params` property:
Expand All @@ -535,12 +531,16 @@ const error = {

### Working with errors on the client side

Optionally, you can install `nestjs-zod` on the client side.
> [!CAUTION]
> `@nest-zod/z/frontend` is deprecated and will not be supported soon. It is recommended to use `zod` directly. See [MIGRATION.md](./MIGRATION.md) for more information.

Optionally, you can install `@nest-zod/z` on the client side.

The library provides you a `/frontend` scope, that can be used to detect custom NestJS Zod issues and process them the way you want.
The library provides you a `@nest-zod/z/frontend` entry point, that can be used to detect custom NestJS Zod issues and process them the way you want.

```ts
import { isNestJsZodIssue, NestJsZodIssue, ZodIssue } from 'nestjs-zod/frontend'
import { isNestJsZodIssue, NestJsZodIssue, ZodIssue } from '@nest-zod/z/frontend'

function mapToFormErrors(issues: ZodIssue[]) {
for (const issue of issues) {
Expand All @@ -551,7 +551,7 @@ function mapToFormErrors(issues: ZodIssue[]) {
}
```

> :warning: **If you use `zod` in your client-side application, and you want to install `nestjs-zod` too, it may be better to completely switch to `nestjs-zod` to prevent issues caused by mismatch between `zod` versions. `nestjs-zod/frontend` doesn't use `zod` at the runtime, but it uses its types.**
> :warning: **If you use `zod` in your client-side application, and you want to install `@nest-zod/z` too, it may be better to completely switch to `@nest-zod/z` to prevent issues caused by mismatch between `zod` versions. `@nest-zod/z/frontend` doesn't use `zod` at the runtime, but it uses its types.**
## OpenAPI (Swagger) support

Expand All @@ -576,7 +576,7 @@ Then follow the [Nest.js' Swagger Module Guide](https://docs.nestjs.com/openapi/
Use `.describe()` method to add Swagger description:

```ts
import { z } from 'nestjs-zod/z'
import { z } from 'zod'

const CredentialsSchema = z.object({
username: z.string().describe('This is an username'),
Expand All @@ -590,16 +590,15 @@ You can convert any Zod schema to an OpenAPI JSON object:

```ts
import { zodToOpenAPI } from 'nestjs-zod'
import { z } from 'nestjs-zod/z'
import { z } from 'zod'

const SignUpSchema = z.object({
username: z.string().min(8).max(20),
password: z.string().min(8).max(20),
sex: z
.enum(['male', 'female', 'nonbinary'])
.describe('We respect your gender choice'),
social: z.record(z.string().url()),
birthDate: z.dateString().past(),
social: z.record(z.string().url())
})

const openapi = zodToOpenAPI(SignUpSchema)
Expand Down
7 changes: 4 additions & 3 deletions packages/example/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "nestjs-zod-example",
"version": "0.0.1",
"description": "",
"author": "",
"description": "Example app showing how to use nestjs-zod",
"author": "Ben Lorantfy <[email protected]>",
"private": true,
"license": "UNLICENSED",
"scripts": {
Expand All @@ -24,7 +24,8 @@
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.4.2",
"nestjs-zod": "0.0.0-set-by-ci",
"@nest-zod/z": "workspace:*",
"nestjs-zod": "workspace:*",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"zod": "^3.23.8"
Expand Down
23 changes: 20 additions & 3 deletions packages/example/src/posts/posts.controller.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
import { Body, Controller, Post } from '@nestjs/common';
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { ApiOkResponse } from '@nestjs/swagger';
import { createZodDto } from 'nestjs-zod'
import { z } from 'nestjs-zod/z'
import { z } from 'zod'

class PostDto extends createZodDto(z.object({
title: z.string().describe('The title of the post'),
content: z.string().describe('The content of the post'),
authorId: z.number().describe('The ID of the author of the post'),
})) {}

@Controller('posts')
export class PostsController {
@Post()
createPost(@Body() body: PostDto) {
console.log(body);
return body;
}

@Get()
@ApiOkResponse({ type: [PostDto], description: 'Get all posts' })
getAll() {
return [];
}

@Get(':id')
@ApiOkResponse({ type: PostDto, description: 'Get a post by ID' })
getById(@Param('id') id: string) {
return {
title: 'Hello',
content: 'World',
authorId: 1,
};
}
}
70 changes: 70 additions & 0 deletions packages/example/test/posts.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('PostsController (e2e)', () => {
let app: INestApplication;

beforeEach(async () => {
app = await createApp();
});

test('POST /posts - should validate input using Zod', async () => {
const validPost = {
title: 'Test Post',
content: 'This is a test post content.',
authorId: 1
};

const invalidPost = {
title: 'Test Post',
content: 'This is a test post content.',
authorId: 'not a number' // Should be a number
};

// Test with valid data
await request(app.getHttpServer())
.post('/posts')
.send(validPost)
.expect(201) // Assuming 201 is returned on successful creation
.expect((res) => {
expect(res.body).toEqual({
title: validPost.title,
content: validPost.content,
authorId: validPost.authorId
})
});

// Test with invalid data
await request(app.getHttpServer())
.post('/posts')
.send(invalidPost)
.expect(400) // Bad request due to validation failure
.expect((res) => {
expect(res.body).toEqual({
statusCode: 400,
message: 'Validation failed',
errors: [
{
code: 'invalid_type',
expected: 'number',
received: 'string',
path: ['authorId'],
message: 'Expected number, received string'
}
]
});
});
});
});

async function createApp() {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

const app = moduleFixture.createNestApplication();
await app.init();
return app;
}
Loading

0 comments on commit 4f65783

Please sign in to comment.