diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cdcf204..14801d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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/get-tag@v2.1 + uses: olegtarasov/get-tag@v2.1.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 \ No newline at end of file + run: cd packages/nestjs-zod && npm publish \ No newline at end of file diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index 7d2d854..b934dee 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -4,7 +4,7 @@ on: push: branches: ['main'] pull_request: - branches: ['main', 'release/alpha', 'release/beta', 'release/next'] + branches: ['main'] jobs: test-and-build: @@ -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 \ No newline at end of file + - 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 diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..db05ac6 --- /dev/null +++ b/MIGRATION.md @@ -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. diff --git a/README.md b/README.md index ba76a39..5907eed 100755 --- a/README.md +++ b/README.md @@ -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 @@ -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) @@ -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(), @@ -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 @@ -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`. @@ -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 @@ -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 @@ -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: @@ -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) { @@ -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 @@ -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'), @@ -590,7 +590,7 @@ 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), @@ -598,8 +598,7 @@ const SignUpSchema = z.object({ 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) diff --git a/packages/example/package.json b/packages/example/package.json index 1e10201..7846de3 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -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 ", "private": true, "license": "UNLICENSED", "scripts": { @@ -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" diff --git a/packages/example/src/posts/posts.controller.ts b/packages/example/src/posts/posts.controller.ts index d8f1c17..70850ba 100644 --- a/packages/example/src/posts/posts.controller.ts +++ b/packages/example/src/posts/posts.controller.ts @@ -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, + }; + } } diff --git a/packages/example/test/posts.e2e-spec.ts b/packages/example/test/posts.e2e-spec.ts new file mode 100644 index 0000000..e3df829 --- /dev/null +++ b/packages/example/test/posts.e2e-spec.ts @@ -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; +} diff --git a/packages/nestjs-zod/package.json b/packages/nestjs-zod/package.json index a561d1e..f837ca6 100755 --- a/packages/nestjs-zod/package.json +++ b/packages/nestjs-zod/package.json @@ -9,10 +9,6 @@ "import": "./dist/z.mjs", "default": "./dist/z.js" }, - "./frontend": { - "import": "./dist/frontend.mjs", - "default": "./dist/frontend.js" - }, "./dto": { "import": "./dist/dto.mjs", "default": "./dist/dto.js" @@ -22,13 +18,15 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "z.d.ts", - "frontend.d.ts", "dto.d.ts" ], "sideEffects": false, "license": "MIT", - "repository": "git@github.com:BenLorantfy/nestjs-zod.git", + "repository": { + "type": "git", + "url": "git@github.com:BenLorantfy/nestjs-zod.git", + "directory": "packages/nestjs-zod" + }, "author": "Evgeny Zakharov ", "publishConfig": { "access": "public" @@ -55,14 +53,12 @@ "reflect-metadata": "^0.2.0", "rollup": "^2.69.0", "rollup-plugin-bundle-size": "^1.0.3", - "rollup-plugin-copy": "^3.4.0", "rollup-plugin-dts": "^4.1.0", "rollup-plugin-esbuild": "^4.8.2", - "rollup-plugin-terser": "^7.0.2", "rxjs": "^7.8.1", "ts-jest": "^29.1.0", "typescript": "^5.1.3", - "zod": "3.21.4" + "zod": "^3.23.8" }, "peerDependencies": { "@nestjs/common": "^10.0.0", @@ -85,6 +81,7 @@ } }, "dependencies": { - "merge-deep": "^3.0.3" + "merge-deep": "^3.0.3", + "@nest-zod/z": "*" } } \ No newline at end of file diff --git a/packages/nestjs-zod/rollup.config.js b/packages/nestjs-zod/rollup.config.js index 37c9f2c..e6aafee 100755 --- a/packages/nestjs-zod/rollup.config.js +++ b/packages/nestjs-zod/rollup.config.js @@ -1,9 +1,7 @@ import { defineConfig } from 'rollup' import dts from 'rollup-plugin-dts' import esbuild from 'rollup-plugin-esbuild' -import { terser } from 'rollup-plugin-terser' import bundleSize from 'rollup-plugin-bundle-size' -import copy from 'rollup-plugin-copy' const src = (file) => `src/${file}` const dist = (file) => `dist/${file}` @@ -39,65 +37,6 @@ const config = defineConfig([ }, ], }), - bundle(src('z/exports/everything.ts'), { - plugins: [esbuild()], - output: [ - { - file: dist('z.js'), - format: 'cjs', - }, - { - file: dist('z.mjs'), - format: 'es', - }, - ], - }), - bundle(src('z/exports/only-override.ts'), { - plugins: [ - dts(), - copy({ - targets: [ - { - src: './z.d.ts', - dest: 'dist', - transform: (contents) => contents.toString().replaceAll('./dist', '.'), - } - ], - }), - ], - output: [ - { - file: dist('z-only-override.d.ts'), - format: 'es', - }, - ], - }), - bundle(src('frontend/index.ts'), { - plugins: [esbuild(), terser()], - output: [ - { - file: dist('frontend.mjs'), - format: 'es', - }, - { - file: dist('frontend.js'), - format: 'cjs', - }, - ], - }), - bundle(src('frontend/index.ts'), { - plugins: [dts()], - output: [ - { - file: root('frontend.d.ts'), - format: 'es', - }, - { - file: dist('frontend.d.ts'), - format: 'es', - }, - ], - }), bundle(src('dto.ts'), { plugins: [esbuild()], output: [ diff --git a/packages/nestjs-zod/src/dto.test.ts b/packages/nestjs-zod/src/dto.test.ts index 6a8a176..7db28b3 100644 --- a/packages/nestjs-zod/src/dto.test.ts +++ b/packages/nestjs-zod/src/dto.test.ts @@ -1,7 +1,11 @@ import { createZodDto } from './dto' -import { z } from './z' +import { z as actualZod } from 'zod' +import { z as nestjsZod } from '@nest-zod/z' -describe('createZodDto', () => { +describe.each([ + ['zod', actualZod], + ['@nest-zod/z', nestjsZod], +])('createZodDto (using %s)', (description, z) => { it('should correctly create DTO', () => { const UserSchema = z.object({ username: z.string(), @@ -23,4 +27,5 @@ describe('createZodDto', () => { password: 'strong', }) }) -}) +}); + diff --git a/packages/nestjs-zod/src/dto.ts b/packages/nestjs-zod/src/dto.ts index c555e9b..e180f6e 100644 --- a/packages/nestjs-zod/src/dto.ts +++ b/packages/nestjs-zod/src/dto.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ZodSchema, ZodTypeDef } from './z' +import { ZodSchema, ZodTypeDef } from '@nest-zod/z' export interface ZodDto< TOutput = any, diff --git a/packages/nestjs-zod/src/exception.test.ts b/packages/nestjs-zod/src/exception.test.ts index 984e533..53463f0 100644 --- a/packages/nestjs-zod/src/exception.test.ts +++ b/packages/nestjs-zod/src/exception.test.ts @@ -1,8 +1,13 @@ import { BadRequestException, HttpStatus } from '@nestjs/common' import { ZodValidationException } from './exception' -import { z } from './z' -describe('ZodValidationException', () => { +import { z as actualZod } from 'zod' +import { z as nestjsZod } from '@nest-zod/z' + +describe.each([ + ['zod', actualZod], + ['@nest-zod/z', nestjsZod], +])('ZodValidationException (using %s)', (description, z) => { it('should correctly create exception', () => { const UserSchema = z.object({ username: z.string(), diff --git a/packages/nestjs-zod/src/exception.ts b/packages/nestjs-zod/src/exception.ts index 3d107f3..ec34692 100644 --- a/packages/nestjs-zod/src/exception.ts +++ b/packages/nestjs-zod/src/exception.ts @@ -3,7 +3,7 @@ import { HttpStatus, InternalServerErrorException, } from '@nestjs/common' -import { ZodError } from './z' +import { ZodError } from '@nest-zod/z' export class ZodValidationException extends BadRequestException { constructor(private error: ZodError) { diff --git a/packages/nestjs-zod/src/guard.test.ts b/packages/nestjs-zod/src/guard.test.ts index df8e3f2..16f7774 100644 --- a/packages/nestjs-zod/src/guard.test.ts +++ b/packages/nestjs-zod/src/guard.test.ts @@ -4,9 +4,14 @@ import { createZodDto } from './dto' import { ZodValidationException } from './exception' import { ZodGuard } from './guard' import { Source } from './shared/types' -import { z } from './z' -describe('ZodGuard', () => { +import { z as actualZod } from 'zod' +import { z as nestjsZod } from '@nest-zod/z' + +describe.each([ + ['zod', actualZod], + ['@nest-zod/z', nestjsZod], +])('ZodGuard (using %s)', (description, z) => { const UserSchema = z.object({ username: z.string(), password: z.string(), diff --git a/packages/nestjs-zod/src/guard.ts b/packages/nestjs-zod/src/guard.ts index e1ba2a0..2369839 100644 --- a/packages/nestjs-zod/src/guard.ts +++ b/packages/nestjs-zod/src/guard.ts @@ -8,7 +8,7 @@ import { ZodDto } from './dto' import { ZodExceptionCreator } from './exception' import { Source } from './shared/types' import { validate } from './validate' -import { ZodSchema } from './z' +import { ZodSchema } from '@nest-zod/z' interface ZodBodyGuardOptions { createValidationException?: ZodExceptionCreator diff --git a/packages/nestjs-zod/src/openapi/__snapshots__/zod-to-openapi.test.ts.snap b/packages/nestjs-zod/src/openapi/__snapshots__/zod-to-openapi.test.ts.snap index 52a7383..55b629c 100644 --- a/packages/nestjs-zod/src/openapi/__snapshots__/zod-to-openapi.test.ts.snap +++ b/packages/nestjs-zod/src/openapi/__snapshots__/zod-to-openapi.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should serialize a complex schema 1`] = ` +exports[`zodToOpenAPI (using @nest-zod/z) should serialize a complex schema 1`] = ` { "properties": { "array": { @@ -10,11 +10,6 @@ exports[`should serialize a complex schema 1`] = ` "maxItems": 3, "type": "array", }, - "dateString": { - "description": "My date string", - "format": "date-time", - "type": "string", - }, "discriminatedUnion": { "oneOf": [ { @@ -100,18 +95,260 @@ exports[`should serialize a complex schema 1`] = ` ], "type": "object", }, - "password": { - "format": "password", - "pattern": "^.*$", + "record": { + "additionalProperties": { + "type": "number", + }, + "type": "object", + }, + "recordWithKeys": { + "additionalProperties": { + "type": "string", + }, + "type": "object", + }, + "stringMinMax": { + "maxLength": 15, + "minLength": 5, "type": "string", }, - "passwordComplex": { - "format": "password", - "maxLength": 100, - "minLength": 8, - "pattern": "^(?:(?=.*\\d)(?=.*[A-Z]).*)$", + "tuple": { + "items": { + "oneOf": [ + { + "type": "string", + }, + { + "properties": { + "name": { + "enum": [ + "Rudy", + ], + "type": "string", + }, + }, + "required": [ + "name", + ], + "type": "object", + }, + { + "items": { + "oneOf": [ + { + "enum": [ + "blue", + ], + "type": "string", + }, + { + "enum": [ + "red", + ], + "type": "string", + }, + ], + }, + "type": "array", + }, + ], + }, + "type": "array", + }, + "union": { + "description": "Please choose topkek", + "oneOf": [ + { + "enum": [ + "kek", + ], + "type": "string", + }, + { + "enum": [ + "topkek", + ], + "type": "string", + }, + ], + }, + "zodDateString": { + "format": "date-time", "type": "string", }, + }, + "required": [ + "stringMinMax", + "numberMinMax", + "numberMultipleOf", + "tuple", + "union", + "discriminatedUnion", + "literalString", + "literalNumber", + "literalBoolean", + "array", + "objectNested", + "record", + "recordWithKeys", + "zodDateString", + ], + "type": "object", +} +`; + +exports[`zodToOpenAPI (using @nest-zod/z) should serialize an intersection of objects 1`] = ` +{ + "properties": { + "one": { + "type": "number", + }, + "two": { + "type": "number", + }, + }, + "required": [ + "one", + "two", + ], + "type": "object", +} +`; + +exports[`zodToOpenAPI (using @nest-zod/z) should serialize an intersection of unions 1`] = ` +{ + "oneOf": [ + { + "enum": [ + "123", + ], + "type": "string", + }, + { + "type": "number", + }, + { + "type": "boolean", + }, + { + "items": { + "type": "string", + }, + "type": "array", + }, + ], +} +`; + +exports[`zodToOpenAPI (using @nest-zod/z) should serialize an intersection with overrided fields 1`] = ` +{ + "properties": { + "one": { + "type": "string", + }, + }, + "required": [ + "one", + ], + "type": "object", +} +`; + +exports[`zodToOpenAPI (using zod) should serialize a complex schema 1`] = ` +{ + "properties": { + "array": { + "items": { + "type": "string", + }, + "maxItems": 3, + "type": "array", + }, + "discriminatedUnion": { + "oneOf": [ + { + "properties": { + "age": { + "type": "number", + }, + "name": { + "enum": [ + "vasya", + ], + "type": "string", + }, + }, + "required": [ + "name", + "age", + ], + "type": "object", + }, + { + "properties": { + "age": { + "type": "number", + }, + "name": { + "enum": [ + "petya", + ], + "type": "string", + }, + }, + "required": [ + "name", + "age", + ], + "type": "object", + }, + ], + }, + "enumDefault": { + "default": "buy-it-now", + "enum": [ + "buy-it-now", + "auctions", + ], + "type": "string", + }, + "literalBoolean": { + "type": "boolean", + }, + "literalNumber": { + "maximum": 123, + "minimum": 123, + "type": "number", + }, + "literalString": { + "enum": [ + "123", + ], + "type": "string", + }, + "numberMinMax": { + "exclusiveMaximum": false, + "exclusiveMinimum": false, + "maximum": 10, + "minimum": 3, + "type": "number", + }, + "numberMultipleOf": { + "multipleOf": 10, + "type": "number", + }, + "objectNested": { + "properties": { + "uuid": { + "format": "uuid", + "type": "string", + }, + }, + "required": [ + "uuid", + ], + "type": "object", + }, "record": { "additionalProperties": { "type": "number", @@ -208,16 +445,13 @@ exports[`should serialize a complex schema 1`] = ` "objectNested", "record", "recordWithKeys", - "dateString", "zodDateString", - "password", - "passwordComplex", ], "type": "object", } `; -exports[`should serialize an intersection of objects 1`] = ` +exports[`zodToOpenAPI (using zod) should serialize an intersection of objects 1`] = ` { "properties": { "one": { @@ -235,7 +469,7 @@ exports[`should serialize an intersection of objects 1`] = ` } `; -exports[`should serialize an intersection of unions 1`] = ` +exports[`zodToOpenAPI (using zod) should serialize an intersection of unions 1`] = ` { "oneOf": [ { @@ -260,7 +494,7 @@ exports[`should serialize an intersection of unions 1`] = ` } `; -exports[`should serialize an intersection with overrided fields 1`] = ` +exports[`zodToOpenAPI (using zod) should serialize an intersection with overrided fields 1`] = ` { "properties": { "one": { diff --git a/packages/nestjs-zod/src/openapi/zod-to-openapi.test.ts b/packages/nestjs-zod/src/openapi/zod-to-openapi.test.ts index b582fa2..ae0c916 100644 --- a/packages/nestjs-zod/src/openapi/zod-to-openapi.test.ts +++ b/packages/nestjs-zod/src/openapi/zod-to-openapi.test.ts @@ -1,234 +1,280 @@ -import { z, ZodTypeAny } from '../z' +import { ZodTypeAny } from 'zod'; +import { z as actualZod } from '@nest-zod/z' +import { z as nestjsZod } from '@nest-zod/z' import { zodToOpenAPI } from './zod-to-openapi' -const complexTestSchema = z.object({ - stringMinMax: z.string().min(5).max(15), - numberMinMax: z.number().min(3).max(10), - numberMultipleOf: z.number().multipleOf(10), - enumDefault: z.enum(['buy-it-now', 'auctions']).default('buy-it-now'), - tuple: z.tuple([ - z.string(), - z.object({ name: z.literal('Rudy') }), - z.array(z.union([z.literal('blue'), z.literal('red')])), - ]), - union: z - .union([z.literal('kek'), z.literal('topkek')]) - .describe('Please choose topkek'), - discriminatedUnion: z.discriminatedUnion('name', [ - z.object({ name: z.literal('vasya'), age: z.number() }), - z.object({ name: z.literal('petya'), age: z.number() }), - ]), - literalString: z.literal('123'), - literalNumber: z.literal(123), - literalBoolean: z.literal(true), - array: z.array(z.string()).max(3), - objectNested: z.object({ - uuid: z.string().uuid(), - }), - record: z.record(z.number()), - recordWithKeys: z.record(z.number(), z.string()), - dateString: z.dateString().cast().describe('My date string'), - zodDateString: z.string().datetime(), - password: z.password(), - passwordComplex: z - .password() - .atLeastOne('digit') - .atLeastOne('uppercase') - .min(8) - .max(100), -}) - -const intersectedObjectsSchema = z.intersection( - z.object({ - one: z.number(), - }), - z.object({ - two: z.number(), - }) -) - -const intersectedUnionsSchema = z.intersection( - z.union([z.literal('123'), z.number()]), - z.union([z.boolean(), z.array(z.string())]) -) - -const overrideIntersectionSchema = z.intersection( - z.object({ - one: z.number(), - }), - z.object({ - one: z.string(), - }) -) - -const transformedSchema = z - .object({ - seconds: z.number(), - }) - .transform((value) => ({ - seconds: value.seconds, - minutes: value.seconds / 60, - hours: value.seconds / 3600, - })) - -const lazySchema = z.lazy(() => z.string()) - -it('should serialize a complex schema', () => { - const openApiObject = zodToOpenAPI(complexTestSchema) - - expect(openApiObject).toMatchSnapshot() -}) - -it('should serialize an intersection of objects', () => { - const openApiObject = zodToOpenAPI(intersectedObjectsSchema) - - expect(openApiObject).toMatchSnapshot() -}) - -it('should serialize an intersection of unions', () => { - const openApiObject = zodToOpenAPI(intersectedUnionsSchema) - - expect(openApiObject).toMatchSnapshot() -}) - -it('should serialize an intersection with overrided fields', () => { - const openApiObject = zodToOpenAPI(overrideIntersectionSchema) - - expect(openApiObject).toMatchSnapshot() -}) - -it('should serialize objects', () => { - const schema = z.object({ - prop1: z.string(), - prop2: z.string().optional(), - }) - const openApiObject = zodToOpenAPI(schema) - - expect(openApiObject).toEqual({ - type: 'object', - required: ['prop1'], - properties: { - prop1: { - type: 'string', - }, - prop2: { - type: 'string', - }, - }, +describe.each([ + ['zod', actualZod], + ['@nest-zod/z', nestjsZod], +])('zodToOpenAPI (using %s)', (description, z) => { + const complexTestSchema = z.object({ + stringMinMax: z.string().min(5).max(15), + numberMinMax: z.number().min(3).max(10), + numberMultipleOf: z.number().multipleOf(10), + enumDefault: z.enum(['buy-it-now', 'auctions']).default('buy-it-now'), + tuple: z.tuple([ + z.string(), + z.object({ name: z.literal('Rudy') }), + z.array(z.union([z.literal('blue'), z.literal('red')])), + ]), + union: z + .union([z.literal('kek'), z.literal('topkek')]) + .describe('Please choose topkek'), + discriminatedUnion: z.discriminatedUnion('name', [ + z.object({ name: z.literal('vasya'), age: z.number() }), + z.object({ name: z.literal('petya'), age: z.number() }), + ]), + literalString: z.literal('123'), + literalNumber: z.literal(123), + literalBoolean: z.literal(true), + array: z.array(z.string()).max(3), + objectNested: z.object({ + uuid: z.string().uuid(), + }), + record: z.record(z.number()), + recordWithKeys: z.record(z.number(), z.string()), + zodDateString: z.string().datetime(), }) -}) - -it('should serialize partial objects', () => { - const schema = z + + const intersectedObjectsSchema = z.intersection( + z.object({ + one: z.number(), + }), + z.object({ + two: z.number(), + }) + ) + + const intersectedUnionsSchema = z.intersection( + z.union([z.literal('123'), z.number()]), + z.union([z.boolean(), z.array(z.string())]) + ) + + const overrideIntersectionSchema = z.intersection( + z.object({ + one: z.number(), + }), + z.object({ + one: z.string(), + }) + ) + + const transformedSchema = z .object({ + seconds: z.number(), + }) + .transform((value) => ({ + seconds: value.seconds, + minutes: value.seconds / 60, + hours: value.seconds / 3600, + })) + + const lazySchema = z.lazy(() => z.string()) + + it('should serialize a complex schema', () => { + const openApiObject = zodToOpenAPI(complexTestSchema) + + expect(openApiObject).toMatchSnapshot() + }) + + it('should serialize an intersection of objects', () => { + const openApiObject = zodToOpenAPI(intersectedObjectsSchema) + + expect(openApiObject).toMatchSnapshot() + }) + + it('should serialize an intersection of unions', () => { + const openApiObject = zodToOpenAPI(intersectedUnionsSchema) + + expect(openApiObject).toMatchSnapshot() + }) + + it('should serialize an intersection with overrided fields', () => { + const openApiObject = zodToOpenAPI(overrideIntersectionSchema) + + expect(openApiObject).toMatchSnapshot() + }) + + it('should serialize objects', () => { + const schema = z.object({ prop1: z.string(), - prop2: z.string(), + prop2: z.string().optional(), }) - .partial() - const openApiObject = zodToOpenAPI(schema) - - expect(openApiObject).toEqual({ - type: 'object', - properties: { - prop1: { - type: 'string', + const openApiObject = zodToOpenAPI(schema) + + expect(openApiObject).toEqual({ + type: 'object', + required: ['prop1'], + properties: { + prop1: { + type: 'string', + }, + prop2: { + type: 'string', + }, }, - prop2: { - type: 'string', + }) + }) + + it('should serialize partial objects', () => { + const schema = z + .object({ + prop1: z.string(), + prop2: z.string(), + }) + .partial() + const openApiObject = zodToOpenAPI(schema) + + expect(openApiObject).toEqual({ + type: 'object', + properties: { + prop1: { + type: 'string', + }, + prop2: { + type: 'string', + }, }, - }, + }) }) -}) - -it('should serialize nullable types', () => { - const schema = z.string().nullable() - const openApiObject = zodToOpenAPI(schema) - - expect(openApiObject).toEqual({ type: 'string', nullable: true }) -}) - -it('should serialize optional types', () => { - const schema = z.string().optional() - const openApiObject = zodToOpenAPI(schema) - - expect(openApiObject).toEqual({ type: 'string' }) -}) - -it('should serialize types with default value', () => { - const schema = z.string().default('abitia') - const openApiObject = zodToOpenAPI(schema) - - expect(openApiObject).toEqual({ type: 'string', default: 'abitia' }) -}) - -it('should serialize enums', () => { - const schema = z.enum(['adama', 'kota']) - const openApiObject = zodToOpenAPI(schema) - - expect(openApiObject).toEqual({ - type: 'string', - enum: ['adama', 'kota'], + + it('should serialize nullable types', () => { + const schema = z.string().nullable() + const openApiObject = zodToOpenAPI(schema) + + expect(openApiObject).toEqual({ type: 'string', nullable: true }) }) -}) - -it('should serialize native enums', () => { - enum NativeEnum { - ADAMA = 'adama', - KOTA = 'kota', - } - - const schema = z.nativeEnum(NativeEnum) - const openApiObject = zodToOpenAPI(schema) - - expect(openApiObject).toEqual({ - 'type': 'string', - 'enum': ['adama', 'kota'], - 'x-enumNames': ['ADAMA', 'KOTA'], + + it('should serialize optional types', () => { + const schema = z.string().optional() + const openApiObject = zodToOpenAPI(schema) + + expect(openApiObject).toEqual({ type: 'string' }) }) -}) - -describe('scalar types', () => { - const testCases: [ZodTypeAny, string, string?][] = [ - // [zod type, expected open api type, expected format] - [z.string(), 'string'], - [z.number(), 'number'], - [z.boolean(), 'boolean'], - [z.bigint(), 'integer', 'int64'], - // [z.null(), 'null'], <- Needs OpenApi 3.1 to be represented correctly - // [z.undefined(), 'undefined'], <- TBD, probably the property should be removed from schema - ] - - for (const [zodType, expectedType, expectedFormat] of testCases) { - // eslint-disable-next-line no-loop-func - it(expectedType, () => { - const openApiObject = zodToOpenAPI(zodType) - - expect(openApiObject).toEqual({ - type: expectedType, - format: expectedFormat ?? undefined, - }) + + it('should serialize types with default value', () => { + const schema = z.string().default('abitia') + const openApiObject = zodToOpenAPI(schema) + + expect(openApiObject).toEqual({ type: 'string', default: 'abitia' }) + }) + + it('should serialize enums', () => { + const schema = z.enum(['adama', 'kota']) + const openApiObject = zodToOpenAPI(schema) + + expect(openApiObject).toEqual({ + type: 'string', + enum: ['adama', 'kota'], }) - } -}) - -it('should serialize transformed schema', () => { - const openApiObject = zodToOpenAPI(transformedSchema) - - expect(openApiObject).toEqual({ - type: 'object', - required: ['seconds'], - properties: { - seconds: { - type: 'number', + }) + + it('should serialize native enums', () => { + enum NativeEnum { + ADAMA = 'adama', + KOTA = 'kota', + } + + const schema = z.nativeEnum(NativeEnum) + const openApiObject = zodToOpenAPI(schema) + + expect(openApiObject).toEqual({ + 'type': 'string', + 'enum': ['adama', 'kota'], + 'x-enumNames': ['ADAMA', 'KOTA'], + }) + }) + + describe('scalar types', () => { + const testCases: [ZodTypeAny, string, string?][] = [ + // [zod type, expected open api type, expected format] + [z.string(), 'string'], + [z.number(), 'number'], + [z.boolean(), 'boolean'], + [z.bigint(), 'integer', 'int64'], + // [z.null(), 'null'], <- Needs OpenApi 3.1 to be represented correctly + // [z.undefined(), 'undefined'], <- TBD, probably the property should be removed from schema + ] + + for (const [zodType, expectedType, expectedFormat] of testCases) { + // eslint-disable-next-line no-loop-func + it(expectedType, () => { + const openApiObject = zodToOpenAPI(zodType) + + expect(openApiObject).toEqual({ + type: expectedType, + format: expectedFormat ?? undefined, + }) + }) + } + }) + + it('should serialize transformed schema', () => { + const openApiObject = zodToOpenAPI(transformedSchema) + + expect(openApiObject).toEqual({ + type: 'object', + required: ['seconds'], + properties: { + seconds: { + type: 'number', + }, }, - }, + }) }) -}) - -it('should serialize lazy schema', () => { - const openApiObject = zodToOpenAPI(lazySchema) - - expect(openApiObject).toEqual({ - type: 'string', + + it('should serialize lazy schema', () => { + const openApiObject = zodToOpenAPI(lazySchema) + + expect(openApiObject).toEqual({ + type: 'string', + }) + }) +}); + + +describe('special types', () => { + const specialSchema = nestjsZod.object({ + dateString: nestjsZod.dateString().cast().describe('My date string'), + zodDateString: nestjsZod.string().datetime(), + password: nestjsZod.password(), + passwordComplex: nestjsZod + .password() + .atLeastOne('digit') + .atLeastOne('uppercase') + .min(8) + .max(100), + }); + + test('works for special types', () => { + const openApiObject = zodToOpenAPI(specialSchema) + + expect(openApiObject).toEqual({ + properties: { + dateString: { + description: 'My date string', + format: 'date-time', + type: 'string' + }, + password: { + format: 'password', + pattern: '^.*$', + type: 'string' + }, + passwordComplex: { + format: 'password', + maxLength: 100, + minLength: 8, + pattern: '^(?:(?=.*\\d)(?=.*[A-Z]).*)$', + type: 'string' + }, + zodDateString: { + format: 'date-time', + type: 'string' + } + }, + required: ['dateString', 'zodDateString', 'password', 'passwordComplex'], + type: 'object' + }); }) -}) +}); \ No newline at end of file diff --git a/packages/nestjs-zod/src/openapi/zod-to-openapi.ts b/packages/nestjs-zod/src/openapi/zod-to-openapi.ts index 8d0f32c..4757105 100644 --- a/packages/nestjs-zod/src/openapi/zod-to-openapi.ts +++ b/packages/nestjs-zod/src/openapi/zod-to-openapi.ts @@ -1,7 +1,7 @@ import { Type } from '@nestjs/common' import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface' import mergeDeep from 'merge-deep' -import { z } from '../z' +import { z } from '@nest-zod/z' interface ExtendedSchemaObject extends SchemaObject { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/nestjs-zod/src/pipe.test.ts b/packages/nestjs-zod/src/pipe.test.ts index c0ac3f9..481bb63 100644 --- a/packages/nestjs-zod/src/pipe.test.ts +++ b/packages/nestjs-zod/src/pipe.test.ts @@ -2,9 +2,14 @@ import { ArgumentMetadata } from '@nestjs/common' import { createZodDto } from './dto' import { ZodValidationException } from './exception' import { ZodValidationPipe } from './pipe' -import { z } from './z' -describe('ZodValidationPipe', () => { +import { z as actualZod } from 'zod' +import { z as nestjsZod } from '@nest-zod/z' + +describe.each([ + ['zod', actualZod], + ['@nest-zod/z', nestjsZod], +])('ZodValidationPipe (using %s)', (description, z) => { const UserSchema = z.object({ username: z.string(), password: z.string(), diff --git a/packages/nestjs-zod/src/pipe.ts b/packages/nestjs-zod/src/pipe.ts index 2d21877..d94bb17 100644 --- a/packages/nestjs-zod/src/pipe.ts +++ b/packages/nestjs-zod/src/pipe.ts @@ -2,7 +2,7 @@ import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common' import { isZodDto, ZodDto } from './dto' import { ZodExceptionCreator } from './exception' import { validate } from './validate' -import { ZodSchema } from './z' +import { ZodSchema } from '@nest-zod/z' interface ZodValidationPipeOptions { createValidationException?: ZodExceptionCreator diff --git a/packages/nestjs-zod/src/serializer.test.ts b/packages/nestjs-zod/src/serializer.test.ts index 73d8efc..cfeebb3 100644 --- a/packages/nestjs-zod/src/serializer.test.ts +++ b/packages/nestjs-zod/src/serializer.test.ts @@ -2,12 +2,17 @@ import { createMock } from '@golevelup/ts-jest' import { CallHandler, ExecutionContext } from '@nestjs/common' import { Reflector } from '@nestjs/core' import { lastValueFrom, of } from 'rxjs' -import { z } from 'zod' import { createZodDto } from './dto' import { ZodSerializationException } from './exception' import { ZodSerializerInterceptor } from './serializer' -describe('ZodSerializerInterceptor', () => { +import { z as actualZod } from 'zod' +import { z as nestjsZod } from '@nest-zod/z' + +describe.each([ + ['zod', actualZod], + ['@nest-zod/z', nestjsZod], +])('ZodSerializerInterceptor (using %s)', (description, z) => { const UserSchema = z.object({ username: z.string(), }) diff --git a/packages/z/.eslintignore b/packages/z/.eslintignore new file mode 100644 index 0000000..4c66063 --- /dev/null +++ b/packages/z/.eslintignore @@ -0,0 +1,5 @@ +dist +node_modules +/*.js +/*.ts +!.eslintrc.js \ No newline at end of file diff --git a/packages/z/.eslintrc.js b/packages/z/.eslintrc.js new file mode 100644 index 0000000..fb0593e --- /dev/null +++ b/packages/z/.eslintrc.js @@ -0,0 +1,15 @@ +const { configure, presets } = require('eslint-kit') + +module.exports = configure({ + presets: [ + presets.imports(), + presets.node(), + presets.prettier(), + presets.typescript(), + ], + extend: { + rules: { + '@typescript-eslint/no-unnecessary-condition': 'off', + }, + }, +}) diff --git a/packages/z/.gitignore b/packages/z/.gitignore new file mode 100644 index 0000000..443f455 --- /dev/null +++ b/packages/z/.gitignore @@ -0,0 +1,22 @@ +.DS_Store +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +node_modules +.env +.env.test +.cache +.next +.nuxt +.vscode/* +.idea +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +dist +package-lock.json +/*.d.ts +!z.d.ts \ No newline at end of file diff --git a/packages/z/.prettierrc b/packages/z/.prettierrc new file mode 100644 index 0000000..ce8395c --- /dev/null +++ b/packages/z/.prettierrc @@ -0,0 +1,16 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "quoteProps": "consistent", + "endOfLine": "lf", + "importOrder": [ + "^(child_process|crypto|events|fs|http|https|os|path)(\\/(.*))?$", + "", + "^[./]" + ], + "importOrderParserPlugins" : ["typescript", "decorators-legacy"], + "experimentalBabelParserPluginsList": [ + "typescript" + ] +} \ No newline at end of file diff --git a/packages/z/jest.config.js b/packages/z/jest.config.js new file mode 100644 index 0000000..33c2bed --- /dev/null +++ b/packages/z/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + passWithNoTests: true +}; \ No newline at end of file diff --git a/packages/z/package.json b/packages/z/package.json new file mode 100755 index 0000000..9975ad5 --- /dev/null +++ b/packages/z/package.json @@ -0,0 +1,84 @@ +{ + "name": "@nest-zod/z", + "description": "Extended zod with a few additional types and helpers", + "version": "1.0.1", + "main": "./dist/z.js", + "exports": { + ".": { + "default": "./dist/z.js" + }, + "./frontend": { + "import": "./dist/frontend.mjs", + "default": "./dist/frontend.js" + } + }, + "module": "./dist/z.js", + "types": "./dist/z.d.ts", + "files": [ + "dist", + "z.d.ts", + "frontend.d.ts" + ], + "sideEffects": false, + "license": "MIT", + "repository": { + "type": "git", + "url": "git@github.com:BenLorantfy/nestjs-zod.git", + "directory": "packages/z" + }, + "author": "Evgeny Zakharov ", + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev": "vite", + "build": "rollup -c", + "test": "TZ=UTC jest", + "lint": "eslint --ext .ts,.tsx src", + "lint:fix": "eslint --ext .ts,.tsx src --fix" + }, + "devDependencies": { + "@golevelup/ts-jest": "^0.3.3", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/swagger": "^7.4.2", + "@types/jest": "^29.5.2", + "@types/merge-deep": "^3.0.0", + "@types/node": "^22.7.5", + "esbuild": "^0.14.24", + "eslint": "^8.42.0", + "eslint-kit": "^5.7.0", + "jest": "^29.5.0", + "reflect-metadata": "^0.2.0", + "rollup": "^2.69.0", + "rollup-plugin-bundle-size": "^1.0.3", + "rollup-plugin-copy": "^3.4.0", + "rollup-plugin-dts": "^4.1.0", + "rollup-plugin-esbuild": "^4.8.2", + "rollup-plugin-terser": "^7.0.2", + "rxjs": "^7.8.1", + "ts-jest": "^29.1.0", + "typescript": "^5.1.3", + "zod": "^3.23.8" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/swagger": "^7.4.2", + "zod": ">= 3.14.3" + }, + "peerDependenciesMeta": { + "@nestjs/common": { + "optional": true + }, + "@nestjs/core": { + "optional": true + }, + "@nestjs/swagger": { + "optional": true + }, + "zod": { + "optional": false + } + } +} \ No newline at end of file diff --git a/packages/z/rollup.config.js b/packages/z/rollup.config.js new file mode 100755 index 0000000..6ab0250 --- /dev/null +++ b/packages/z/rollup.config.js @@ -0,0 +1,85 @@ +import { defineConfig } from 'rollup' +import dts from 'rollup-plugin-dts' +import esbuild from 'rollup-plugin-esbuild' +import bundleSize from 'rollup-plugin-bundle-size' +import copy from 'rollup-plugin-copy' +import { terser } from 'rollup-plugin-terser' + +const src = (file) => `src/${file}` +const dist = (file) => `dist/${file}` +const root = (file) => `${file}` + +const bundle = (input, config) => + defineConfig({ + ...config, + input, + external: (id) => !/^[./]/.test(id), + plugins: [ + ...(config.plugins || []), + bundleSize() + ] + }) + +const config = defineConfig([ + bundle(src('frontend/index.ts'), { + plugins: [esbuild(), terser()], + output: [ + { + file: dist('frontend.mjs'), + format: 'es', + }, + { + file: dist('frontend.js'), + format: 'cjs', + }, + ], + }), + bundle(src('frontend/index.ts'), { + plugins: [dts()], + output: [ + { + file: root('frontend.d.ts'), + format: 'es', + }, + { + file: dist('frontend.d.ts'), + format: 'es', + }, + ], + }), + bundle(src('z/exports/everything.ts'), { + plugins: [esbuild()], + output: [ + { + file: dist('z.js'), + format: 'cjs', + }, + { + file: dist('z.mjs'), + format: 'es', + }, + ], + }), + bundle(src('z/exports/only-override.ts'), { + plugins: [ + dts(), + copy({ + targets: [ + { + src: './z.d.ts', + dest: 'dist', + transform: (contents) => contents.toString().replaceAll('./dist', '.'), + } + ], + }), + ], + output: [ + { + file: dist('z-only-override.d.ts'), + format: 'es', + }, + ], + }) +]) + +export default config diff --git a/packages/nestjs-zod/src/frontend/index.ts b/packages/z/src/frontend/index.ts similarity index 100% rename from packages/nestjs-zod/src/frontend/index.ts rename to packages/z/src/frontend/index.ts diff --git a/packages/nestjs-zod/src/z/error-map/date-string.ts b/packages/z/src/z/error-map/date-string.ts similarity index 100% rename from packages/nestjs-zod/src/z/error-map/date-string.ts rename to packages/z/src/z/error-map/date-string.ts diff --git a/packages/nestjs-zod/src/z/error-map/index.ts b/packages/z/src/z/error-map/index.ts similarity index 100% rename from packages/nestjs-zod/src/z/error-map/index.ts rename to packages/z/src/z/error-map/index.ts diff --git a/packages/nestjs-zod/src/z/error-map/password.ts b/packages/z/src/z/error-map/password.ts similarity index 100% rename from packages/nestjs-zod/src/z/error-map/password.ts rename to packages/z/src/z/error-map/password.ts diff --git a/packages/nestjs-zod/src/z/error-map/shared.ts b/packages/z/src/z/error-map/shared.ts similarity index 100% rename from packages/nestjs-zod/src/z/error-map/shared.ts rename to packages/z/src/z/error-map/shared.ts diff --git a/packages/nestjs-zod/src/z/exports/everything.ts b/packages/z/src/z/exports/everything.ts similarity index 100% rename from packages/nestjs-zod/src/z/exports/everything.ts rename to packages/z/src/z/exports/everything.ts diff --git a/packages/nestjs-zod/src/z/exports/namespace.ts b/packages/z/src/z/exports/namespace.ts similarity index 100% rename from packages/nestjs-zod/src/z/exports/namespace.ts rename to packages/z/src/z/exports/namespace.ts diff --git a/packages/nestjs-zod/src/z/exports/only-override.ts b/packages/z/src/z/exports/only-override.ts similarity index 100% rename from packages/nestjs-zod/src/z/exports/only-override.ts rename to packages/z/src/z/exports/only-override.ts diff --git a/packages/nestjs-zod/src/z/generic-types/from.ts b/packages/z/src/z/generic-types/from.ts similarity index 100% rename from packages/nestjs-zod/src/z/generic-types/from.ts rename to packages/z/src/z/generic-types/from.ts diff --git a/packages/nestjs-zod/src/z/generic-types/index.ts b/packages/z/src/z/generic-types/index.ts similarity index 100% rename from packages/nestjs-zod/src/z/generic-types/index.ts rename to packages/z/src/z/generic-types/index.ts diff --git a/packages/nestjs-zod/src/z/generic-types/json.ts b/packages/z/src/z/generic-types/json.ts similarity index 100% rename from packages/nestjs-zod/src/z/generic-types/json.ts rename to packages/z/src/z/generic-types/json.ts diff --git a/packages/nestjs-zod/src/z/index.ts b/packages/z/src/z/index.ts similarity index 100% rename from packages/nestjs-zod/src/z/index.ts rename to packages/z/src/z/index.ts diff --git a/packages/nestjs-zod/src/z/is-nestjs-zod-issue.ts b/packages/z/src/z/is-nestjs-zod-issue.ts similarity index 100% rename from packages/nestjs-zod/src/z/is-nestjs-zod-issue.ts rename to packages/z/src/z/is-nestjs-zod-issue.ts diff --git a/packages/nestjs-zod/src/z/issues/date-string.ts b/packages/z/src/z/issues/date-string.ts similarity index 100% rename from packages/nestjs-zod/src/z/issues/date-string.ts rename to packages/z/src/z/issues/date-string.ts diff --git a/packages/nestjs-zod/src/z/issues/index.ts b/packages/z/src/z/issues/index.ts similarity index 100% rename from packages/nestjs-zod/src/z/issues/index.ts rename to packages/z/src/z/issues/index.ts diff --git a/packages/nestjs-zod/src/z/issues/overrided.ts b/packages/z/src/z/issues/overrided.ts similarity index 100% rename from packages/nestjs-zod/src/z/issues/overrided.ts rename to packages/z/src/z/issues/overrided.ts diff --git a/packages/nestjs-zod/src/z/issues/password.ts b/packages/z/src/z/issues/password.ts similarity index 100% rename from packages/nestjs-zod/src/z/issues/password.ts rename to packages/z/src/z/issues/password.ts diff --git a/packages/nestjs-zod/src/z/new-types/__snapshots__/date-string.test.ts.snap b/packages/z/src/z/new-types/__snapshots__/date-string.test.ts.snap similarity index 100% rename from packages/nestjs-zod/src/z/new-types/__snapshots__/date-string.test.ts.snap rename to packages/z/src/z/new-types/__snapshots__/date-string.test.ts.snap diff --git a/packages/nestjs-zod/src/z/new-types/__snapshots__/password.test.ts.snap b/packages/z/src/z/new-types/__snapshots__/password.test.ts.snap similarity index 100% rename from packages/nestjs-zod/src/z/new-types/__snapshots__/password.test.ts.snap rename to packages/z/src/z/new-types/__snapshots__/password.test.ts.snap diff --git a/packages/nestjs-zod/src/z/new-types/date-string.test.ts b/packages/z/src/z/new-types/date-string.test.ts similarity index 100% rename from packages/nestjs-zod/src/z/new-types/date-string.test.ts rename to packages/z/src/z/new-types/date-string.test.ts diff --git a/packages/nestjs-zod/src/z/new-types/date-string.ts b/packages/z/src/z/new-types/date-string.ts similarity index 100% rename from packages/nestjs-zod/src/z/new-types/date-string.ts rename to packages/z/src/z/new-types/date-string.ts diff --git a/packages/nestjs-zod/src/z/new-types/index.ts b/packages/z/src/z/new-types/index.ts similarity index 100% rename from packages/nestjs-zod/src/z/new-types/index.ts rename to packages/z/src/z/new-types/index.ts diff --git a/packages/nestjs-zod/src/z/new-types/password.test.ts b/packages/z/src/z/new-types/password.test.ts similarity index 100% rename from packages/nestjs-zod/src/z/new-types/password.test.ts rename to packages/z/src/z/new-types/password.test.ts diff --git a/packages/nestjs-zod/src/z/new-types/password.ts b/packages/z/src/z/new-types/password.ts similarity index 100% rename from packages/nestjs-zod/src/z/new-types/password.ts rename to packages/z/src/z/new-types/password.ts diff --git a/packages/nestjs-zod/src/z/shared.ts b/packages/z/src/z/shared.ts similarity index 100% rename from packages/nestjs-zod/src/z/shared.ts rename to packages/z/src/z/shared.ts diff --git a/packages/nestjs-zod/src/z/type-names.ts b/packages/z/src/z/type-names.ts similarity index 100% rename from packages/nestjs-zod/src/z/type-names.ts rename to packages/z/src/z/type-names.ts diff --git a/packages/z/tsconfig.json b/packages/z/tsconfig.json new file mode 100644 index 0000000..30e69e7 --- /dev/null +++ b/packages/z/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "allowJs": false, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "experimentalDecorators": true, + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "module": "esnext", + "moduleResolution": "node", + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "es6", + "declaration": true, + "declarationDir": "./dist", + "baseUrl": "./", + "outDir": "./dist", + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "./node_modules", + "./dist" + ], + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/nestjs-zod/z.d.ts b/packages/z/z.d.ts similarity index 100% rename from packages/nestjs-zod/z.d.ts rename to packages/z/z.d.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64cbe76..aa6c6f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: packages/example: dependencies: + '@nest-zod/z': + specifier: workspace:* + version: link:../z '@nestjs/common': specifier: ^10.0.0 version: 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -27,7 +30,7 @@ importers: specifier: ^7.4.2 version: 7.4.2(@nestjs/common@10.4.4)(@nestjs/core@10.4.4)(reflect-metadata@0.2.2) nestjs-zod: - specifier: 0.0.0-set-by-ci + specifier: workspace:* version: link:../nestjs-zod reflect-metadata: specifier: ^0.2.0 @@ -105,9 +108,75 @@ importers: packages/nestjs-zod: dependencies: + '@nest-zod/z': + specifier: '*' + version: link:../z merge-deep: specifier: ^3.0.3 version: 3.0.3 + devDependencies: + '@golevelup/ts-jest': + specifier: ^0.3.3 + version: 0.3.8 + '@nestjs/common': + specifier: ^10.0.0 + version: 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': + specifier: ^10.0.0 + version: 10.4.4(@nestjs/common@10.4.4)(@nestjs/platform-express@10.4.4)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/swagger': + specifier: ^7.4.2 + version: 7.4.2(@nestjs/common@10.4.4)(@nestjs/core@10.4.4)(reflect-metadata@0.2.2) + '@types/jest': + specifier: ^29.5.2 + version: 29.5.13 + '@types/merge-deep': + specifier: ^3.0.0 + version: 3.0.3 + '@types/node': + specifier: ^22.7.5 + version: 22.7.5 + esbuild: + specifier: ^0.14.24 + version: 0.14.54 + eslint: + specifier: ^8.42.0 + version: 8.57.1 + eslint-kit: + specifier: ^5.7.0 + version: 5.10.0(effector@23.2.3)(eslint@8.57.1)(svelte@3.59.2)(typescript@5.6.3) + jest: + specifier: ^29.5.0 + version: 29.7.0(@types/node@22.7.5)(ts-node@10.9.2) + reflect-metadata: + specifier: ^0.2.0 + version: 0.2.2 + rollup: + specifier: ^2.69.0 + version: 2.79.2 + rollup-plugin-bundle-size: + specifier: ^1.0.3 + version: 1.0.3 + rollup-plugin-dts: + specifier: ^4.1.0 + version: 4.2.3(rollup@2.79.2)(typescript@5.6.3) + rollup-plugin-esbuild: + specifier: ^4.8.2 + version: 4.10.3(esbuild@0.14.54)(rollup@2.79.2) + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + ts-jest: + specifier: ^29.1.0 + version: 29.2.5(@babel/core@7.25.8)(esbuild@0.14.54)(jest@29.7.0)(typescript@5.6.3) + typescript: + specifier: ^5.1.3 + version: 5.6.3 + zod: + specifier: ^3.23.8 + version: 3.23.8 + + packages/z: devDependencies: '@golevelup/ts-jest': specifier: ^0.3.3 @@ -173,8 +242,8 @@ importers: specifier: ^5.1.3 version: 5.6.3 zod: - specifier: 3.21.4 - version: 3.21.4 + specifier: ^3.23.8 + version: 3.23.8 packages: @@ -7687,10 +7756,5 @@ packages: engines: {node: '>=10'} dev: true - /zod@3.21.4: - resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} - dev: true - /zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - dev: false