├── .gitignore ├── .npmignore ├── .release-it.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── decorators │ ├── common.ts │ ├── decorate-with-zod-input.ts │ ├── index.ts │ ├── input-type │ │ ├── index.ts │ │ ├── input-from-zod.ts │ │ ├── input-type-with-zod.ts │ │ ├── options.inteface.ts │ │ └── zod-args.ts │ ├── make-decorator-from-factory.ts │ ├── query-types │ │ ├── index.ts │ │ ├── mutation-with-zod.ts │ │ ├── query-with-zod.ts │ │ └── subscription-with-zod.ts │ ├── types.ts │ └── zod-options-wrapper.interface.ts ├── helpers │ ├── build-enum-type.ts │ ├── constants.ts │ ├── create-zod-property-descriptor.ts │ ├── extract-name-and-description.ts │ ├── generate-defaults.ts │ ├── get-description.ts │ ├── get-field-info-from-zod.ts │ ├── get-zod-object-name.ts │ ├── get-zod-object.ts │ ├── index.ts │ ├── is-zod-instance.ts │ ├── parse-shape.ts │ ├── to-title-case.ts │ ├── unwrap.ts │ ├── with-suffix.ts │ └── zod-validator.pipe.ts ├── index.ts ├── model-from-zod.ts └── types │ ├── enum-provider.ts │ ├── prev.ts │ └── type-provider.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *lock.y[a]ml 3 | .DS_Store 4 | dist 5 | release.sh 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *lock.y[a]ml 2 | release.sh 3 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | This file contains the changes made to the package. 3 | 4 | The sections are in descending order of the change date. 5 | 6 | ## [3.3.0] - 2023-09-28 7 | ### Added 8 | - Support for `NativeEnum`s. 9 | - `parseToInstance` option to let users/consumers choose if the successful 10 | `zod` validation should parse to the instance of dynamically generated class. 11 | 12 | ## [3.2.0] - 2023-05-16 13 | ### Added 14 | - Now `.int()` can be used and it will have `GraphQLInt` type. Previously 15 | it was always `Float` type. 16 | 17 | Thanks [Ugzuzg](https://github.com/Ugzuzg) for [their contribution](https://github.com/incetarik/nestjs-graphql-zod/pull/12). 18 | 19 | ### Changed 20 | - Dependencies are now moved to `peerDependencies`. 21 | 22 | ## [3.1.0] - 2023-04-05 23 | ### Added 24 | - `setDefaultEnumProvider` function. 25 | 26 | This can be used to provide types for enums parsed from the zod schema. Use this function to return your custom, already registered GraphQL enums instead of dynamic, parsed zod enum. By this way, it would prevent multiple identical enums being generated. 27 | 28 | - Added `getDefaultEnumProvider` function. 29 | 30 | This function gets the previously set enum provider function 31 | through `setDefaultEnumProvider`. 32 | 33 | These functions are also added to the options of `@ZodArgs()`. 34 | 35 | ## [3.0.0] - 2023-03-27 36 | ### Added 37 | - `setDefaultTypeProvider` function which can be used to provide custom GraphQL 38 | scalar types for complex types. This function will receive a handler function 39 | which will take the built name representation of the type that is built from 40 | the zod schema and return a `GraphQLScalarType` to represent that custom type. 41 | 42 | The built name for complex types will be similar to TypeScript types, such as: 43 | `Record, Array>`. For example, for this type 44 | it is possible to return a `JSONObject` scalar type to represent this zod type. 45 | 46 | This _Type Provider_ function can be set through this function and also via 47 | the decorator properties. Hence, it is possible to set different handlers for 48 | different decorators. 49 | 50 | - Support for primitives at `@ZodArgs` decorator 51 | 52 | Now it is possible to pass a primitive value validator or array of it, example: 53 | ```ts 54 | @ZodArgs(z.number().gt(10).optional().array()) input: number[] 55 | ``` 56 | And this decorator will be validating the input passed to the parameter without 57 | creating a proxy type for it (as it is primitive). 58 | 59 | - Added `getZodObjectName` function. 60 | 61 | This function takes any zod type and builds a string representing the zod 62 | structure like TypeScript types as the example above. 63 | 64 | ### Changed 65 | - Description parsing is improved. Now the description properties of fields 66 | will still be extracted if they are under different zod effects such as 67 | `Optional` or `Nullable` or `Array` etc. 68 | - Improved error messages during parsing. Now the error messages will provide 69 | a string that represents the schema that caused the error. 70 | 71 | ### Removed 72 | - `IModelFromZodOptionsWithMapper` interface is removed which means that 73 | `propertyMap` will not be provided any more, there will be no renaming when 74 | a zod is being converted to a class. 75 | - `types.d.ts` file. 76 | 77 | ## [2.0.3] - 2023-02-07 78 | ### Changed 79 | - Provides safe naming mechanism. Now using same name for different or same 80 | schemes will not cause error. 81 | - Now returns the same dynamic class for same zod scheme input for `@ZodArgs()`. 82 | 83 | ## [2.0.2] - 2022-09-28 84 | ### Changed 85 | - The behavior of error handling has been changed. If an error is thrown from 86 | the method body, then the error will not be modified. If a zod validation 87 | error occurs, the zod error will always be placed in a `BadRequestException`. 88 | 89 | ## [2.0.1] - 2022-09-27 90 | ### Fixed 91 | - Nested classes were causing to register the top most class multiple times. 92 | 93 | ## [2.0.0] - 2022-08-23 94 | ### Added 95 | - `.transform` support. Now `zod` objects/properties may have `transform` calls. 96 | 97 | ### Changed 98 | - Renamed `InputZodType` decorator to `ZodArgs`. 99 | - Renamed `InputTypeFromZod` to `inputFromZod`. 100 | - `README` file. 101 | 102 | ## [1.0.0] - 2022-08-23 103 | ### Added 104 | - New decorators for `InputType` with `zod` validations: 105 | - `ZodArgs`: Takes a `zod` validation object and can be used in place of 106 | `Args`. If the validation fails, `BadRequest` exception will be thrown. 107 | The corresponding `GraphQL` schema objects will be created according to the 108 | given `zod` input. 109 | 110 | - `InputTypeWithZod`: This is a helper decorator that might be used rarely. 111 | When applied with a `zod` validation object to a class, the properties found 112 | in the class will be decorated with `Field` property and the class will be 113 | decorated with `InputType` decorator. 114 | 115 | - `ZodArgs.Of`: This is a type that will infer the type from a `zod` 116 | validation object. 117 | 118 | ### Fixed 119 | - Previosuly default values were being incorrectly set. 120 | 121 | ## [0.1.5] - 2022-05-08 122 | ### Changed 123 | When an error occurs, the `issues` are placed in `BadRequestException` body. 124 | 125 | ## [0.1.3] - 2022-05-08 126 | ### Changed 127 | Type checking strategy is moved to a function file, all type checking will be 128 | done through `isZodInstance` function. 129 | 130 | ## [0.1.2] - 2022-05-08 131 | ### Changed 132 | Type checking strategy is now changed to constructor name comparison. 133 | 134 | ## [0.1.1] - 2022-05-08 135 | ### Added 136 | The initial version of the package. 137 | 138 | [Unreleased]: https://github.com/incetarik/nestjs-graphql-zod/compare/3.2.0...HEAD 139 | 140 | [3.3.0]: https://github.com/incetarik/nestjs-graphql-zod/compare/3.2.0...3.3.0 141 | [3.2.0]: https://github.com/incetarik/nestjs-graphql-zod/compare/3.1.0...3.2.0 142 | [3.1.0]: https://github.com/incetarik/nestjs-graphql-zod/compare/3.0.0...3.1.0 143 | [3.0.0]: https://github.com/incetarik/nestjs-graphql-zod/compare/2.0.3...3.0.0 144 | [2.0.3]: https://github.com/incetarik/nestjs-graphql-zod/compare/2.0.2...2.0.3 145 | [2.0.2]: https://github.com/incetarik/nestjs-graphql-zod/compare/2.0.1...2.0.2 146 | [2.0.1]: https://github.com/incetarik/nestjs-graphql-zod/compare/2.0.0...2.0.1 147 | [2.0.0]: https://github.com/incetarik/nestjs-graphql-zod/compare/1.0.0...2.0.0 148 | [1.0.0]: https://github.com/incetarik/nestjs-graphql-zod/compare/0.1.5...1.0.0 149 | [0.1.5]: https://github.com/incetarik/nestjs-graphql-zod/compare/0.1.3...0.1.5 150 | [0.1.3]: https://github.com/incetarik/nestjs-graphql-zod/compare/0.1.2...0.1.3 151 | [0.1.2]: https://github.com/incetarik/nestjs-graphql-zod/compare/0.1.1...0.1.2 152 | [0.1.1]: https://github.com/incetarik/nestjs-graphql-zod/releases/tag/0.1.1 153 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nestjs-graphql-zod 2 | 3 | **Use `zod` validation objects in your GraphQL actions!** 4 | 5 | This library provides utility functions and decorators similar to NestJS 6 | GraphQL decorators that lets you work with `zod` objects without the need of 7 | writing GraphQL schema classes. 8 | 9 | - Nested `zod.object(...)` calls are supported. These will lead to generate 10 | another GraphQL model for each definition. 11 | 12 | - Descriptions are supported. Additionally, for `zod.object(...)` definitions, 13 | to provide a custom name (instead of the dynamically generated ones) the 14 | description should be in `{ClassName}:{Description}` format 15 | (for example `UpdateModel: Contains properties that will be updated`). This 16 | will cause a model generation with given class name with given description. 17 | 18 | - `zod.enum(...)` calls are supported. Enum models will be generated in GraphQL 19 | schema. 20 | 21 | - Primitive types are also supported and they will not cause any custom type 22 | creation in GraphQL schema file. 23 | 24 | ## Decorators 25 | All the decorators are the same with underlying decorator with an exception that 26 | the first parameter is the `zod` object. 27 | 28 | The overloads of the underlying decorators are also reflected. Therefore it is 29 | possible to use an overload of the decorators provided by this library. 30 | 31 | ### Method Decorators: 32 | - `@QueryWithZod` 33 | - `@MutationWithZod` 34 | - `@SubscriptionWithZod` 35 | 36 | These decorators will do **output validation**. 37 | They take `zod` object and validate the output with given `zod` object. 38 | 39 | ### Parameter/Class Decorators 40 | - `@ZodArgs` 41 | - `@InputTypeWithZod` 42 | 43 | These decorators will do **input validation**. 44 | They take `zod` object and validate the input with given `zod` object. 45 | 46 | ## Utility Functions 47 | - `modelFromZodBase`: Takes a `zod` input, options and a 48 | decorator to decorate the dynamically built class with (such as `ObjectType`). 49 | 50 | - `modelFromZod`: Takes a `zod` input and options to build 51 | an `ObjectType` decorated class and return the class itself. The class will 52 | contain the properties from the given `zod` input. 53 | 54 | - `inputFromZod`: Takes a `zod` input and options to build 55 | an `InputType` decorated class and return the class itself. The class will 56 | contain the properties from the given `zod` input. 57 | 58 | - `getZodObject`: Takes an object and returns the source `zod` object that is 59 | used to build the class. For this function to return a defined value, the 60 | classes should be built with `keepZodObject` option property set to `true`. 61 | 62 | - `setDefaultTypeProvider`: Takes a handler that will provide custom 63 | `GraphQLScalarType`s for any complex schemas. Therefore, it is also possible 64 | to set, for example, `JSONObject` for `z.record()` validations. 65 | 66 | - `getZodObjectName`: This function is producing a type string for the passed 67 | zod schema. The string will also be provided to the handler passed to 68 | `setDefaultTypeProvider` function. 69 | 70 | --- 71 | 72 | ## Setup 73 | - Add `nestjs-graphql-zod` to your dependencies in `package.json`. 74 | - Either use: 75 | - Classes which you can create with `modelFromZod`. 76 | - Use decorators for `GraphQL` action methods: 77 | - `@MutationWithZod` 78 | - `@QueryWithZod` 79 | - `@SubscriptionWithZod` 80 | 81 | or decorators for parameters: 82 | - `@ZodArgs` 83 | 84 | These are the same with their corresponding `GraphQL` method decorators. 85 | Yet, they work with `zod` objects. 86 | 87 | ## Example 88 | ### Simple 89 | ```ts 90 | import * as zod from 'zod' 91 | 92 | const UserZod = zod.object({ 93 | name: zod.string().describe('The name of the user'), 94 | age: zod.number().int().gt(10).describe('The age of the user.'), 95 | fields: zod.string().optional().array().optional().describe('The fields of the user'), 96 | sortBy: zod.enum([ 'asc', 'desc' ]).describe('The sorting parameter of user.') 97 | }).describe('ExampleUser: Represents an example user instance.') 98 | 99 | class UserResolver { 100 | @QueryWithZod(UserZod) 101 | async getUser() { 102 | // You can simply return an object to be parsed and if the parsing is 103 | // successful, then the data will be returned, otherwise an error will 104 | // be thrown. 105 | 106 | return { 107 | name: 'User Name', 108 | age: 15, 109 | fields: [ 'Field 1', 'Field 2' ], 110 | sortBy: 'asc' 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | With the example above, you will have the following generated `GraphQL` schema 117 | type if you use `code-first` approach: 118 | 119 | ```gql 120 | """ Represents an example user instance.""" 121 | type ExampleUser { 122 | """The name of the user""" 123 | name: String! 124 | 125 | """The age of the user.""" 126 | age: Int! 127 | 128 | """The fields of the user""" 129 | fields: [String] 130 | 131 | """The sorting parameter of user.""" 132 | sortBy: ExampleUser_SortByEnum_0! 133 | } 134 | 135 | """The sorting parameter of user.""" 136 | enum ExampleUser_SortByEnum_0 { 137 | asc 138 | desc 139 | } 140 | ``` 141 | 142 | ### Nested Object 143 | ```ts 144 | import * as zod from 'zod' 145 | 146 | const UserZod = zod.object({ 147 | name: zod.string().describe('The name of the user'), 148 | age: zod.number().int().gt(10).describe('The age of the user.'), 149 | fields: zod.string().optional().array().optional().describe('The fields of the user'), 150 | sortBy: zod.enum([ 'asc', 'desc' ]).describe('The sorting parameter of user.'), 151 | settings: zod.object({ 152 | darkTheme: zod.boolean().optional().describe('The dark theme setting'), 153 | ratio: zod.number().describe('This will be float by default'), 154 | profile: zod.object({ 155 | showImage: zod.boolean().describe('Indicates whether the user is showing images.'), 156 | }).describe('UserProfileSetting: Represents user profile settings.'), 157 | }).describe('ExampleUserSettings: The user settings.'), 158 | }).describe('ExampleUser: Represents an example user instance.') 159 | 160 | class UserResolver { 161 | @QueryWithZod(UserZod) 162 | async getUser() { 163 | // You can simply return an object to be parsed and if the parsing is 164 | // successful, then the data will be returned, otherwise an error will 165 | // be thrown. 166 | 167 | return { 168 | name: 'User Name', 169 | age: 15, 170 | fields: [ 'Field 1', 'Field 2' ], 171 | sortBy: 'asc', 172 | settings: { 173 | darkTheme: false, 174 | ratio: 2.5, 175 | profile: { 176 | showImage: true 177 | } 178 | } 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | With the example above, you will have the following generated `GraphQL` schema 185 | type if you use `code-first` approach: 186 | 187 | ```gql 188 | """ Represents an example user instance.""" 189 | type ExampleUser { 190 | """The name of the user""" 191 | name: String! 192 | 193 | """The age of the user.""" 194 | age: Int! 195 | 196 | """The fields of the user""" 197 | fields: [String] 198 | 199 | """The sorting parameter of user.""" 200 | sortBy: ExampleUser_SortByEnum_0! 201 | 202 | """ExampleUserSettings: The user settings.""" 203 | settings: ExampleUser_Settings! 204 | } 205 | 206 | """The sorting parameter of user.""" 207 | enum ExampleUser_SortByEnum_0 { 208 | asc 209 | desc 210 | } 211 | 212 | """ExampleUserSettings: The user settings.""" 213 | type ExampleUser_Settings { 214 | """The dark theme setting""" 215 | darkTheme: Boolean 216 | 217 | """This will be float by default""" 218 | ratio: Float! 219 | 220 | """UserProfileSetting: Represents user profile settings.""" 221 | profile: ExampleUser_Settings_Profile! 222 | } 223 | 224 | """UserProfileSetting: Represents user profile settings.""" 225 | type ExampleUser_Settings_Profile { 226 | """Indicates whether the user is showing images.""" 227 | showImage: Boolean! 228 | } 229 | ``` 230 | 231 | ### InputType/Args Example 232 | ```ts 233 | import * as zod from 'zod' 234 | import { ZodArgs } from 'nestjs-graphql-zod' 235 | 236 | const RequestSchema = zod.object({ 237 | username: zod.string().min(5).max(20).describe('The username of the request owner'), 238 | email: zod.string().email().describe('The email of the user'), 239 | changes: zod.object({ 240 | themeSelection: zod.enum([ 'light', 'dark' ]).describe('The theme type'), 241 | permissions: zod.object({ 242 | add: zod.number().array().describe('The flags added to the user permissions'), 243 | remove: zod.number().array().describe('The flags removed to the user permissions'), 244 | isAdmin: zod.boolean().describe('Indicates if the user is an admin') 245 | }).describe('The permissions change set of the user') 246 | }).describe('The changes made by the user') 247 | }).describe('RequestSchema: The request schema type for changing user data') 248 | 249 | class ExampleResolver() { 250 | @Query(() => Boolean) 251 | processRequest(@ZodArgs(RequestSchema) input: ZodArgs.Of) { 252 | // The input will contain all the properties validated according to the 253 | // schema defined above. If the validation was failed, the user will get 254 | // BadRequest error and this method will not be called. 255 | 256 | // The @ZodArgs(Schema) decorator is behaving like 257 | // @Args() + @InputType() decorators. 258 | // 259 | // The @InputType() is applied to underlying class, the @Args() is applied 260 | // to take the input as the parameter. By default, the name of the 261 | // property will be 'input'. This can be changed through the overloads 262 | // of the decorator. 263 | } 264 | } 265 | ``` 266 | 267 | With the example above, you will have the following generated `GraphQL` schema 268 | type if you use `code-first` approach: 269 | 270 | ```gql 271 | """The request schema type for changing user data""" 272 | input RequestSchema { 273 | """The username of the request owner""" 274 | username: String! 275 | 276 | """The email of the user""" 277 | email: String! 278 | 279 | """The changes made by the user""" 280 | changes: RequestSchema_Changes! 281 | } 282 | 283 | """The request schema type for changing user data""" 284 | input RequestSchema_Changes { 285 | """The theme type""" 286 | themeSelection: RequestSchema_Changes_ThemeSelectionEnum_0! 287 | 288 | """The permissions change set of the user""" 289 | permissions: RequestSchema_Changes_Permissions! 290 | } 291 | 292 | """The theme type""" 293 | enum RequestSchema_Changes_ThemeSelectionEnum_0 { 294 | light 295 | dark 296 | } 297 | 298 | """The request schema type for changing user data""" 299 | input RequestSchema_Changes_Permissions { 300 | """The flags added to the user permissions""" 301 | add: [Float!]! 302 | 303 | """The flags removed to the user permissions""" 304 | remove: [Float!]! 305 | 306 | """Indicates if the user is an admin""" 307 | isAdmin: Boolean! 308 | } 309 | ``` 310 | 311 | Inline `@ZodArgs` example with complex type input. 312 | ```ts 313 | 314 | setDefaultTypeProvider((typeName) => { 315 | if (typeName.startsWith('Record')) { 316 | return GraphQLJSONObject // a custom scalar type 317 | } 318 | 319 | // Otherwise, return no custom scalar type which will cause 320 | // a parsing error. 321 | }) 322 | 323 | class ExampleResolver() { 324 | @Query(() => Boolean) 325 | inlineExample( 326 | @ZodArgs(zod.number().gt(10).array()) numberArray: number[], 327 | @ZodArgs(zod.record(zod.number())) dictionary: Record, 328 | @ZodArgs(zod.string().url().optional()) urlString?: string, 329 | ) { 330 | // Here the @ZodArgs(Schema) decorator is inlined and will produce 331 | // corresponding schema file. If the types are primitives, there will 332 | // be no custom proxy type creation. 333 | 334 | // The record example above will require a custom scalar type to be 335 | // provided. The provider function can be set in the decorator properties 336 | // or through the global/default setter function as in this example. 337 | } 338 | } 339 | ``` 340 | 341 | # Support 342 | To support the project, you can send donations to following addresses: 343 | 344 | ```md 345 | - Bitcoin : bc1qtut2ss8udkr68p6k6axd0na6nhvngm5dqlyhtn 346 | - Bitcoin Cash: qzmmv43ztae0tfsjx8zf4wwnq3uk6k7zzgcfr9jruk 347 | - Ether : 0xf542BED91d0218D9c195286e660da2275EF8eC84 348 | ``` 349 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-graphql-zod", 3 | "version": "3.4.1", 4 | "description": "A library for integrating zod validation objects into nestjs graphql objects.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "test": "jest", 9 | "prepublish": "tsc", 10 | "build": "tsc", 11 | "build-watch": "tsc --watch", 12 | "release": "release-it" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/incetarik/nestjs-graphql-zod.git" 17 | }, 18 | "keywords": [ 19 | "nestjs", 20 | "zod", 21 | "resolver", 22 | "decorator", 23 | "graphql", 24 | "validation" 25 | ], 26 | "author": "Tarık İnce ", 27 | "license": "SEE LICENSE IN LICENSE", 28 | "bugs": { 29 | "url": "https://github.com/incetarik/nestjs-graphql-zod/issues" 30 | }, 31 | "homepage": "https://github.com/incetarik/nestjs-graphql-zod#readme", 32 | "peerDependencies": { 33 | "@nestjs/graphql": ">= 10.0", 34 | "@nestjs/core": ">= 8.0", 35 | "class-transformer": ">= 0.5.1", 36 | "@nestjs/common": ">= 8.0", 37 | "rxjs": ">= 7.5.5", 38 | "zod": ">= 3.15", 39 | "reflect-metadata": ">= 0.1.13", 40 | "graphql": ">= 16.3.0" 41 | }, 42 | "devDependencies": { 43 | "typescript": "4.9.5", 44 | "release-it": "*" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/decorators/common.ts: -------------------------------------------------------------------------------- 1 | import type { DynamicZodModelClass, GraphQLMDF } from './types' 2 | import type { WrapWithZodOptions } from './zod-options-wrapper.interface' 3 | import type { TypeProvider } from '../types/type-provider' 4 | import type { EnumProvider } from '../types/enum-provider' 5 | 6 | import type { AnyZodObject } from 'zod' 7 | import type { BaseTypeOptions } from '@nestjs/graphql' 8 | 9 | import { IModelFromZodOptions, modelFromZod } from '../model-from-zod' 10 | import { decorateWithZodInput } from './decorate-with-zod-input' 11 | import { makeDecoratorFromFactory } from './make-decorator-from-factory' 12 | 13 | type BaseOptions = WrapWithZodOptions 14 | 15 | let DEFAULT_TYPE_PROVIDER: TypeProvider | undefined 16 | let DEFAULT_ENUM_PROVIDER: EnumProvider | undefined 17 | 18 | /** 19 | * Returns a method decorator that is built with `zod` validation object. 20 | * 21 | * @export 22 | * @template T The type of the `zod` validation object. 23 | * @param {T} input The `zod` validation object. 24 | * @param {(string | BaseOptions | undefined)} nameOrOptions The name or 25 | * the options. 26 | * 27 | * @param {GraphQLMDF} graphqlDecoratorFactory The actual 28 | * decorator factory function. 29 | * 30 | * @param {DynamicZodModelClass} model The dynamically built model class from 31 | * `zod` validation object. 32 | * 33 | * @return {MethodDecorator} A method decorator. 34 | */ 35 | export function MethodWithZodModel( 36 | input: T, 37 | nameOrOptions: string | BaseOptions | undefined, 38 | graphqlDecoratorFactory: GraphQLMDF, 39 | model: DynamicZodModelClass 40 | ): MethodDecorator { 41 | return function _ModelWithZod( 42 | target: Record, 43 | methodName: string | symbol, 44 | descriptor: PropertyDescriptor 45 | ) { 46 | let newDescriptor = descriptor || {} 47 | 48 | const originalFunction = descriptor?.value ?? target[ methodName ] 49 | 50 | let decorationProps: typeof nameOrOptions 51 | if (typeof nameOrOptions === 'string') { 52 | decorationProps = { 53 | zod: { parseToInstance: true }, 54 | } 55 | } 56 | else { 57 | decorationProps = nameOrOptions 58 | } 59 | 60 | const decoratedFunction = decorateWithZodInput(originalFunction, input, model, decorationProps) 61 | 62 | newDescriptor.value = decoratedFunction 63 | 64 | if (!descriptor) { 65 | Object.defineProperty(target, methodName, newDescriptor) 66 | } 67 | 68 | const methodDecorator = makeDecoratorFromFactory( 69 | nameOrOptions, 70 | graphqlDecoratorFactory, 71 | model 72 | ) 73 | 74 | methodDecorator(target, methodName, newDescriptor) 75 | } 76 | } 77 | 78 | /** 79 | * Returns a method decorator that is built with `zod` validation object. 80 | * 81 | * @export 82 | * @template T The type of the `zod` validation object. 83 | * @param {T} input The `zod` validation object. 84 | * @param {(string | BaseOptions | undefined)} nameOrOptions The name or 85 | * the options. 86 | * 87 | * @param {GraphQLMDF} graphqlDecoratorFactory The actual 88 | * decorator factory function. 89 | * 90 | * @return {MethodDecorator} A method decorator. 91 | */ 92 | export function MethodWithZod( 93 | input: T, 94 | nameOrOptions: string | BaseOptions | undefined, 95 | graphqlDecoratorFactory: GraphQLMDF 96 | ) { 97 | let zodOptions: IModelFromZodOptions | undefined 98 | 99 | if (typeof nameOrOptions === 'object') { 100 | zodOptions = nameOrOptions.zod 101 | } 102 | 103 | return MethodWithZodModel( 104 | input, 105 | nameOrOptions, 106 | graphqlDecoratorFactory, 107 | modelFromZod(input, zodOptions) as DynamicZodModelClass 108 | ) 109 | } 110 | 111 | /** 112 | * Sets the default type provider for custom GraphQL Scalars. 113 | * 114 | * The type name will be calculated and it will be similar to `TypeScript` 115 | * types such as: `Record, Array>`. 116 | * 117 | * The user will provide custom scalar type to use for that kind of 118 | * zod validation. 119 | * 120 | * @export 121 | * @param {TypeProvider} fn The type provider. 122 | */ 123 | export function setDefaultTypeProvider(fn: TypeProvider) { 124 | DEFAULT_TYPE_PROVIDER = fn 125 | } 126 | 127 | /** 128 | * Gets the default type provided set previously 129 | * via {@link setDefaultTypeProvider}. 130 | * 131 | * @export 132 | * @return {TypeProvider | undefined} The default type provider. 133 | */ 134 | export function getDefaultTypeProvider(): TypeProvider | undefined { 135 | return DEFAULT_TYPE_PROVIDER 136 | } 137 | 138 | /** 139 | * Sets the default enum provider for custom GraphQL Scalars. 140 | * 141 | * @export 142 | * @param {EnumProvider} fn The enum provider. 143 | */ 144 | export function setDefaultEnumProvider(fn: EnumProvider) { 145 | DEFAULT_ENUM_PROVIDER = fn 146 | } 147 | 148 | /** 149 | * Gets the default enum provided set previously 150 | * via {@link setDefaultEnumProvider}. 151 | * 152 | * @export 153 | * @return {EnumProvider | undefined} The default enum provider. 154 | */ 155 | export function getDefaultEnumProvider(): EnumProvider | undefined { 156 | return DEFAULT_ENUM_PROVIDER 157 | } 158 | -------------------------------------------------------------------------------- /src/decorators/decorate-with-zod-input.ts: -------------------------------------------------------------------------------- 1 | import type { BaseOptions } from './zod-options-wrapper.interface' 2 | import type { DynamicZodModelClass } from './types' 3 | 4 | import { plainToInstance } from 'class-transformer' 5 | import { AnyZodObject, ZodError } from 'zod' 6 | 7 | import { BadRequestException } from '@nestjs/common' 8 | 9 | type Fn = (...args: any) => any 10 | 11 | /** 12 | * Decorates a method with given zod validation object. 13 | * 14 | * @export 15 | * @template T The type of the zod validation object. 16 | * @template F The type of the function that will be replaced. 17 | * @param {Function} originalFunction The original function which will be 18 | * replaced. 19 | * 20 | * @param {T} input The zod validation object. 21 | * @param {DynamicZodModelClass} model The dynamically built zod class that 22 | * has the validations installed. 23 | * 24 | * @return {F} 25 | */ 26 | export function decorateWithZodInput< 27 | T extends AnyZodObject, 28 | F extends Fn = Fn 29 | >( 30 | originalFunction: F, 31 | input: T, 32 | model: DynamicZodModelClass, 33 | options?: BaseOptions 34 | ) { 35 | return function _modelWithZod(this: any, ...args: Parameters) { 36 | const result = originalFunction.apply(this, args) 37 | let parseToInstance = true 38 | 39 | if (typeof options?.zod === 'object') { 40 | if (typeof options.zod.parseToInstance === 'boolean') { 41 | parseToInstance = options.zod.parseToInstance 42 | } 43 | } 44 | 45 | if (result instanceof Promise) { 46 | return result 47 | .then(output => input.parseAsync(output)) 48 | .then(output => parseToInstance ? plainToInstance(model, output) : output) 49 | .catch((error: Error) => { 50 | if (error instanceof ZodError) { 51 | throw new BadRequestException(error.issues) 52 | } 53 | else { 54 | throw error 55 | } 56 | }) 57 | } 58 | else { 59 | const parseResult = input.safeParse(result) 60 | if (parseResult.success) { 61 | return parseToInstance ? plainToInstance(model, parseResult.data) : parseResult.data 62 | } 63 | else { 64 | throw new BadRequestException(parseResult.error.issues) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './query-types' 2 | export * from './input-type' 3 | export { setDefaultTypeProvider, setDefaultEnumProvider } from './common' 4 | -------------------------------------------------------------------------------- /src/decorators/input-type/index.ts: -------------------------------------------------------------------------------- 1 | export { inputFromZod } from './input-from-zod' 2 | export { InputTypeWithZod } from './input-type-with-zod' 3 | export { ZodArgs } from './zod-args' 4 | -------------------------------------------------------------------------------- /src/decorators/input-type/input-from-zod.ts: -------------------------------------------------------------------------------- 1 | import { InputTypeWithZod } from './input-type-with-zod' 2 | 3 | import type { AnyZodObject, infer as Infer } from 'zod' 4 | import type { Type } from '@nestjs/common' 5 | 6 | import type { Options } from './options.inteface' 7 | 8 | /** 9 | * Returns a {@link InputTypeWithZod} decorated class from given `zod` input. 10 | * 11 | * You can use this returned dynamic class to extend your classes. 12 | * @export 13 | * @template T The type of the `zod` object input. 14 | * @param {T} input The `zod` object input. 15 | * @return {Type>} A class that contains the properties from given 16 | * `zod` input, decorated with {@link InputTypeWithZod}. 17 | */ 18 | export function inputFromZod(input: T): Type> 19 | 20 | /** 21 | * Returns a {@link InputTypeWithZod} decorated class from given `zod` input. 22 | * 23 | * You can use this returned dynamic class to extend your classes. 24 | * @export 25 | * @template T The type of the `zod` object input. 26 | * @param {T} input The `zod` object input. 27 | * @param {Options} options The options for the decorator. 28 | * @return {Type>} A class that contains the properties from given 29 | * `zod` input, decorated with {@link InputTypeWithZod}. 30 | */ 31 | export function inputFromZod(input: T, options: Options): Type> 32 | 33 | /** 34 | * Returns a {@link InputTypeWithZod} decorated class from given `zod` input. 35 | * 36 | * You can use this returned dynamic class to extend your classes. 37 | * @export 38 | * @template T The type of the `zod` object input. 39 | * @param {T} input The `zod` object input. 40 | * @param {string} name The name of the {@link InputType}. 41 | * @param {Options} options The options for the decorator. 42 | * @return {Type>} A class that contains the properties from given 43 | * `zod` input, decorated with {@link InputTypeWithZod}. 44 | */ 45 | export function inputFromZod(input: T, name: string, options?: Options): Type> 46 | 47 | export function inputFromZod( 48 | input: T, 49 | nameOrOptions?: string | Options, 50 | options?: Options 51 | ) { 52 | class DynamicZodModel {} 53 | 54 | InputTypeWithZod(input, nameOrOptions as string, options)(DynamicZodModel) 55 | return DynamicZodModel as Type> 56 | } 57 | -------------------------------------------------------------------------------- /src/decorators/input-type/input-type-with-zod.ts: -------------------------------------------------------------------------------- 1 | import { InputType, InputTypeOptions } from '@nestjs/graphql' 2 | 3 | import { extractNameAndDescription, parseShape } from '../../helpers' 4 | import { ZodObjectKey } from '../../helpers/constants' 5 | 6 | import type { AnyZodObject } from 'zod' 7 | import type { Options } from './options.inteface' 8 | 9 | /** 10 | * Decorator that marks a class as a GraphQL input type. 11 | * 12 | * Uses a `zod` object. 13 | * 14 | * @export 15 | * @template T The type of the zod object input. 16 | * @param {T} input The zod input object. 17 | * @return {ClassDecorator} A {@link ClassDecorator}. 18 | */ 19 | export function InputTypeWithZod(input: T): ClassDecorator 20 | 21 | /** 22 | * Decorator that marks a class as a GraphQL input type. 23 | * 24 | * Uses a `zod` object. 25 | * 26 | * @export 27 | * @template T The type of the zod object input. 28 | * @param {T} input The zod input object. 29 | * @param {Options} options The options for the decorator. 30 | * @return {ClassDecorator} A {@link ClassDecorator}. 31 | */ 32 | export function InputTypeWithZod(input: T, options: Options): ClassDecorator 33 | 34 | /** 35 | * Decorator that marks a class as a GraphQL input type. 36 | * 37 | * Uses a `zod` object. 38 | * 39 | * @export 40 | * @template T The type of the zod object input. 41 | * @param {T} input The zod input object. 42 | * @param {string} name The name of the {@link InputType}. 43 | * @param {Options} [options] The options for the decorator. 44 | * @return {ClassDecorator} A {@link ClassDecorator}. 45 | */ 46 | export function InputTypeWithZod(input: T, name: string, options?: Options): ClassDecorator 47 | 48 | export function InputTypeWithZod( 49 | input: T, 50 | nameOrOptions?: string | Options, 51 | options?: Options 52 | ): ClassDecorator { 53 | //#region Parameter Normalization - `name`, `zodOptions`, `inputTypeOptions` 54 | if (typeof nameOrOptions === 'object') { 55 | options = nameOrOptions 56 | nameOrOptions = undefined 57 | } 58 | 59 | if (!nameOrOptions) { 60 | nameOrOptions = options?.name 61 | } 62 | 63 | const name = nameOrOptions 64 | let zodOptions = options?.zod 65 | 66 | let inputTypeOptions: InputTypeOptions | undefined 67 | if (typeof options === 'object') { 68 | const { name: _, zod: __, ...rest } = options 69 | inputTypeOptions = rest 70 | } 71 | //#endregion 72 | 73 | const decorate = buildInputTypeDecorator(name, inputTypeOptions) 74 | 75 | return function ZodClassDecoratorBase(target: Function) { 76 | zodOptions ??= {} 77 | 78 | const { prototype } = target 79 | const { description, name = target.name } = extractNameAndDescription(input, zodOptions) 80 | const { keepZodObject = false } = zodOptions 81 | 82 | const returnValue = decorate(target) 83 | 84 | if (keepZodObject) { 85 | Object.defineProperty(prototype, ZodObjectKey, { 86 | value: { ...input }, 87 | configurable: false, 88 | writable: false 89 | }) 90 | } 91 | 92 | const parsed = parseShape(input, { 93 | ...zodOptions, 94 | name, 95 | description, 96 | getDecorator(_, key) { 97 | // Returning another `@InputType()` as we have another key now, that 98 | // will be another sub class built, therefore we need to decorate that 99 | // with another `@InputType()`. 100 | return buildInputTypeDecorator(key, inputTypeOptions) 101 | }, 102 | getScalarTypeFor: zodOptions.getScalarTypeFor, 103 | }) 104 | 105 | for (const { descriptor, key, decorateFieldProperty } of parsed) { 106 | Object.defineProperty(prototype, key as string, descriptor) 107 | decorateFieldProperty(prototype, key as string) 108 | } 109 | 110 | return returnValue as void 111 | } 112 | } 113 | 114 | /** 115 | * Builds an input type decorator for given name and options. 116 | * 117 | * @param {string} [name] The name of the property. 118 | * @param {InputTypeOptions} [opts] The options for the decorator. 119 | * @return {ClassDecorator} A decorator for the dynamic input type class. 120 | */ 121 | export function buildInputTypeDecorator(name?: string, opts?: InputTypeOptions): ClassDecorator { 122 | if (typeof opts === 'object') { 123 | if (typeof name === 'string') { 124 | return InputType(name, opts) 125 | } 126 | 127 | return InputType(opts) 128 | } 129 | 130 | if (typeof name === 'string') { 131 | return InputType(name) 132 | } 133 | 134 | return InputType() 135 | } 136 | 137 | -------------------------------------------------------------------------------- /src/decorators/input-type/options.inteface.ts: -------------------------------------------------------------------------------- 1 | import type { InputTypeOptions } from '@nestjs/graphql' 2 | import type { ZodType } from 'zod' 3 | 4 | import type { WrapWithZodOptions } from '../zod-options-wrapper.interface' 5 | 6 | /** 7 | * An option type for decorators. 8 | * 9 | * @export 10 | * @interface Options 11 | * @extends {WrapWithZodOptions} 12 | * @template T The type of the validation object. 13 | */ 14 | export interface Options extends WrapWithZodOptions { 15 | /** 16 | * The name of the {@link InputType}. 17 | * 18 | * @type {string} 19 | * @memberof Options 20 | */ 21 | name?: string 22 | } 23 | -------------------------------------------------------------------------------- /src/decorators/input-type/zod-args.ts: -------------------------------------------------------------------------------- 1 | import { AnyZodObject, infer as Infer, ZodObject, ZodTypeAny } from 'zod' 2 | 3 | import { PipeTransform, Type } from '@nestjs/common' 4 | import { Args, ArgsOptions } from '@nestjs/graphql' 5 | 6 | import { extractNameAndDescription, getNullability } from '../../helpers' 7 | import { getDescription } from '../../helpers/get-description' 8 | import { getFieldInfoFromZod } from '../../helpers/get-field-info-from-zod' 9 | import { isZodInstance } from '../../helpers/is-zod-instance' 10 | import { ZodValidatorPipe } from '../../helpers/zod-validator.pipe' 11 | import { EnumProvider } from '../../types/enum-provider' 12 | import { TypeProvider } from '../../types/type-provider' 13 | import { getDefaultTypeProvider } from '../common' 14 | import { inputFromZod } from './input-from-zod' 15 | 16 | type PT = PipeTransform | Type> 17 | 18 | type CustomDecoratorOptions = { 19 | /** 20 | * Gets the scalar type for given type name. 21 | * 22 | * @param {string} typeName The type name corresponding to the zod object. 23 | * @return {GraphQLScalarType} The scalar type for the zod object. 24 | */ 25 | getScalarTypeFor?: TypeProvider 26 | 27 | /** 28 | * Provides a name for nested classes when they are created dynamically from 29 | * object properties of zod types. 30 | * 31 | * @param {string} parentName The parent class name. 32 | * @param {string} propertyKey The property key/name. 33 | * @return {(string | undefined)} The name to set for the class. If 34 | * any value returned other than a `string`, the class name will be generated 35 | * automatically. 36 | * 37 | * @memberof IModelFromZodOptions 38 | */ 39 | provideNameForNestedClass?(parentName: string, propertyKey: string): string | undefined 40 | 41 | /** 42 | * Gets an enum type for given information. 43 | * 44 | * Use this function to prevent creating different enums in GraphQL schema 45 | * if you are going to use same values in different places. 46 | * 47 | * @param {string | undefined} name The parent name that contains the enum 48 | * type. 49 | * @param {string} key The property name of the enum. 50 | * @param {(Record)} enumObject The enum object 51 | * that is extracted from the zod. 52 | * @return {(Record | undefined)} The enum 53 | * that will be used instead of creating a new one. If `undefined` is 54 | * returned, then a new enum will be created. 55 | * 56 | * @memberof IModelFromZodOptions 57 | */ 58 | getEnumType?: EnumProvider 59 | } 60 | 61 | type DecoratorOptions = ArgsOptions & CustomDecoratorOptions 62 | 63 | let GENERATED_TYPES: WeakMap | undefined 64 | let USED_NAMES: string[] | undefined 65 | 66 | /** 67 | * Creates a new type from given zod object or returns previously created one. 68 | * 69 | * @template T The type of the zod object passed. 70 | * @param {T} input The zod scheme object. 71 | * @param {CustomDecoratorOptions} options The custom decorator options. 72 | * @return {*} The newly or previously created class instance. 73 | */ 74 | function _getOrCreateRegisteredType( 75 | input: T, 76 | options: CustomDecoratorOptions 77 | ) { 78 | if (!GENERATED_TYPES) { GENERATED_TYPES = new WeakMap() } 79 | let RegisteredType = GENERATED_TYPES.get(input) as Type> | undefined 80 | if (RegisteredType) return RegisteredType 81 | 82 | const { name, description } = extractNameAndDescription(input, {}) 83 | const safeName = _getSafeName(name) 84 | RegisteredType = inputFromZod(input, { 85 | name: safeName, 86 | description, 87 | zod: { 88 | name: safeName, 89 | description, 90 | getEnumType: options.getEnumType, 91 | getScalarTypeFor: options.getScalarTypeFor, 92 | provideNameForNestedClass: options.provideNameForNestedClass, 93 | } 94 | }) 95 | 96 | GENERATED_TYPES.set(input, RegisteredType) 97 | return RegisteredType 98 | } 99 | 100 | /** 101 | * Checks if the name is used before, in that case, adds a suffix of `_{number}` 102 | * indicating the number of times the name is used. 103 | * 104 | * @param {string} name The name to check. 105 | * @return {string} The name that is not used in any other types before. 106 | */ 107 | function _getSafeName(name: string): string { 108 | if (!USED_NAMES) { USED_NAMES = [] } 109 | 110 | let total = 0 111 | for (let i = 0, limit = USED_NAMES.length; i < limit; ++i) { 112 | const current = USED_NAMES[ i ] 113 | if (current.startsWith(name)) { ++total } 114 | } 115 | 116 | if (total) { 117 | const newName = `${name}_${total + 1}` 118 | USED_NAMES.push(newName) 119 | return newName 120 | } 121 | 122 | USED_NAMES.push(name) 123 | return name 124 | } 125 | 126 | /** 127 | * A parameter decorator that takes a `zod` validation input and marks it as 128 | * GraphQL `Args` with `property` name with given `options` and pipes. 129 | * 130 | * @export 131 | * @template T The type of the `zod` validation input. 132 | * @param {T} input The `zod` validation schema object. 133 | * @param {string} property The name of the property for the GraphQL request 134 | * argument. 135 | * 136 | * @param {DecoratorOptions} options The options for {@link Args} decorator. 137 | * @param {...PT[]} pipes The pipes that will be passed to {@link Args} 138 | * decorator. 139 | * 140 | * @return {ParameterDecorator} A {@link ParameterDecorator} for GraphQL 141 | * argument. 142 | */ 143 | export function ZodArgs( 144 | input: T, 145 | property: string, 146 | options: DecoratorOptions, 147 | ...pipes: PT[] 148 | ): ParameterDecorator 149 | 150 | /** 151 | * A parameter decorator that takes a `zod` validation input and marks it as 152 | * GraphQL `Args` with `property` name with given `options` and pipes. 153 | * 154 | * @export 155 | * @template T The type of the `zod` validation input. 156 | * @param {T} input The `zod` validation schema object. 157 | * @param {DecoratorOptions} options The options for {@link Args} decorator. 158 | * @param {...PT[]} pipes The pipes that will be passed to {@link Args} 159 | * decorator. 160 | * 161 | * @return {ParameterDecorator} A {@link ParameterDecorator} for GraphQL 162 | * argument. 163 | */ 164 | export function ZodArgs( 165 | input: T, 166 | options: DecoratorOptions, 167 | ...pipes: PT[] 168 | ): ParameterDecorator 169 | 170 | /** 171 | * A parameter decorator that takes a `zod` validation input and marks it as 172 | * GraphQL `Args` with `property` name with given `options` and pipes. 173 | * 174 | * @export 175 | * @template T The type of the `zod` validation input. 176 | * @param {T} input The `zod` validation schema object. 177 | * @param {string} property The name of the property for the GraphQL request 178 | * argument. 179 | * 180 | * @param {...PT[]} pipes The pipes that will be passed to {@link Args} 181 | * decorator. 182 | * 183 | * @return {ParameterDecorator} A {@link ParameterDecorator} for GraphQL 184 | * argument. 185 | */ 186 | export function ZodArgs( 187 | input: T, 188 | property: string, 189 | ...pipes: PT[] 190 | ): ParameterDecorator 191 | 192 | /** 193 | * A parameter decorator that takes a `zod` validation input and marks it as 194 | * GraphQL `Args` with `property` name with given `options` and pipes. 195 | * 196 | * @export 197 | * @template T The type of the `zod` validation input. 198 | * @param {T} input The `zod` validation schema object. 199 | * @param {...PT[]} pipes The pipes that will be passed to {@link Args} 200 | * decorator. 201 | * 202 | * @return {ParameterDecorator} A {@link ParameterDecorator} for GraphQL 203 | * argument. 204 | */ 205 | export function ZodArgs( 206 | input: T, 207 | ...pipes: PT[] 208 | ): ParameterDecorator 209 | 210 | export function ZodArgs( 211 | input: T, 212 | propertyOrOptions?: string | DecoratorOptions | PT, 213 | optionsOrPipe?: DecoratorOptions | PT, 214 | ...pipes: PT[] 215 | ): ParameterDecorator { 216 | let property: string | undefined 217 | let options: DecoratorOptions | undefined 218 | 219 | // Parameter normalization 220 | if (typeof propertyOrOptions === 'string') { 221 | property = propertyOrOptions 222 | if (typeof optionsOrPipe === 'object') { 223 | if ('transform' in optionsOrPipe) { 224 | pipes.unshift(optionsOrPipe) 225 | } 226 | else { 227 | options = optionsOrPipe 228 | } 229 | } 230 | } 231 | else if (typeof propertyOrOptions === 'object') { 232 | if (typeof optionsOrPipe === 'object') { 233 | if ('transform' in optionsOrPipe) { 234 | pipes.unshift(optionsOrPipe) 235 | } 236 | else { 237 | options = optionsOrPipe 238 | } 239 | } 240 | else if ('transform' in propertyOrOptions) { 241 | pipes.unshift(propertyOrOptions) 242 | } 243 | else { 244 | options = propertyOrOptions 245 | } 246 | } 247 | 248 | options ??= {} 249 | const { getScalarTypeFor = getDefaultTypeProvider() } = options 250 | 251 | if (!isZodInstance(ZodObject, input)) { 252 | pipes.unshift(new ZodValidatorPipe(input)) 253 | const typeInfo = getFieldInfoFromZod('', input, options) 254 | const nullability = getNullability(typeInfo) 255 | const description = getDescription(input) 256 | 257 | const { type } = typeInfo 258 | options.type = () => type 259 | options.nullable = nullability 260 | options.description ??= description 261 | } 262 | else { 263 | const RegisteredType = _getOrCreateRegisteredType( 264 | input as AnyZodObject, 265 | { 266 | getScalarTypeFor 267 | } 268 | ) 269 | 270 | pipes.unshift(new ZodValidatorPipe(input, RegisteredType)) 271 | options.type ??= () => RegisteredType 272 | } 273 | 274 | if (options.name) { 275 | return prepareDecorator(property, options, ...pipes) 276 | } 277 | else { 278 | return function _anonymousZodArgsWrapper(target, propKey, index) { 279 | options ??= {} 280 | options!.name = `arg_${index}` 281 | const decorator = prepareDecorator(property, options, ...pipes) 282 | decorator(target, propKey, index) 283 | } 284 | } 285 | } 286 | 287 | /** 288 | * Gets a prepared {@link ParameterDecorator} after {@link Args} is called. 289 | * 290 | * @param {string} [property] The property string. 291 | * @param {DecoratorOptions} [options] The decorator options. 292 | * @param {...PT[]} pipes The pipes to apply. 293 | * @return {ParameterDecorator} The built parameter decorator. 294 | */ 295 | function prepareDecorator(property?: string, options?: DecoratorOptions, ...pipes: PT[]): ParameterDecorator { 296 | let args: ParameterDecorator 297 | if (typeof property === 'string') { 298 | if (typeof options === 'object') { 299 | args = Args(property, options, ...pipes) 300 | } 301 | else { 302 | args = Args(property, ...pipes) 303 | } 304 | } 305 | else if (typeof options === 'object') { 306 | args = Args(options, ...pipes) 307 | } 308 | else { 309 | args = Args(...pipes) 310 | } 311 | 312 | return args 313 | } 314 | 315 | export module ZodArgs { 316 | /** 317 | * A type for inferring the type of a given `zod` validation object. 318 | */ 319 | export type Of = Infer 320 | 321 | /** 322 | * Frees the used objects during the startup. 323 | * 324 | * The {@link ZodArgs} decorator uses helper local variables to keep the 325 | * naming system working when the same scheme is used multiple times in 326 | * separate decorators and same name for different schemes. 327 | * 328 | * This function should be called after the GraphQL scheme is created. 329 | * 330 | * @export 331 | */ 332 | export function free() { 333 | USED_NAMES = undefined 334 | GENERATED_TYPES = undefined 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/decorators/make-decorator-from-factory.ts: -------------------------------------------------------------------------------- 1 | import type { AnyZodObject } from 'zod' 2 | import type { 3 | DynamicZodModelClass, 4 | GraphQLCDF, 5 | GraphQLMDF, 6 | NameInputMethodDecoratorFactory, 7 | TypeOptionInputMethodDecoratorFactory, 8 | } from './types' 9 | import type { 10 | SupportedOptionTypes, 11 | WrapWithZodOptions, 12 | } from './zod-options-wrapper.interface' 13 | 14 | /** 15 | * Builds a decorator from a decorator factory function. 16 | * 17 | * @export 18 | * @template T The type of the `zod` validation object. 19 | * @template O The type of the supported option type. 20 | * @param {(WrapWithZodOptions | string | undefined)} nameOrOptions The 21 | * name or the options. 22 | * 23 | * @param {(GraphQLMDF | GraphQLCDF)} decoratorFactory The decorator 24 | * factory. 25 | * 26 | * @param {DynamicZodModelClass} model The class that is dynamically built. 27 | * 28 | * @return {MethodDecorator | ClassDecorator | ParameterDecorator} A decorator. 29 | */ 30 | export function makeDecoratorFromFactory< 31 | T extends AnyZodObject, 32 | O extends SupportedOptionTypes 33 | >( 34 | nameOrOptions: WrapWithZodOptions | string | undefined, 35 | decoratorFactory: GraphQLMDF | GraphQLCDF, 36 | model: DynamicZodModelClass, 37 | ) { 38 | let decorator: MethodDecorator | ClassDecorator | ParameterDecorator 39 | if (typeof nameOrOptions === 'string') { 40 | const factory = decoratorFactory as NameInputMethodDecoratorFactory 41 | decorator = factory(nameOrOptions) 42 | } 43 | else if (typeof nameOrOptions === 'object') { 44 | const { zod, ...rest } = nameOrOptions 45 | const factory = decoratorFactory as TypeOptionInputMethodDecoratorFactory 46 | decorator = factory(() => model, rest as O) 47 | } 48 | else { 49 | const factory = decoratorFactory as TypeOptionInputMethodDecoratorFactory 50 | decorator = factory(() => model) 51 | } 52 | 53 | return decorator 54 | } 55 | -------------------------------------------------------------------------------- /src/decorators/query-types/index.ts: -------------------------------------------------------------------------------- 1 | export { MutationWithZod } from './mutation-with-zod' 2 | export { QueryWithZod } from './query-with-zod' 3 | export { SubscriptionWithZod } from './subscription-with-zod' 4 | -------------------------------------------------------------------------------- /src/decorators/query-types/mutation-with-zod.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, MutationOptions as MO } from '@nestjs/graphql' 2 | 3 | import { MethodWithZod } from '../common' 4 | 5 | import type { AnyZodObject } from 'zod' 6 | import type { IModelFromZodOptions } from '../../model-from-zod' 7 | 8 | export interface MutationOptions extends MO { 9 | /** 10 | * Options for model creation from `zod`. 11 | * 12 | * @type {IModelFromZodOptions} 13 | * @memberof QueryOptions 14 | */ 15 | zod?: IModelFromZodOptions 16 | } 17 | 18 | /** 19 | * Mutation handler (method) Decorator. 20 | * Routes specified mutation to this method. 21 | * 22 | * Uses a `zod` object. 23 | * 24 | * @export 25 | * @template T The type of the zod object input. 26 | * @param {T} input The zod input object. 27 | * @return {MethodDecorator} A {@link MethodDecorator}. 28 | */ 29 | export function MutationWithZod(input: T): MethodDecorator 30 | 31 | /** 32 | * Mutation handler (method) Decorator. 33 | * Routes specified mutation to this method. 34 | * 35 | * Uses a `zod` object. 36 | * 37 | * @export 38 | * @template T The type of the zod object input. 39 | * @param {T} input The zod input object. 40 | * @param {string} name The name of the method. 41 | * @return {MethodDecorator} A {@link MethodDecorator}. 42 | */ 43 | export function MutationWithZod(input: T, name: string): MethodDecorator 44 | 45 | /** 46 | * Mutation handler (method) Decorator. 47 | * Routes specified mutation to this method. 48 | * 49 | * Uses a `zod` object. 50 | * 51 | * @export 52 | * @template T The type of the zod object input. 53 | * @param {T} input The zod input object. 54 | * @param {MutationOptions} options The options for query method. 55 | * @return {MethodDecorator} A {@link MethodDecorator}. 56 | */ 57 | export function MutationWithZod(input: T, options: MutationOptions): MethodDecorator 58 | 59 | export function MutationWithZod(input: T, nameOrOptions?: string | MutationOptions) { 60 | return MethodWithZod(input, nameOrOptions, Mutation) 61 | } 62 | -------------------------------------------------------------------------------- /src/decorators/query-types/query-with-zod.ts: -------------------------------------------------------------------------------- 1 | import { Query, QueryOptions as QO } from '@nestjs/graphql' 2 | 3 | import { MethodWithZod } from '../common' 4 | 5 | import type { AnyZodObject } from 'zod' 6 | import type { IModelFromZodOptions } from '../../model-from-zod' 7 | 8 | export interface QueryOptions extends QO { 9 | /** 10 | * Options for model creation from `zod`. 11 | * 12 | * @type {IModelFromZodOptions} 13 | * @memberof QueryOptions 14 | */ 15 | zod?: IModelFromZodOptions 16 | } 17 | 18 | /** 19 | * Query handler (method) Decorator. 20 | * Routes specified query to this method. 21 | * 22 | * Uses a `zod` object. 23 | * 24 | * @export 25 | * @template T The type of the zod object input. 26 | * @param {T} input The zod input object. 27 | * @return {MethodDecorator} A {@link MethodDecorator}. 28 | */ 29 | export function QueryWithZod(input: T): MethodDecorator 30 | 31 | /** 32 | * Query handler (method) Decorator. 33 | * Routes specified query to this method. 34 | * 35 | * Uses a `zod` object. 36 | * 37 | * @export 38 | * @template T The type of the zod object input. 39 | * @param {T} input The zod input object. 40 | * @param {string} name The name of the method. 41 | * @return {MethodDecorator} A {@link MethodDecorator}. 42 | */ 43 | export function QueryWithZod(input: T, name: string): MethodDecorator 44 | 45 | /** 46 | * Query handler (method) Decorator. 47 | * Routes specified query to this method. 48 | * 49 | * Uses a `zod` object. 50 | * 51 | * @export 52 | * @template T The type of the zod object input. 53 | * @param {T} input The zod input object. 54 | * @param {QueryOptions} options The options for query. 55 | * @return {MethodDecorator} A {@link MethodDecorator}. 56 | */ 57 | export function QueryWithZod(input: T, options: QueryOptions): MethodDecorator 58 | 59 | export function QueryWithZod(input: T, nameOrOptions?: string | QueryOptions) { 60 | return MethodWithZod(input, nameOrOptions, Query) 61 | } 62 | -------------------------------------------------------------------------------- /src/decorators/query-types/subscription-with-zod.ts: -------------------------------------------------------------------------------- 1 | import { plainToInstance } from 'class-transformer' 2 | 3 | import { BadRequestException } from '@nestjs/common' 4 | import { Subscription, SubscriptionOptions as SO } from '@nestjs/graphql' 5 | 6 | import { IModelFromZodOptions, modelFromZod } from '../../model-from-zod' 7 | 8 | import type { AnyZodObject, ZodError } from 'zod' 9 | 10 | export interface SubscriptionOptions extends SO { 11 | /** 12 | * Options for model creation from `zod`. 13 | * 14 | * @type {IModelFromZodOptions} 15 | * @memberof QueryOptions 16 | */ 17 | zod?: IModelFromZodOptions 18 | } 19 | 20 | /** 21 | * Subscription handler (method) Decorator. 22 | * Routes subscriptions to this method. 23 | * 24 | * Uses a `zod` object. 25 | * 26 | * @export 27 | * @template T The type of the zod object input. 28 | * @param {T} input The zod input object. 29 | * @return {MethodDecorator} A {@link MethodDecorator}. 30 | */ 31 | export function SubscriptionWithZod(input: T): MethodDecorator 32 | 33 | /** 34 | * Subscription handler (method) Decorator. 35 | * Routes subscriptions to this method. 36 | * 37 | * Uses a `zod` object. 38 | * 39 | * @export 40 | * @template T The type of the zod object input. 41 | * @param {T} input The zod input object. 42 | * @param {string} name The name of the method. 43 | * @return {MethodDecorator} A {@link MethodDecorator}. 44 | */ 45 | export function SubscriptionWithZod(input: T, name: string): MethodDecorator 46 | 47 | /** 48 | * Subscription handler (method) Decorator. 49 | * Routes subscriptions to this method. 50 | * 51 | * Uses a `zod` object. 52 | * 53 | * @export 54 | * @template T The type of the zod object input. 55 | * @param {T} input The zod input object. 56 | * @param {SubscriptionOptions>} options The options for 57 | * subscription method. 58 | * 59 | * @return {MethodDecorator} A {@link MethodDecorator}. 60 | */ 61 | export function SubscriptionWithZod(input: T, options: SubscriptionOptions): MethodDecorator 62 | 63 | /** 64 | * Subscription handler (method) Decorator. 65 | * Routes subscriptions to this method. 66 | * 67 | * Uses a `zod` object. 68 | * 69 | * @export 70 | * @template T The type of the zod object input. 71 | * @param {T} input The zod input object. 72 | * @param {string} name The name of the method. 73 | * @param {SubscriptionOptions>} options The options for 74 | * subscription method. 75 | * 76 | * @return {MethodDecorator} A {@link MethodDecorator}. 77 | */ 78 | export function SubscriptionWithZod( 79 | input: T, 80 | name: string, 81 | options: Pick, 'filter' | 'resolve' | 'zod'> 82 | ): MethodDecorator 83 | 84 | export function SubscriptionWithZod( 85 | input: T, 86 | nameOrOptions?: string | SubscriptionOptions, 87 | pickedOptions?: Pick, 'filter' | 'resolve' | 'zod'> 88 | ) { 89 | let zodOptions: IModelFromZodOptions | undefined 90 | 91 | if (typeof nameOrOptions === 'object') { 92 | zodOptions = nameOrOptions.zod 93 | } 94 | else if (typeof pickedOptions === 'object') { 95 | zodOptions = pickedOptions.zod 96 | } 97 | 98 | const model = modelFromZod(input, zodOptions) 99 | 100 | return function _SubscriptionWithZod( 101 | target: any, 102 | methodName: string, 103 | descriptor: PropertyDescriptor 104 | ) { 105 | let newDescriptor = descriptor || {} 106 | 107 | const originalFunction = descriptor?.value ?? target[ methodName ] 108 | 109 | newDescriptor.value = function _subscriptionWithZod(...args: any[]) { 110 | const result = originalFunction.apply(this, args) 111 | if (result instanceof Promise) { 112 | return result 113 | .then(output => input.parseAsync(output)) 114 | .then(output => plainToInstance(model, output)) 115 | .catch((error: ZodError) => { 116 | const messages = error.issues.reduce((prev, curr) => { 117 | prev[ curr.path.join('.') ] = curr.message 118 | return prev 119 | }, {} as any) 120 | 121 | return new BadRequestException(messages) 122 | }) 123 | } 124 | else { 125 | const parseResult = input.safeParse(result) 126 | if (parseResult.success) { 127 | return plainToInstance(model, parseResult.data) 128 | } 129 | else { 130 | const messages = parseResult.error.issues.reduce((prev, curr) => { 131 | prev[ curr.path.join('.') ] = curr.message 132 | return prev 133 | }, {} as any) 134 | 135 | return new BadRequestException(messages) 136 | } 137 | } 138 | } 139 | 140 | if (!descriptor) { 141 | Object.defineProperty(target, methodName, newDescriptor) 142 | } 143 | 144 | let decorate: MethodDecorator 145 | 146 | if (typeof nameOrOptions === 'string') { 147 | if (typeof pickedOptions === 'object') { 148 | decorate = Subscription(nameOrOptions, pickedOptions) 149 | } 150 | else { 151 | decorate = Subscription(nameOrOptions) 152 | } 153 | } 154 | else if (typeof nameOrOptions === 'object') { 155 | const { zod, ...rest } = nameOrOptions 156 | decorate = Subscription(() => model, rest) 157 | } 158 | else { 159 | decorate = Subscription(() => model) 160 | } 161 | 162 | decorate(target, methodName, descriptor) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/decorators/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReturnTypeFunc } from '@nestjs/graphql' 2 | import type { SupportedOptionTypes } from './zod-options-wrapper.interface' 3 | import type { AnyZodObject } from 'zod' 4 | import type { Type } from '@nestjs/common' 5 | 6 | /** 7 | * Describes a dynamically built class out of the given zod validation object. 8 | */ 9 | export type DynamicZodModelClass = Type 10 | 11 | type Decorators = MethodDecorator | ClassDecorator | ParameterDecorator 12 | 13 | /** 14 | * Describes a factory with no input. 15 | */ 16 | export type NoInputDecoratorFactory = (() => T) 17 | 18 | /** 19 | * Describes a factory that takes a string input. 20 | */ 21 | export type NameInputDecoratorFactory = ((name: string) => T) 22 | 23 | /** 24 | * Describes a factory that takes an option. 25 | */ 26 | export type OptionInputDecoratorFactory< 27 | O extends SupportedOptionTypes, 28 | T extends Decorators 29 | > = ((options?: O) => T) 30 | 31 | /** 32 | * Describes a factory that takes a type function and an optional option. 33 | */ 34 | export type TypeOptionInputDecoratorFactory< 35 | O extends SupportedOptionTypes, 36 | T extends Decorators 37 | > = ((typeFunc: ReturnTypeFunc, options?: O) => T) 38 | 39 | type DefaultDecoratorFactory< 40 | O extends SupportedOptionTypes, 41 | T extends Decorators 42 | > = NoInputDecoratorFactory 43 | | NameInputDecoratorFactory 44 | | OptionInputDecoratorFactory 45 | 46 | /** 47 | * Describes a factory with no input. 48 | */ 49 | export type NoInputMethodDecoratorFactory 50 | = NoInputDecoratorFactory 51 | 52 | /** 53 | * Describes a factory that takes a string input. 54 | */ 55 | export type NameInputMethodDecoratorFactory 56 | = NameInputDecoratorFactory 57 | 58 | /** 59 | * Describes a factory that takes an option. 60 | */ 61 | export type OptionInputMethodDecoratorFactory 62 | = OptionInputDecoratorFactory 63 | 64 | /** 65 | * Describes a factory that takes an option. 66 | */ 67 | export type TypeOptionInputMethodDecoratorFactory< 68 | O extends SupportedOptionTypes 69 | > = TypeOptionInputDecoratorFactory 70 | 71 | /** 72 | * Describes a method decorator function of GraphQL. 73 | */ 74 | export type GraphQLMDF 75 | = DefaultDecoratorFactory 76 | 77 | /** 78 | * Describes a method decorator with type function support of GraphQL. 79 | */ 80 | export type GraphQLMDFWithType 81 | = GraphQLMDF 82 | | TypeOptionInputMethodDecoratorFactory 83 | 84 | /** 85 | * Describes a class decorator function of GraphQL. 86 | */ 87 | export type GraphQLCDF 88 | = DefaultDecoratorFactory 89 | -------------------------------------------------------------------------------- /src/decorators/zod-options-wrapper.interface.ts: -------------------------------------------------------------------------------- 1 | import type { IModelFromZodOptions } from '../model-from-zod' 2 | import type { 3 | ArgsOptions, 4 | BaseTypeOptions, 5 | InputTypeOptions, 6 | } from '@nestjs/graphql' 7 | import type { ZodType } from 'zod' 8 | 9 | /** 10 | * Defines the supported option types by the library. 11 | */ 12 | export type SupportedOptionTypes 13 | = BaseTypeOptions 14 | | InputTypeOptions 15 | | ArgsOptions 16 | 17 | /** 18 | * Wraps given type with given zod options. 19 | * 20 | * This type extends the option types for NestJS and adds common properties 21 | * which will be used by this library. 22 | */ 23 | export type WrapWithZodOptions = O & BaseOptions 24 | 25 | /** 26 | * The base options for providing `zod` property containing options. 27 | */ 28 | export interface BaseOptions { 29 | /** 30 | * Options for model creation from `zod`. 31 | * 32 | * @type {IModelFromZodOptions} 33 | * @memberof QueryOptions 34 | */ 35 | zod?: IModelFromZodOptions 36 | } 37 | -------------------------------------------------------------------------------- /src/helpers/build-enum-type.ts: -------------------------------------------------------------------------------- 1 | import { AnyZodObject, infer as Infer, ZodEnum, ZodNativeEnum } from 'zod' 2 | 3 | import { registerEnumType } from '@nestjs/graphql' 4 | 5 | import { getDefaultEnumProvider } from '../decorators/common' 6 | import { getRegisterCount } from './constants' 7 | import { isZodInstance } from './is-zod-instance' 8 | import { toTitleCase } from './to-title-case' 9 | import { withSuffix } from './with-suffix' 10 | 11 | import type { ZodTypeInfo } from './get-field-info-from-zod' 12 | import type { IModelFromZodOptions } from '../model-from-zod' 13 | /** 14 | * Builds an enum type for GraphQL schema. 15 | * 16 | * @export 17 | * @template T The type of the zod object. 18 | * @param {keyof zod.infer} key The key of the zod object. 19 | * @param {ZodTypeInfo} typeInfo The parsed zod type info. 20 | * @param {IModelFromZodOptions>} options The options for building 21 | * enum type. 22 | * 23 | * @return {object} The enum object. 24 | */ 25 | export function buildEnumType( 26 | key: keyof Infer, 27 | typeInfo: ZodTypeInfo, 28 | options: IModelFromZodOptions 29 | ): object { 30 | 31 | const { type } = typeInfo 32 | 33 | const isNative = isZodInstance(ZodNativeEnum, type) 34 | 35 | if (isZodInstance(ZodEnum, type) || isNative) { 36 | const Enum = isNative ? type.enum : type.Enum 37 | 38 | let enumProvider = options.getEnumType ?? getDefaultEnumProvider() 39 | 40 | if (typeof enumProvider === 'function') { 41 | const replacement = enumProvider(Enum, { 42 | isNative, 43 | name: String(key), 44 | parentName: options.name, 45 | description: type.description, 46 | }) 47 | 48 | if (typeof replacement === 'object' && Enum !== replacement) { 49 | return typeInfo.type = replacement 50 | } 51 | } 52 | 53 | const incompatibleKey = getFirstIncompatibleEnumKey(Enum) 54 | if (incompatibleKey) { 55 | throw new Error(`The value of the Key("${incompatibleKey}") of ${options.name}.${String(key)} Enum was not valid`) 56 | } 57 | 58 | const parentName = options.name 59 | const enumName = withSuffix('Enum')(toTitleCase(key as string)) 60 | const registerCount = getRegisterCount() 61 | 62 | registerEnumType(Enum, { 63 | name: toTitleCase(`${parentName}_${enumName}_${registerCount}`), 64 | description: type.description ?? `Enum values for ${options.name}.${String(key)}`, 65 | }) 66 | 67 | return typeInfo.type = Enum 68 | } 69 | else if (Array.isArray(type)) { 70 | const dynamicEnumClass = buildEnumType(key, { 71 | type: type[ 0 ], 72 | isNullable: !!typeInfo.isItemNullable, 73 | isOptional: !!typeInfo.isItemOptional, 74 | }, options) 75 | 76 | return typeInfo.type = [ dynamicEnumClass ] 77 | } 78 | else { 79 | throw new Error(`Unexpected enum type for Key("${String(key)}")`) 80 | } 81 | } 82 | 83 | function getFirstIncompatibleEnumKey(input: Record) { 84 | const digitTest = /^\s*?\d/ 85 | 86 | for (const key in input) { 87 | const value = input[ key ] 88 | if (typeof value !== 'string') return key 89 | if (digitTest.test(value)) return key 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | // A global variable tracking the registered classes count. 2 | // This is used for providing unique class names. 3 | let registerCount = 0 4 | 5 | /** 6 | * A {@link Symbol} that is used for keeping the source zod object inside 7 | * a model class built from that source. 8 | */ 9 | export const ZodObjectKey = Symbol('[[DynamicZodModelSource]]') 10 | 11 | /** 12 | * Gets the total registered class count. 13 | * 14 | * @export 15 | * @return {number} The registered class count. 16 | */ 17 | export function getRegisterCount(): number { 18 | return registerCount 19 | } 20 | 21 | /** 22 | * Gets the total registered class count after increases the global counter. 23 | * 24 | * @export 25 | * @return {number} The new registered class count. 26 | */ 27 | export function getAndIncreaseRegisterCount(): number { 28 | return ++registerCount 29 | } 30 | -------------------------------------------------------------------------------- /src/helpers/create-zod-property-descriptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | infer as Infer, 3 | ParseParams, 4 | ZodDefault, 5 | ZodType, 6 | ZodTypeAny, 7 | } from 'zod' 8 | 9 | import { isZodInstance } from './is-zod-instance' 10 | 11 | import type { IModelFromZodOptions } from '../model-from-zod' 12 | 13 | /** 14 | * Creates a property descriptor that provides `get` and `set` functions 15 | * that are using `parse` or `safeParse` methods of the `zod` library. 16 | * 17 | * @export 18 | * @template T The type of the target object. 19 | * @param {keyof T} key The key of the property that is being created. 20 | * @param {zod.ZodTypeAny} input The zod object input. 21 | * @param {IModelFromZodOptions} opts The options. 22 | * @return {PropertyDescriptor} A {@link PropertyDescriptor}. 23 | */ 24 | export function createZodPropertyDescriptor( 25 | key: keyof Infer, 26 | input: ZodTypeAny, 27 | opts: IModelFromZodOptions 28 | ): PropertyDescriptor { 29 | let localVariable: any 30 | 31 | if (isZodInstance(ZodDefault, input)) { 32 | localVariable = input._def.defaultValue() 33 | } 34 | 35 | const { 36 | safe, 37 | doNotThrow, 38 | 39 | onParsing, 40 | onParseError, 41 | } = opts 42 | 43 | let keyProps: Partial | undefined 44 | if (typeof onParsing === 'function') { 45 | keyProps = onParsing(key, localVariable) 46 | } 47 | 48 | return { 49 | get() { 50 | return localVariable 51 | }, 52 | set(newValue) { 53 | if (safe) { 54 | const result = input.safeParse(newValue, keyProps) 55 | if (result.success) { 56 | localVariable = result.data 57 | } 58 | else { 59 | let replaceValue: typeof localVariable 60 | 61 | if (typeof onParseError === 'function') { 62 | replaceValue = onParseError( 63 | key, 64 | newValue, 65 | localVariable, 66 | result.error 67 | ) 68 | } 69 | 70 | if (typeof replaceValue !== 'undefined') { 71 | localVariable = replaceValue 72 | } 73 | else if (doNotThrow) { 74 | localVariable = undefined 75 | } 76 | else { 77 | throw result.error 78 | } 79 | } 80 | } 81 | else { 82 | if (doNotThrow) { 83 | try { 84 | const result = input.parse(newValue, keyProps) 85 | localVariable = result 86 | } 87 | catch (_) { 88 | localVariable = undefined 89 | } 90 | } 91 | else { 92 | const result = input.parse(newValue, keyProps) 93 | localVariable = result 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/helpers/extract-name-and-description.ts: -------------------------------------------------------------------------------- 1 | import { getAndIncreaseRegisterCount } from './constants' 2 | import { getDescription } from './get-description' 3 | 4 | import type { ZodType } from 'zod' 5 | import type { IModelFromZodOptions } from '../model-from-zod' 6 | 7 | /** 8 | * Extracts the name and description from a zod object input. 9 | * 10 | * @export 11 | * @template T The zod object input type. 12 | * @param {T} zodInput The zod object input. 13 | * @param {IModelFromZodOptions} options The options for the operation. 14 | * @return {{ name: string, description: string }} An object containing 15 | * normalized name and description info. 16 | */ 17 | export function extractNameAndDescription( 18 | zodInput: T, 19 | options: IModelFromZodOptions 20 | ) { 21 | let { name } = options 22 | let description = getDescription(zodInput) 23 | 24 | if (!name) { 25 | if (description) { 26 | 27 | const match = description.match(/(\w+):\s*?(.*)+/) 28 | if (match) { 29 | const [ _full, className, actualDescription ] = match 30 | 31 | name = className 32 | description = actualDescription.trim() 33 | 34 | return { name, description } 35 | } 36 | } 37 | 38 | name = `ClassFromZod_${getAndIncreaseRegisterCount()}` 39 | } 40 | 41 | return { name, description } 42 | } 43 | -------------------------------------------------------------------------------- /src/helpers/generate-defaults.ts: -------------------------------------------------------------------------------- 1 | import { AnyZodObject, ZodDefault, ZodObject, ZodTypeAny } from 'zod' 2 | 3 | import { isZodInstance } from './is-zod-instance' 4 | 5 | /** 6 | * Generates the default values for given object. 7 | * 8 | * This function is recursive with {@link generateDefaults}. 9 | * 10 | * @param {AnyZodObject} input The input. 11 | * @return {Record} A record of default values. 12 | */ 13 | function generateDefaultsForObject(input: AnyZodObject) { 14 | return Object.keys(input.shape).reduce((curr, key) => { 15 | const res = generateDefaults(input.shape[ key ]) 16 | if (res) { 17 | curr[ key ] = res 18 | } 19 | 20 | return curr 21 | }, {} as Record) 22 | } 23 | 24 | /** 25 | * Generates the default vales for given input. 26 | * 27 | * @export 28 | * @template T The type of the input. 29 | * @param {T} input The input. 30 | * @return {*} A record containing keys and the zod 31 | * values with defaults. 32 | */ 33 | export function generateDefaults(input: T) { 34 | if (isZodInstance(ZodObject, input)) { 35 | return generateDefaultsForObject(input as AnyZodObject) 36 | } 37 | else if (isZodInstance(ZodDefault, input)) { 38 | return input._def.defaultValue?.() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/helpers/get-description.ts: -------------------------------------------------------------------------------- 1 | import { ZodType } from 'zod' 2 | 3 | import { iterateZodLayers } from './unwrap' 4 | 5 | /** 6 | * Extracts the description from a given type. 7 | * 8 | * The given input may also be wrapped more than one time with the ones listed 9 | * above. Therefore, assuming there is a value which 10 | * is `Nullable>` and there is no description associated to the 11 | * nullable and the array wrappers, the description will still tried to be 12 | * extracted from the number instance. 13 | * 14 | * @template T The type of the zod object. 15 | * @param {T} [input] The zod object input. 16 | * @return {(string | undefined)} The description of the input or `undefined.` 17 | */ 18 | export function getDescription(input?: T): string | undefined { 19 | if (!input) return 20 | 21 | if (input.description) return input.description 22 | 23 | for (const layer of iterateZodLayers(input)) { 24 | if (layer.description) return layer.description 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/helpers/get-field-info-from-zod.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql/type/definition' 2 | import { 3 | ZodArray, 4 | ZodBoolean, 5 | ZodDefault, 6 | ZodEnum, 7 | ZodNativeEnum, 8 | ZodNullable, 9 | ZodNumber, 10 | ZodObject, 11 | ZodOptional, 12 | ZodString, 13 | ZodTransformer, 14 | ZodType, 15 | ZodTypeAny, 16 | } from 'zod' 17 | 18 | import { Int } from '@nestjs/graphql' 19 | 20 | import { getDefaultTypeProvider } from '../decorators/common' 21 | import { 22 | IModelFromZodOptions, 23 | modelFromZod, 24 | modelFromZodBase, 25 | } from '../model-from-zod' 26 | import { getZodObjectName } from './get-zod-object-name' 27 | import { isZodInstance } from './is-zod-instance' 28 | import { toTitleCase } from './to-title-case' 29 | 30 | import type { ZodNumberCheck } from 'zod' 31 | 32 | /** 33 | * Describes the properties of a zod type that can be used to apply to `Field` 34 | * decorator of NestJS. 35 | * 36 | * @export 37 | * @interface ZodTypeInfo 38 | */ 39 | export interface ZodTypeInfo { 40 | /** 41 | * The corresponing type of the `zod` property. 42 | * 43 | * This type will be used by the `Field` property of the NestJS decorators. 44 | * 45 | * @type {*} 46 | * @memberof ZodTypeInfo 47 | */ 48 | type: any 49 | 50 | /** 51 | * Indicates whether or not the prperty is optional. 52 | * 53 | * @type {boolean} 54 | * @memberof ZodTypeInfo 55 | */ 56 | isOptional: boolean 57 | 58 | /** 59 | * Indicates whether or not the property is nullable. 60 | * 61 | * @type {boolean} 62 | * @memberof ZodTypeInfo 63 | */ 64 | isNullable: boolean 65 | 66 | /** 67 | * Indicates whether or not the property is an enum type. 68 | * 69 | * @type {boolean} 70 | * @memberof ZodTypeInfo 71 | */ 72 | isEnum?: boolean 73 | 74 | /** 75 | * Indicates whether or not the property is an object (another type). 76 | * 77 | * @type {boolean} 78 | * @memberof ZodTypeInfo 79 | */ 80 | isType?: boolean 81 | 82 | /** 83 | * Indicates whether or not the property is an array. 84 | * 85 | * @type {boolean} 86 | * @memberof ZodTypeInfo 87 | */ 88 | isOfArray?: boolean 89 | 90 | /** 91 | * Indicates whether or not the item of the array of the property is 92 | * optional. 93 | * 94 | * @type {boolean} 95 | * @memberof ZodTypeInfo 96 | */ 97 | isItemOptional?: boolean 98 | 99 | /** 100 | * Indicates whether or not the item of the array of the property is 101 | * nullable. 102 | * 103 | * @type {boolean} 104 | * @memberof ZodTypeInfo 105 | */ 106 | isItemNullable?: boolean 107 | } 108 | 109 | /** 110 | * The options for {@link getFieldInfoFromZod} function. 111 | * 112 | * The options extends {@link IModelFromZodOptions} because it may create 113 | * instances if the given zod type was an object, therefore a class would 114 | * be created. 115 | * 116 | * @template T The zod type. 117 | */ 118 | type Options = IModelFromZodOptions & { 119 | /** 120 | * Provides the decorator to decorate the dynamically generated class. 121 | * 122 | * @param {T} zodInput The zod input. 123 | * @param {string} key The name of the currently processsed property. 124 | * @return {ClassDecorator} The class decorator to decorate the class. 125 | * @memberof IOptions 126 | */ 127 | getDecorator?(zodInput: T, key: string): ClassDecorator 128 | } 129 | 130 | /** 131 | * Converts a given `zod` object input for a key, into {@link ZodTypeInfo}. 132 | * 133 | * @export 134 | * @template T The type of the `zod` object input. 135 | * @param {string} key The key of the property of the `zod` object input, 136 | * that is being converted. 137 | * 138 | * @param {ZodTypeAny} prop The `zod` object property. 139 | * @param {Options} options The options for conversion. 140 | * @return {ZodTypeInfo} The {@link ZodTypeInfo} of the property. 141 | */ 142 | export function getFieldInfoFromZod( 143 | key: string, 144 | prop: ZodTypeAny, 145 | options: Options 146 | ): ZodTypeInfo { 147 | 148 | if (isZodInstance(ZodArray, prop)) { 149 | const data = getFieldInfoFromZod(key, prop.element, options) 150 | 151 | const { 152 | type, 153 | isEnum, 154 | isNullable: isItemNullable, 155 | isOptional: isItemOptional, 156 | } = data 157 | 158 | return { 159 | type: [ type ], 160 | isOptional: prop.isOptional(), 161 | isNullable: prop.isNullable(), 162 | isEnum, 163 | isOfArray: true, 164 | isItemNullable, 165 | isItemOptional, 166 | } 167 | } 168 | else if (isZodInstance(ZodBoolean, prop)) { 169 | return { 170 | type: Boolean, 171 | isOptional: prop.isOptional(), 172 | isNullable: prop.isNullable(), 173 | } 174 | } 175 | if (isZodInstance(ZodString, prop)) { 176 | return { 177 | type: String, 178 | isOptional: prop.isOptional(), 179 | isNullable: prop.isNullable(), 180 | } 181 | } 182 | else if (isZodInstance(ZodNumber, prop)) { 183 | const isInt = Boolean(prop._def.checks.find((check: ZodNumberCheck) => check.kind === 'int')) 184 | 185 | return { 186 | type: isInt ? Int : Number, 187 | isOptional: prop.isOptional(), 188 | isNullable: prop.isNullable(), 189 | } 190 | } 191 | else if (isZodInstance(ZodOptional, prop)) { 192 | const { 193 | type, 194 | isEnum, 195 | isOfArray, 196 | isItemNullable, 197 | isItemOptional, 198 | } = getFieldInfoFromZod(key, prop.unwrap(), options) 199 | 200 | return { 201 | type, 202 | isEnum, 203 | isOfArray, 204 | isItemNullable, 205 | isItemOptional, 206 | isOptional: true, 207 | isNullable: prop.isNullable(), 208 | } 209 | } 210 | else if (isZodInstance(ZodObject, prop)) { 211 | const isNullable = prop.isNullable() || prop.isOptional() 212 | const { 213 | provideNameForNestedClass = defaultNestedClassNameProvider, 214 | } = options 215 | 216 | let name = provideNameForNestedClass(options.name || '', key) 217 | if (typeof name !== 'string') { 218 | name = defaultNestedClassNameProvider(options.name || '', key) 219 | } 220 | 221 | name = name.trim() 222 | if (!name) { 223 | name = defaultNestedClassNameProvider(options.name || '', key) 224 | } 225 | 226 | const nestedOptions = { 227 | ...options, 228 | name, 229 | description: prop.description, 230 | isAbstract: isNullable, 231 | } 232 | 233 | let model: any 234 | if (typeof options.getDecorator === 'function') { 235 | model = modelFromZodBase( 236 | prop as any, 237 | nestedOptions, 238 | options.getDecorator(prop as any as T, nestedOptions.name) 239 | ) 240 | } 241 | else { 242 | model = modelFromZod(prop as any, nestedOptions) 243 | } 244 | 245 | return { 246 | type: model, 247 | isType: true, 248 | isNullable: prop.isNullable(), 249 | isOptional: prop.isOptional(), 250 | } 251 | } 252 | else if (isZodInstance(ZodEnum, prop) || isZodInstance(ZodNativeEnum, prop)) { 253 | return { 254 | type: prop, 255 | isNullable: prop.isNullable(), 256 | isOptional: prop.isOptional(), 257 | isEnum: true, 258 | } 259 | } 260 | else if (isZodInstance(ZodDefault, prop)) { 261 | return getFieldInfoFromZod(key, prop._def.innerType, options) 262 | } 263 | else if (isZodInstance(ZodTransformer, prop)) { 264 | return getFieldInfoFromZod(key, prop.innerType(), options) 265 | } 266 | else if (isZodInstance(ZodNullable, prop)) { 267 | return getFieldInfoFromZod(key, prop._def.innerType, options) 268 | } 269 | else { 270 | const { getScalarTypeFor = getDefaultTypeProvider() } = options 271 | const typeName = getZodObjectName(prop) 272 | 273 | if (typeof getScalarTypeFor === 'function') { 274 | const scalarType = getScalarTypeFor(typeName) 275 | let isScalarType = scalarType instanceof GraphQLScalarType 276 | 277 | if (!isScalarType && scalarType) { 278 | let constructor: Function = (scalarType as any)[ 'constructor' ] 279 | if (typeof constructor === 'function' && constructor.name === GraphQLScalarType.name) { 280 | isScalarType = true 281 | } 282 | } 283 | 284 | if (isScalarType) { 285 | return { 286 | isType: true, 287 | type: scalarType, 288 | isNullable: prop.isNullable(), 289 | isOptional: prop.isOptional(), 290 | } 291 | } 292 | else { 293 | throw new Error(`The Scalar(Value="${scalarType}", Type="${typeof scalarType}") as Key("${key}") of Type("${typeName}") was not an instance of GraphQLScalarType.`) 294 | } 295 | } 296 | 297 | throw new Error(`Unsupported type info of Key("${key}") of Type("${typeName}")`) 298 | } 299 | } 300 | 301 | export module getFieldInfoFromZod { 302 | /** 303 | * The types that are parseable by the {@link getFieldInfoFromZod} function. 304 | */ 305 | export const PARSED_TYPES = [ 306 | ZodArray, 307 | ZodBoolean, 308 | ZodDefault, 309 | ZodEnum, 310 | ZodNativeEnum, 311 | ZodNullable, 312 | ZodNumber, 313 | ZodObject, 314 | ZodOptional, 315 | ZodString, 316 | ZodTransformer, 317 | ] as const 318 | 319 | /** 320 | * Determines if the given zod type is parseable by the {@link getFieldInfoFromZod} 321 | * function. 322 | * 323 | * @export 324 | * @param {ZodType} input The zod type input. 325 | * @return {boolean} `true` if the given input is parseable. 326 | */ 327 | export function canParse(input: ZodType): boolean { 328 | return PARSED_TYPES.some(it => isZodInstance(it, input)) 329 | } 330 | } 331 | 332 | /** 333 | * Provides a name for nested classes. 334 | * 335 | * @param {string} parentName The parent class name. 336 | * @param {string} propertyKey The property key. 337 | * @return {string} A new name for the new class. 338 | * 339 | * @__PURE__ 340 | */ 341 | function defaultNestedClassNameProvider(parentName: string, propertyKey: string): string { 342 | return `${parentName}_${toTitleCase(propertyKey)}` 343 | } 344 | -------------------------------------------------------------------------------- /src/helpers/get-zod-object-name.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ZodAny, 3 | ZodArray, 4 | ZodBigInt, 5 | ZodBoolean, 6 | ZodDate, 7 | ZodDefault, 8 | ZodEffects, 9 | ZodEnum, 10 | ZodLiteral, 11 | ZodNull, 12 | ZodNullable, 13 | ZodNumber, 14 | ZodObject, 15 | ZodOptional, 16 | ZodRecord, 17 | ZodString, 18 | ZodTransformer, 19 | ZodTypeAny, 20 | ZodUnion, 21 | } from 'zod' 22 | 23 | import { isZodInstance } from './is-zod-instance' 24 | import { toTitleCase } from './to-title-case' 25 | 26 | /** 27 | * Builds the corresponding zod type name. 28 | * 29 | * @export 30 | * @param {ZodTypeAny} instance The zod type instance. 31 | * @return {string} A built type name for the input. 32 | * 33 | * @__PURE__ 34 | */ 35 | export function getZodObjectName(instance: ZodTypeAny): string { 36 | if (isZodInstance(ZodArray, instance)) { 37 | const innerName = getZodObjectName(instance.element) 38 | return `Array<${innerName}>` 39 | } 40 | 41 | if (isZodInstance(ZodOptional, instance)) { 42 | const innerName = getZodObjectName(instance.unwrap()) 43 | return `Optional<${innerName}>` 44 | } 45 | 46 | if (isZodInstance(ZodTransformer, instance)) { 47 | return getZodObjectName(instance.innerType()) 48 | } 49 | 50 | if (isZodInstance(ZodDefault, instance)) { 51 | return getZodObjectName(instance._def.innerType) 52 | } 53 | 54 | if (isZodInstance(ZodEnum, instance)) { 55 | const { description = '', Enum } = instance 56 | const nameSeparatorIndex = description.indexOf(':') 57 | 58 | if (nameSeparatorIndex > 0) { 59 | const name = description.slice(0, nameSeparatorIndex) 60 | return `Enum<${name}>` 61 | } 62 | else { 63 | const values = Object.values(Enum) 64 | const name = values.join(',') 65 | return `Enum<${name}>` 66 | } 67 | } 68 | 69 | if (isZodInstance(ZodObject, instance)) { 70 | const { description = '' } = instance 71 | const nameSeparatorIndex = description.indexOf(':') 72 | 73 | if (nameSeparatorIndex > 0) { 74 | const name = description.slice(0, nameSeparatorIndex) 75 | return name 76 | } 77 | else { 78 | return `Object` 79 | } 80 | } 81 | 82 | if (isZodInstance(ZodRecord, instance)) { 83 | const { keySchema, valueSchema } = instance 84 | const keyName = getZodObjectName(keySchema) 85 | const valueName = getZodObjectName(valueSchema) 86 | return `Record<${keyName}, ${valueName}>` 87 | } 88 | 89 | if (isZodInstance(ZodEffects, instance)) { 90 | return getZodObjectName(instance.innerType()) 91 | } 92 | 93 | if (isZodInstance(ZodLiteral, instance)) { 94 | const { value } = instance 95 | if (typeof value === 'object') { 96 | if (value === null) return `Literal` 97 | let constructor: any 98 | if ('prototype' in value) { 99 | const prototype = value[ 'prototype' ] 100 | if (typeof prototype === 'object' && prototype && ('constructor' in prototype)) { 101 | constructor = prototype[ 'constructor' ] 102 | } 103 | } 104 | else if ('constructor' in value) { 105 | constructor = value[ 'constructor' ] 106 | } 107 | 108 | if (typeof constructor === 'function') { 109 | return `Literal<${constructor.name}>` 110 | } 111 | } 112 | 113 | return `Literal<${toTitleCase(typeof instance.value)}>` 114 | } 115 | 116 | if (isZodInstance(ZodUnion, instance)) { 117 | return instance.options.map(getZodObjectName).join(' | ') 118 | } 119 | 120 | if (isZodInstance(ZodNullable, instance)) { 121 | const innerName = getZodObjectName(instance._def.innerType) 122 | return `Nullable<${innerName}>` 123 | } 124 | 125 | if (isZodInstance(ZodBoolean, instance)) return 'Boolean' 126 | if (isZodInstance(ZodString, instance)) return 'String' 127 | if (isZodInstance(ZodNumber, instance)) return 'Number' 128 | if (isZodInstance(ZodBigInt, instance)) return 'BigInt' 129 | if (isZodInstance(ZodDate, instance)) return 'Date' 130 | if (isZodInstance(ZodAny, instance)) return 'Any' 131 | if (isZodInstance(ZodNull, instance)) return 'Null' 132 | return 'Unknown' 133 | } 134 | -------------------------------------------------------------------------------- /src/helpers/get-zod-object.ts: -------------------------------------------------------------------------------- 1 | import { ZodObjectKey } from './constants' 2 | 3 | /** 4 | * Extracts the source zod object that is used for creating passed dynamic 5 | * model class. 6 | * 7 | * @export 8 | * @template T The type of the model class that is dynamically generated from 9 | * a zod object. 10 | * 11 | * @param {T} input The model class instance that is dynamically generated 12 | * from a zod object. 13 | * 14 | * @return {*} The source zod object. 15 | */ 16 | export function getZodObject>( 17 | input: T 18 | ) { 19 | return input[ ZodObjectKey ] 20 | } 21 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build-enum-type' 2 | export * from './extract-name-and-description' 3 | export * from './parse-shape' 4 | export * from './get-zod-object' 5 | export * from './unwrap' 6 | export * from './get-zod-object-name' 7 | -------------------------------------------------------------------------------- /src/helpers/is-zod-instance.ts: -------------------------------------------------------------------------------- 1 | import type { ZodTypeAny } from 'zod' 2 | import type { Type } from '@nestjs/common' 3 | 4 | /** 5 | * Checks whether the given `input` is instance of given `klass`. 6 | * 7 | * @export 8 | * @template T The type of the input. 9 | * @param {T} klass The class type. 10 | * @param {Object} input The object input. 11 | * @return {input is InstanceType} A boolean value indicating if the 12 | * input is instance of given class. 13 | */ 14 | export function isZodInstance>( 15 | klass: T, 16 | input: Object 17 | ): input is InstanceType { 18 | return klass.name === input.constructor.name 19 | } 20 | -------------------------------------------------------------------------------- /src/helpers/parse-shape.ts: -------------------------------------------------------------------------------- 1 | import type { IModelFromZodOptions } from '../model-from-zod' 2 | 3 | import { ZodObject, ZodType } from 'zod' 4 | 5 | import { Field, NullableList } from '@nestjs/graphql' 6 | 7 | import { getDefaultTypeProvider } from '../decorators/common' 8 | import { buildEnumType } from './build-enum-type' 9 | import { createZodPropertyDescriptor } from './create-zod-property-descriptor' 10 | import { generateDefaults } from './generate-defaults' 11 | import { getDescription } from './get-description' 12 | import { getFieldInfoFromZod, ZodTypeInfo } from './get-field-info-from-zod' 13 | import { getZodObjectName } from './get-zod-object-name' 14 | import { isZodInstance } from './is-zod-instance' 15 | 16 | /** 17 | * An interface describing a parsed field. 18 | */ 19 | export interface ParsedField { 20 | /** 21 | * The key of the parsed property. 22 | * 23 | * @type {string} 24 | */ 25 | key: string 26 | 27 | /** 28 | * The type of the field of the parsed property. 29 | * 30 | * Can be used for GraphQL @{@link Field} decorator. 31 | * 32 | * @type {*} 33 | */ 34 | fieldType: any 35 | 36 | /** 37 | * The {@link PropertyDescriptor} of the parsed property. 38 | * 39 | * @type {PropertyDescriptor} 40 | */ 41 | descriptor: PropertyDescriptor 42 | 43 | /** 44 | * A {@link PropertyDecorator} for decorating fields. 45 | * 46 | * @type {PropertyDecorator} 47 | */ 48 | decorateFieldProperty: PropertyDecorator 49 | } 50 | 51 | type ParseOptions 52 | = IModelFromZodOptions 53 | & { 54 | /** 55 | * Provides the decorator to decorate the dynamically generated class. 56 | * 57 | * @param {T} zodInput The zod input. 58 | * @param {string} key The name of the currently processsed property. 59 | * @return {ClassDecorator} The class decorator to decorate the class. 60 | * @memberof IOptions 61 | */ 62 | getDecorator?(zodInput: T, key: string): ClassDecorator 63 | } 64 | 65 | /** 66 | * Parses a zod input object with given options. 67 | * 68 | * @export 69 | * @template T The type of the zod object. 70 | * @param {T} zodInput The zod object input. 71 | * @param {ParseOptions} [options={}] The options for the parsing. 72 | * @return {ParsedField[]} An array of {@link ParsedField}. 73 | */ 74 | export function parseShape( 75 | zodInput: T, 76 | options: ParseOptions = {} 77 | ): ParsedField[] { 78 | // Parsing an object shape 79 | if (isZodInstance(ZodObject, zodInput)) { 80 | return Object 81 | .entries(zodInput.shape) 82 | .map(([ key, value ]) => parseSingleShape(key, value as ZodType, options)) 83 | } 84 | 85 | // Parsing a primitive shape 86 | const parsedShape = parseSingleShape('', zodInput, options) 87 | return [ parsedShape ] 88 | } 89 | 90 | /** 91 | * Gets the nullability of a field from type info. 92 | * 93 | * @export 94 | * @param {ZodTypeInfo} typeInfo The type info. 95 | * @return {(boolean | NullableList)} The nullability state. 96 | */ 97 | export function getNullability(typeInfo: ZodTypeInfo): boolean | NullableList { 98 | const { 99 | isNullable, 100 | isOptional, 101 | isOfArray, 102 | isItemOptional, 103 | isItemNullable, 104 | } = typeInfo 105 | 106 | let nullable: boolean | NullableList = isNullable || isOptional 107 | 108 | if (isOfArray) { 109 | if (isItemNullable || isItemOptional) { 110 | if (nullable) { 111 | nullable = 'itemsAndList' 112 | } 113 | else { 114 | nullable = 'items' 115 | } 116 | } 117 | } 118 | 119 | return nullable 120 | } 121 | 122 | /** 123 | * Parses a field from given parameters. 124 | * 125 | * @template T The zod type that will be parsed. 126 | * @param {string} key The proprety key of the zod type. 127 | * @param {T} input The zod type input. 128 | * @param {ParseOptions} options The options for parsing. 129 | * @return {ParsedField} The parsed field output. 130 | */ 131 | function parseSingleShape(key: string, input: T, options: ParseOptions): ParsedField { 132 | const elementType = getFieldInfoFromZod(key, input, options) 133 | 134 | const { isEnum } = elementType 135 | 136 | if (isEnum) { 137 | buildEnumType(key, elementType, options) 138 | } 139 | 140 | const { type: fieldType } = elementType 141 | 142 | let defaultValue = elementType.isType ? undefined : generateDefaults(input) 143 | const nullable = getNullability(elementType) 144 | 145 | if (nullable === 'items') { 146 | defaultValue = undefined 147 | } 148 | 149 | const description = getDescription(input) 150 | const descriptor = buildPropertyDescriptor(key, input, options) 151 | 152 | return { 153 | key, 154 | fieldType, 155 | descriptor, 156 | decorateFieldProperty: Field(() => fieldType, { 157 | name: key, 158 | nullable, 159 | defaultValue, 160 | description, 161 | }) 162 | } 163 | } 164 | 165 | /** 166 | * Creates a property descriptor for given parameters. 167 | * 168 | * @param {string} key The key of the input in its object. 169 | * @param {ZodType} input The zod type input. 170 | * @param {ParseOptions} options The parse options. 171 | * @return {PropertyDescriptor} The property descriptor created for it, 172 | * if the operation was successful. 173 | * 174 | * @throws {Error} - The input was not processable and there was no 175 | * GraphQLScalar type provided for it. 176 | */ 177 | function buildPropertyDescriptor(key: string, input: ZodType, options: ParseOptions): PropertyDescriptor { 178 | if (getFieldInfoFromZod.canParse(input)) { 179 | return createZodPropertyDescriptor(key, input, options) 180 | } 181 | 182 | const { getScalarTypeFor = getDefaultTypeProvider() } = options 183 | const name = getZodObjectName(input) 184 | 185 | if (typeof getScalarTypeFor == 'function') { 186 | const scalarType = getScalarTypeFor(name) 187 | 188 | if (typeof scalarType === 'object') { 189 | return createZodPropertyDescriptor(key, input, options) 190 | } 191 | } 192 | 193 | let error = `"${key || name}" could not be processed, a corresponding GraphQL scalar type should be provided.` 194 | throw new Error(error) 195 | } 196 | -------------------------------------------------------------------------------- /src/helpers/to-title-case.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates another string where the initial letters of the words are 3 | * capitalized. 4 | * 5 | * @export 6 | * @param {string} value The input string. 7 | * @return {string} A string which is title-cased. 8 | */ 9 | export function toTitleCase(value: string) { 10 | return value.replace(/\b(\p{Alpha})(.*?)\b/u, (_string, match, rest) => { 11 | return match.toUpperCase() + rest 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/unwrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ZodArray, 3 | ZodCatch, 4 | ZodDefault, 5 | ZodEffects, 6 | ZodLazy, 7 | ZodNullable, 8 | ZodOptional, 9 | ZodPromise, 10 | ZodSet, 11 | ZodTransformer, 12 | ZodType, 13 | } from 'zod' 14 | 15 | import { Prev } from '../types/prev' 16 | import { isZodInstance } from './is-zod-instance' 17 | 18 | /** 19 | * Unwraps any given zod type by one level. 20 | * 21 | * The supported zod wrappers are: 22 | * - {@link ZodArray} 23 | * - {@link ZodCatch} 24 | * - {@link ZodDefault} 25 | * - {@link ZodEffects} 26 | * - {@link ZodLazy} 27 | * - {@link ZodNullable} 28 | * - {@link ZodOptional} 29 | * - {@link ZodPromise} 30 | * - {@link ZodSet} 31 | * - {@link ZodTransformer} 32 | * 33 | * @template T The zod type. 34 | */ 35 | export type UnwrapNestedZod 36 | = T extends ZodArray ? I : ( 37 | T extends ZodOptional ? I : ( 38 | T extends ZodTransformer ? I : ( 39 | T extends ZodDefault ? I : ( 40 | T extends ZodEffects ? I : ( 41 | T extends ZodNullable ? I : ( 42 | T extends ZodCatch ? I : ( 43 | T extends ZodPromise ? I : ( 44 | T extends ZodSet ? I : ( 45 | T extends ZodLazy ? I : T 46 | ) 47 | ) 48 | ) 49 | ) 50 | ) 51 | ) 52 | ) 53 | ) 54 | ) 55 | 56 | /** 57 | * Unwraps any given zod type recursively. 58 | * 59 | * The supported zod wrappers are: 60 | * - {@link ZodArray} 61 | * - {@link ZodCatch} 62 | * - {@link ZodDefault} 63 | * - {@link ZodEffects} 64 | * - {@link ZodLazy} 65 | * - {@link ZodNullable} 66 | * - {@link ZodOptional} 67 | * - {@link ZodPromise} 68 | * - {@link ZodSet} 69 | * - {@link ZodTransformer} 70 | * 71 | * @template T The zod type. 72 | * @template Depth The maximum depth to unwrap, default `5`. 73 | */ 74 | export type UnwrapNestedZodRecursive 75 | = [ Prev[ Depth ] ] extends [ never ] ? never : [ T ] extends [ UnwrapNestedZod ] ? T : ( 76 | UnwrapNestedZodRecursive, Prev[ Depth ]> 77 | ) 78 | 79 | /** 80 | * Unwraps the zod object one level. 81 | * 82 | * The supported zod wrappers are: 83 | * - {@link ZodArray} 84 | * - {@link ZodCatch} 85 | * - {@link ZodDefault} 86 | * - {@link ZodEffects} 87 | * - {@link ZodLazy} 88 | * - {@link ZodNullable} 89 | * - {@link ZodOptional} 90 | * - {@link ZodPromise} 91 | * - {@link ZodSet} 92 | * - {@link ZodTransformer} 93 | * 94 | * @export 95 | * @template T The type of the input. 96 | * @param {T} input The zod input. 97 | * @return {UnwrapNestedZod} The unwrapped zod instance. 98 | * 99 | * @__PURE__ 100 | */ 101 | export function unwrapNestedZod(input: T): UnwrapNestedZod { 102 | if (isZodInstance(ZodArray, input)) return input.element as UnwrapNestedZod 103 | if (isZodInstance(ZodCatch, input)) return input._def.innerType as UnwrapNestedZod 104 | if (isZodInstance(ZodDefault, input)) return input._def.innerType as UnwrapNestedZod 105 | if (isZodInstance(ZodEffects, input)) return input.innerType() as UnwrapNestedZod 106 | if (isZodInstance(ZodLazy, input)) return input.schema as UnwrapNestedZod 107 | if (isZodInstance(ZodNullable, input)) return input.unwrap() as UnwrapNestedZod 108 | if (isZodInstance(ZodOptional, input)) return input.unwrap() as UnwrapNestedZod 109 | if (isZodInstance(ZodPromise, input)) return input.unwrap() as UnwrapNestedZod 110 | if (isZodInstance(ZodSet, input)) return input._def.valueType as UnwrapNestedZod 111 | if (isZodInstance(ZodTransformer, input)) return input.innerType() as UnwrapNestedZod 112 | return input as UnwrapNestedZod 113 | } 114 | 115 | /** 116 | * Unwraps the zob object recursively. 117 | * 118 | * @export 119 | * @template T The type of the input. 120 | * @template Depth The maximum depth for the recursion, `5` by default. 121 | * @param {T} input The zod input. 122 | * @return {UnwrapNestedZodRecursive} The unwrapped zod instance. 123 | * 124 | * @__PURE__ 125 | */ 126 | export function unwrapNestedZodRecursively< 127 | T extends ZodType, 128 | Depth extends number = 5 129 | >(input: T): UnwrapNestedZodRecursive { 130 | let current = input as ZodType 131 | 132 | for (const layer of iterateZodLayers(input)) { 133 | current = layer 134 | } 135 | 136 | return current as UnwrapNestedZodRecursive & ZodType 137 | } 138 | 139 | /** 140 | * Iterates the zod layers by unwrapping the values of the following types: 141 | * 142 | * - {@link ZodArray} 143 | * - {@link ZodCatch} 144 | * - {@link ZodDefault} 145 | * - {@link ZodEffects} 146 | * - {@link ZodLazy} 147 | * - {@link ZodNullable} 148 | * - {@link ZodOptional} 149 | * - {@link ZodPromise} 150 | * - {@link ZodSet} 151 | * - {@link ZodTransformer} 152 | * 153 | * @export 154 | * @template T The input zod type. 155 | * @param {T} input The zod input. 156 | */ 157 | export function* iterateZodLayers(input: T) { 158 | let current = input as ZodType 159 | let unwrapped = unwrapNestedZod(input) as ZodType 160 | 161 | while (unwrapped !== current) { 162 | yield current 163 | current = unwrapped 164 | unwrapped = unwrapNestedZod(current) as ZodType 165 | } 166 | 167 | yield current 168 | } 169 | -------------------------------------------------------------------------------- /src/helpers/with-suffix.ts: -------------------------------------------------------------------------------- 1 | type Suffixed 2 | = S extends `${string}${Suffix}` ? S : `${S}${Suffix}` 3 | 4 | /** 5 | * Returns a function which will take a string and return another string 6 | * with the given suffix, if the suffix was already there then this function 7 | * will simply return the string value. 8 | * 9 | * @export 10 | * @typedef S The suffix string. 11 | * @param {S} suffix The suffix string. 12 | * @return {(input: string) => string} The suffixed string. 13 | */ 14 | export function withSuffix(suffix: S) { 15 | /** 16 | * Returns a string with previously entered suffix is applied. 17 | * @typedef I The input string. 18 | * @param {I} input The input string. 19 | * @return {Suffixed} The suffixed string. 20 | */ 21 | return function _withSuffix(input: I): Suffixed { 22 | if (input.endsWith(suffix)) { 23 | return input as Suffixed 24 | } 25 | 26 | return (input + suffix) as Suffixed 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/helpers/zod-validator.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ZodError, ZodType } from 'zod' 2 | 3 | import { 4 | ArgumentMetadata, 5 | BadRequestException, 6 | PipeTransform, 7 | Type, 8 | ValidationError, 9 | } from '@nestjs/common' 10 | 11 | /** 12 | * A validation pipe from `zod` validation schema. 13 | * 14 | * @export 15 | * @class ZodValidatorPipe 16 | * @implements {PipeTransform} 17 | * @template T The type of the `zod` validation schema. 18 | */ 19 | export class ZodValidatorPipe implements PipeTransform { 20 | constructor( 21 | protected readonly input: T, 22 | protected readonly klass?: Type 23 | ) {} 24 | 25 | async transform(value: any, _metadata: ArgumentMetadata): Promise { 26 | try { 27 | return await this.input.parseAsync(value, { async: true }) 28 | } 29 | catch (error_) { 30 | const error = error_ as ZodError 31 | 32 | const message = error.issues.map(issue => { 33 | const property = issue.path[ 0 ] 34 | 35 | const targetValue = issue.path.reduce((prev, curr) => { 36 | if (!prev) return 37 | if (typeof prev !== 'object') return 38 | return prev[ curr ] 39 | }, value) 40 | 41 | let children: ValidationError[] | undefined 42 | if (issue.path.length > 1) { 43 | children = [ { property: String(issue.path[ 1 ]) } ] 44 | 45 | let curr = children[ 0 ] 46 | for (let i = 2, limit = issue.path.length; i < limit; ++i) { 47 | curr.children = [ curr = { property: String(issue.path[ i ]) } ] 48 | } 49 | 50 | curr.value = targetValue 51 | curr.constraints = { 52 | [ issue.code ]: issue.message 53 | } 54 | } 55 | 56 | return { 57 | property, 58 | children, 59 | value: targetValue, 60 | constraints: { 61 | [ issue.code ]: issue.message 62 | }, 63 | } as ValidationError 64 | }) 65 | 66 | throw new BadRequestException(message, 'Validation Exception') 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decorators' 2 | export { getZodObject, getZodObjectName } from './helpers' 3 | 4 | export { 5 | IModelFromZodOptions, 6 | modelFromZod, 7 | modelFromZodBase, 8 | } from './model-from-zod' 9 | -------------------------------------------------------------------------------- /src/model-from-zod.ts: -------------------------------------------------------------------------------- 1 | import type { Type } from '@nestjs/common' 2 | import type { EnumProvider } from './types/enum-provider' 3 | import type { TypeProvider } from './types/type-provider' 4 | 5 | import type { AnyZodObject, ParseParams, TypeOf, ZodError, ZodTypeAny } from 'zod' 6 | 7 | import { ObjectType, ObjectTypeOptions } from '@nestjs/graphql' 8 | 9 | import { extractNameAndDescription, parseShape } from './helpers' 10 | import { ZodObjectKey } from './helpers/constants' 11 | 12 | export interface IModelFromZodOptions 13 | extends ObjectTypeOptions { 14 | /** 15 | * The name of the model class in GraphQL schema. 16 | * 17 | * @type {string} 18 | * @memberof IModelFromZodOptions 19 | */ 20 | name?: string 21 | 22 | /** 23 | * Indicates whether or not the property should be parsed safely. 24 | * 25 | * If this property is set to `true`, then `safeParse` will be used and 26 | * if parsing is failed, the {@link onParseError} function will be called 27 | * to provide a replace value. 28 | * 29 | * @type {boolean} 30 | * @memberof IModelFromZodOptions 31 | * @see {@link doNotThrow} 32 | */ 33 | safe?: boolean 34 | 35 | /** 36 | * Indicates if the parsing should throw when no value could be set 37 | * when there was an error during parsing. 38 | * 39 | * If this property is set to `true`, then the value will be `undefined` 40 | * if data could not be parsed successfully. 41 | * 42 | * @type {boolean} 43 | * @memberof IModelFromZodOptions 44 | */ 45 | doNotThrow?: boolean 46 | 47 | /** 48 | * Indicates whether or not the zod object should be kept inside the 49 | * dynamically generated class. 50 | * 51 | * If this property is set to `true`, use {@link getZodObject} function 52 | * to get the source object from a target. 53 | * 54 | * @type {boolean} 55 | * @memberof IModelFromZodOptions 56 | */ 57 | keepZodObject?: boolean 58 | 59 | /** 60 | * Indicates whether or not the successfully parsed objects should 61 | * be converted to their dynamically built class instances. 62 | * 63 | * @type {boolean} 64 | * @memberof IModelFromZodOptions 65 | * @default true 66 | */ 67 | parseToInstance?: boolean 68 | 69 | /** 70 | * A function that can be used for providing a default value for a property 71 | * that had an error during parsing. 72 | * 73 | * @template K The type of the key. 74 | * @param {K} key The key that could not be parsed. 75 | * @param {T[ K ]} newValue The new value that is tried to be parsed. 76 | * @param {(T[ K ] | undefined)} oldValue The previous value of the property. 77 | * @param {ZodError} error The error thrown during parsing. 78 | * @return {*} {(T[ keyof T ] | void)} An alternative fallback value to 79 | * replace and dismiss the error, or nothing. 80 | * 81 | * @memberof IModelFromZodOptions 82 | */ 83 | onParseError?>( 84 | key: K, 85 | newValue: TypeOf[ K ], 86 | oldValue: TypeOf[ K ] | undefined, 87 | error: ZodError[ K ]> 88 | ): TypeOf[ keyof TypeOf ] | void 89 | 90 | /** 91 | * A function that can be used for providing {@link zod.ParseParams} for 92 | * a key during the parsing process (on set). 93 | * 94 | * @template K The type of the key. 95 | * @param {K} key The key that is being parsed. 96 | * @param {(T[ K ] | undefined)} previousValue The previously set value. 97 | * @return {Partial} The {@link zod.ParseParams} for the 98 | * current parsing stage. 99 | * 100 | * @memberof IModelFromZodOptions 101 | */ 102 | onParsing?>( 103 | key: K, 104 | previousValue: TypeOf[ K ] | undefined 105 | ): Partial 106 | 107 | /** 108 | * Gets the scalar type for given type name. 109 | * 110 | * @param {string} typeName The type name corresponding to the zod object. 111 | * @return {GraphQLScalarType} The scalar type for the zod object. 112 | */ 113 | getScalarTypeFor?: TypeProvider 114 | 115 | /** 116 | * Provides a name for nested classes when they are created dynamically from 117 | * object properties of zod types. 118 | * 119 | * @param {string} parentName The parent class name. 120 | * @param {string} propertyKey The property key/name. 121 | * @return {(string | undefined)} The name to set for the class. If 122 | * any value returned other than a `string`, the class name will be generated 123 | * automatically. 124 | * 125 | * @memberof IModelFromZodOptions 126 | */ 127 | provideNameForNestedClass?(parentName: string, propertyKey: string): string | undefined 128 | 129 | /** 130 | * Gets an enum type for given information. 131 | * 132 | * Use this function to prevent creating different enums in GraphQL schema 133 | * if you are going to use same values in different places. 134 | * 135 | * @param {string | undefined} name The parent name that contains the enum 136 | * type. 137 | * @param {string} key The property name of the enum. 138 | * @param {(Record)} enumObject The enum object 139 | * that is extracted from the zod. 140 | * @return {(Record | undefined)} The enum 141 | * that will be used instead of creating a new one. If `undefined` is 142 | * returned, then a new enum will be created. 143 | * 144 | * @memberof IModelFromZodOptions 145 | */ 146 | getEnumType?: EnumProvider 147 | } 148 | 149 | type Options 150 | = IModelFromZodOptions 151 | & { 152 | /** 153 | * Provides the decorator to decorate the dynamically generated class. 154 | * 155 | * @param {T} zodInput The zod input. 156 | * @param {string} key The name of the currently processsed property. 157 | * @return {ClassDecorator} The class decorator to decorate the class. 158 | * @memberof IOptions 159 | */ 160 | getDecorator?(zodInput: T, key: string): ClassDecorator 161 | } 162 | 163 | let _generatedClasses: WeakMap | undefined 164 | 165 | /** 166 | * Creates a dynamic class which will be compatible with GraphQL, from a 167 | * `zod` model. 168 | * 169 | * @export 170 | * @template T The type of the zod input. 171 | * @param {T} zodInput The zod object input. 172 | * @param {IModelFromZodOptions} [options={}] The options for model creation. 173 | * @return {Type} A class that represents the `zod` object and also 174 | * compatible with `GraphQL`. 175 | */ 176 | export function modelFromZodBase< 177 | T extends AnyZodObject, 178 | O extends Options 179 | >( 180 | zodInput: T, 181 | options: O = {} as O, 182 | decorator: ClassDecorator 183 | ): Type> { 184 | const previousRecord 185 | = (_generatedClasses ??= new WeakMap()) 186 | .get(zodInput) 187 | 188 | if (previousRecord) return previousRecord 189 | 190 | const { name, description } = extractNameAndDescription(zodInput, options) 191 | let { keepZodObject = false } = options 192 | 193 | class DynamicZodModel {} 194 | const prototype = DynamicZodModel.prototype 195 | 196 | decorator(DynamicZodModel) 197 | 198 | if (keepZodObject) { 199 | Object.defineProperty(prototype, ZodObjectKey, { 200 | value: { ...zodInput }, 201 | configurable: false, 202 | writable: false, 203 | }) 204 | } 205 | 206 | const parsed = parseShape(zodInput, { 207 | ...options, 208 | name, 209 | description, 210 | getDecorator: options.getDecorator as any, 211 | }) 212 | 213 | for (const { descriptor, key, decorateFieldProperty } of parsed) { 214 | Object.defineProperty(prototype, key as string, descriptor) 215 | decorateFieldProperty(prototype, key as string) 216 | } 217 | 218 | _generatedClasses.set(zodInput, DynamicZodModel) 219 | return DynamicZodModel as Type> 220 | } 221 | 222 | /** 223 | * Creates a dynamic class which will be compatible with GraphQL, from a 224 | * `zod` model. 225 | * 226 | * @export 227 | * @template T The type of the zod input. 228 | * @param {T} zodInput The zod object input. 229 | * @param {IModelFromZodOptions} [options={}] The options for model creation. 230 | * @return {Type} A class that represents the `zod` object and also 231 | * compatible with `GraphQL`. 232 | */ 233 | export function modelFromZod< 234 | T extends AnyZodObject, 235 | O extends IModelFromZodOptions 236 | >(zodInput: T, options: O = {} as O): Type> { 237 | const { name, description } = extractNameAndDescription(zodInput, options) 238 | 239 | const decorator = ObjectType(name, { 240 | description, 241 | isAbstract: zodInput.isNullable() || zodInput.isOptional(), 242 | ...options 243 | }) 244 | 245 | return modelFromZodBase(zodInput, options, decorator) 246 | } 247 | -------------------------------------------------------------------------------- /src/types/enum-provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The enum provider data. 3 | */ 4 | export interface EnumProviderData { 5 | /** 6 | * The property name of the enum. 7 | * 8 | * @type {string} 9 | * @memberof EnumProviderData 10 | */ 11 | name: string 12 | 13 | /** 14 | * The parent name that contains the enum type. 15 | * 16 | * @type {string} 17 | * @memberof EnumProviderData 18 | */ 19 | parentName?: string 20 | 21 | /** 22 | * The description of the enum. 23 | * 24 | * @type {string} 25 | * @memberof EnumProviderData 26 | */ 27 | description?: string 28 | 29 | /** 30 | * Indicates that if the enum was a native enum. 31 | * 32 | * @type {boolean} 33 | * @memberof EnumProviderData 34 | */ 35 | isNative?: boolean 36 | } 37 | 38 | /** 39 | * Gets an enum type for given information. 40 | * 41 | * Use this function to prevent creating different enums in GraphQL schema 42 | * if you are going to use same values in different places. 43 | * 44 | * @param {(Record)} enumObject The enum object 45 | * that is extracted from the zod. 46 | * 47 | * @param {EnumProviderData} info The information of the enum property. 48 | * 49 | * @return {(Record | undefined)} The enum 50 | * that will be used instead of creating a new one. If `undefined` is 51 | * returned, then a new enum will be created. 52 | * 53 | * @memberof IModelFromZodOptions 54 | */ 55 | export type EnumProvider = ( 56 | enumObject: object, 57 | info: EnumProviderData 58 | ) => object | undefined 59 | -------------------------------------------------------------------------------- /src/types/prev.ts: -------------------------------------------------------------------------------- 1 | // This is a helper type to prevent infinite recursion in typing rules. 2 | // 3 | // Use this with your `depth` variable in your types. 4 | export type Prev = [ 5 | never, 6 | 0, 7 | 1, 8 | 2, 9 | 3, 10 | 4, 11 | 5, 12 | 6, 13 | 7, 14 | 8, 15 | 9, 16 | 10, 17 | 11, 18 | 12, 19 | 13, 20 | 14, 21 | 15, 22 | 16, 23 | 17, 24 | 18, 25 | 19, 26 | 20, 27 | ...0[] 28 | ] 29 | -------------------------------------------------------------------------------- /src/types/type-provider.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLScalarType } from 'graphql/type/definition' 2 | 3 | /** 4 | * The type provider function. 5 | * 6 | * The type name will be calculated and it will be similar to `TypeScript` 7 | * types such as: `Record, Array>`. 8 | * 9 | * The user will provide custom scalar type to use for that kind of 10 | * zod validation. 11 | */ 12 | export type TypeProvider = (typeName: string) => GraphQLScalarType | undefined 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "experimentalDecorators": true, 5 | "emitDecoratorMetadata": true, 6 | "module": "commonjs", 7 | "rootDir": "./src", 8 | "moduleResolution": "node", 9 | "baseUrl": ".", 10 | "declaration": true, 11 | "emitDeclarationOnly": false, 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "newLine": "lf", 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "strict": true, 18 | "skipDefaultLibCheck": true, 19 | "skipLibCheck": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true 22 | }, 23 | "exclude": [ 24 | "node_modules", 25 | "tests", 26 | "dist" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------