Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split extended zod into separate package @nest-zod/z #105

Merged
merged 8 commits into from
Oct 17, 2024
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
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