express-zod-api
Advanced tools
Comparing version 2.5.2 to 2.6.0
@@ -5,2 +5,39 @@ # Changelog | ||
### v2.6.0 | ||
- Zod version is 3.9.8 | ||
- It supports the ability to specify the key schema of `z.record()`. | ||
- In case of using enums and literals in the key schema they will be described as required ones in the generated | ||
OpenAPI / Swagger documentation. | ||
```typescript | ||
// example | ||
z.record( | ||
z.enum(['option1', 'option2']), // keys | ||
z.boolean() // values | ||
); | ||
``` | ||
- Feature #145: `attachRouting()` now returns the `logger` instance and `notFoundHandler`. You can use it with your | ||
custom express app for handling `404` (not found) errors: | ||
```typescript | ||
const {notFoundHandler} = attachRouting(config, routing); | ||
app.use(notFoundHandler); | ||
app.listen(); | ||
``` | ||
Or you can use the `logger` instance with any `ResultHandler` for the same purpose: | ||
```typescript | ||
const {logger} = attachRouting(config, routing); | ||
app.use((request, response) => { | ||
defaultResultHandler.handler({ | ||
request, response, logger, | ||
error: createHttpError(404, `${request.path} not found`), | ||
input: null, | ||
output: null | ||
}); | ||
}); | ||
app.listen(); | ||
``` | ||
### v2.5.2 | ||
@@ -7,0 +44,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { ParseContext, ParseReturnType, ZodParsedType, ZodType } from 'zod'; | ||
import { ParseContext, ParseReturnType, ZodParsedType, ZodType, ZodTypeDef } from 'zod'; | ||
import { ErrMessage } from './helpers'; | ||
@@ -11,3 +11,3 @@ declare const zodFileKind = "ZodFile"; | ||
}; | ||
export interface ZodFileDef { | ||
export interface ZodFileDef extends ZodTypeDef { | ||
checks: ZodFileCheck[]; | ||
@@ -14,0 +14,0 @@ typeName: typeof zodFileKind; |
@@ -28,7 +28,7 @@ "use strict"; | ||
if (parsedType !== zod_1.ZodParsedType.string) { | ||
ctx.addIssue(data, { | ||
this.addIssue(ctx, { | ||
code: zod_1.ZodIssueCode.invalid_type, | ||
expected: zod_1.ZodParsedType.string, | ||
received: parsedType, | ||
}); | ||
}, { data }); | ||
return zod_1.INVALID; | ||
@@ -41,6 +41,6 @@ } | ||
invalid = true; | ||
ctx.addIssue(data, { | ||
this.addIssue(ctx, { | ||
code: zod_1.ZodIssueCode.custom, | ||
message: check.message, | ||
}); | ||
}, { data }); | ||
} | ||
@@ -47,0 +47,0 @@ } |
@@ -49,4 +49,3 @@ "use strict"; | ||
...otherProps, | ||
type: 'object', | ||
additionalProperties: describeSchema(value._def.valueType, isResponse) | ||
...describeRecord(value._def, isResponse) | ||
}; | ||
@@ -78,3 +77,3 @@ case value instanceof zod_1.z.ZodObject: | ||
...otherProps, | ||
...describeTransformation(value, isResponse) | ||
...describeEffect(value, isResponse) | ||
}; | ||
@@ -137,2 +136,44 @@ case value instanceof zod_1.z.ZodOptional: | ||
}; | ||
const describeRecord = (definition, isResponse) => { | ||
if (definition.keyType instanceof zod_1.z.ZodEnum || definition.keyType instanceof zod_1.z.ZodNativeEnum) { | ||
const keys = Object.values(definition.keyType._def.values); | ||
const shape = keys.reduce((carry, key) => ({ | ||
...carry, | ||
[key]: definition.valueType | ||
}), {}); | ||
return { | ||
type: 'object', | ||
properties: describeObjectProperties(zod_1.z.object(shape), isResponse), | ||
required: keys | ||
}; | ||
} | ||
if (definition.keyType instanceof zod_1.z.ZodLiteral) { | ||
return { | ||
type: 'object', | ||
properties: describeObjectProperties(zod_1.z.object({ | ||
[definition.keyType._def.value]: definition.valueType | ||
}), isResponse), | ||
required: [definition.keyType._def.value] | ||
}; | ||
} | ||
if (definition.keyType instanceof zod_1.z.ZodUnion) { | ||
const areOptionsLiteral = definition.keyType.options | ||
.reduce((carry, option) => carry && option instanceof zod_1.z.ZodLiteral, true); | ||
if (areOptionsLiteral) { | ||
const shape = definition.keyType.options.reduce((carry, option) => ({ | ||
...carry, | ||
[option.value]: definition.valueType | ||
}), {}); | ||
return { | ||
type: 'object', | ||
properties: describeObjectProperties(zod_1.z.object(shape), isResponse), | ||
required: definition.keyType.options.map((option) => option.value) | ||
}; | ||
} | ||
} | ||
return { | ||
type: 'object', | ||
additionalProperties: describeSchema(definition.valueType, isResponse) | ||
}; | ||
}; | ||
const describeArray = (definition, isResponse) => { | ||
@@ -205,20 +246,9 @@ var _a; | ||
}; | ||
const getTransformationMod = (def) => { | ||
if ('effects' in def && def.effects && def.effects.length > 0) { | ||
const effect = def.effects.filter((ef) => ef.type === 'transform').slice(-1)[0]; | ||
if (effect && 'transform' in effect) { | ||
return { ...effect, isPreprocess: false }; | ||
} | ||
} | ||
if ('preprocess' in def && def.preprocess && def.preprocess.type === 'transform') { | ||
return { ...def.preprocess, isPreprocess: true }; | ||
} | ||
}; | ||
const describeTransformation = (value, isResponse) => { | ||
const describeEffect = (value, isResponse) => { | ||
const input = describeSchema(value._def.schema, isResponse); | ||
const mod = getTransformationMod(value._def); | ||
if (isResponse && mod && !mod.isPreprocess) { | ||
const effect = value._def.effect; | ||
if (isResponse && effect && effect.type === 'transform') { | ||
let output = 'undefined'; | ||
try { | ||
output = typeof mod.transform(['integer', 'number'].includes(`${input.type}`) ? 0 : | ||
output = typeof effect.transform(['integer', 'number'].includes(`${input.type}`) ? 0 : | ||
'string' === input.type ? '' : | ||
@@ -238,3 +268,3 @@ 'boolean' === input.type ? false : | ||
} | ||
if (!isResponse && mod && mod.isPreprocess) { | ||
if (!isResponse && effect && effect.type === 'preprocess') { | ||
const { type: inputType, ...rest } = input; | ||
@@ -241,0 +271,0 @@ return { |
@@ -0,3 +1,4 @@ | ||
/// <reference types="qs" /> | ||
/// <reference types="node" /> | ||
import { ErrorRequestHandler, RequestHandler } from 'express'; | ||
import express, { ErrorRequestHandler, RequestHandler } from 'express'; | ||
import { Server } from 'http'; | ||
@@ -9,3 +10,6 @@ import { Logger } from 'winston'; | ||
export declare const createNotFoundHandler: (errorHandler: import("./result-handler").ResultHandlerDefinition<any, any>, logger: Logger) => RequestHandler; | ||
export declare function attachRouting(config: AppConfig & CommonConfig, routing: Routing): void; | ||
export declare function attachRouting(config: AppConfig & CommonConfig, routing: Routing): { | ||
notFoundHandler: express.RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>; | ||
logger: Logger; | ||
}; | ||
export declare function createServer(config: ServerConfig & CommonConfig, routing: Routing): Server; |
@@ -66,3 +66,6 @@ "use strict"; | ||
const logger = (0, helpers_1.isLoggerConfig)(config.logger) ? (0, logger_1.createLogger)(config.logger) : config.logger; | ||
return (0, routing_1.initRouting)({ app: config.app, routing, logger, config }); | ||
(0, routing_1.initRouting)({ app: config.app, routing, logger, config }); | ||
const errorHandler = config.errorHandler || result_handler_1.defaultResultHandler; | ||
const notFoundHandler = (0, exports.createNotFoundHandler)(errorHandler, logger); | ||
return { notFoundHandler, logger }; | ||
} | ||
@@ -69,0 +72,0 @@ exports.attachRouting = attachRouting; |
import { UploadedFile } from 'express-fileupload'; | ||
import { ParseContext, ParseReturnType, ZodParsedType, ZodType } from 'zod'; | ||
import { ParseContext, ParseReturnType, ZodParsedType, ZodType, ZodTypeDef } from 'zod'; | ||
declare const zodUploadKind = "ZodUpload"; | ||
export interface ZodUploadDef { | ||
export interface ZodUploadDef extends ZodTypeDef { | ||
typeName: typeof zodUploadKind; | ||
@@ -6,0 +6,0 @@ } |
@@ -15,6 +15,6 @@ "use strict"; | ||
if (parsedType !== zod_1.ZodParsedType.object || !isUploadedFile(data)) { | ||
ctx.addIssue(data, { | ||
this.addIssue(ctx, { | ||
code: zod_1.ZodIssueCode.custom, | ||
message: `Expected file upload, received ${parsedType}` | ||
}); | ||
}, { data }); | ||
return zod_1.INVALID; | ||
@@ -21,0 +21,0 @@ } |
{ | ||
"name": "express-zod-api", | ||
"version": "2.5.2", | ||
"version": "2.6.0", | ||
"description": "A Typescript library to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -16,4 +16,6 @@ # Express Zod API | ||
1. [Technologies](#technologies) | ||
2. [Concept](#concept) | ||
1. [Why and what is it for](#why-and-what-is-it-for) | ||
2. [How it works](#how-it-works) | ||
1. [Technologies](#technologies) | ||
2. [Concept](#concept) | ||
3. [Installation](#installation) | ||
@@ -43,13 +45,34 @@ 4. [Basic usage](#basic-usage) | ||
If you're upgrading from v1 please check out the information in [Changelog](CHANGELOG.md). | ||
If you're upgrading from v1 please check out the information in [Changelog](CHANGELOG.md#v200-beta1). | ||
# Technologies | ||
# Why and what is it for | ||
- [Typescript](https://www.typescriptlang.org/) first | ||
I made this library because of the often repetitive tasks of starting a web server APIs with the need to validate input | ||
data. It integrates and provides the capabilities of popular web server, logger, validation and documenting solutions. | ||
Therefore, many basic tasks can be accomplished faster and easier, in particular: | ||
- You can describe web server routes as a hierarchical object. | ||
- You can keep the endpoint's input and output type declarations right next to its handler. | ||
- All input and output data types are validated, so it ensures you won't have an empty string, null or undefined where | ||
you expect a number. | ||
- Variables within an endpoint handler have types according to the declared schema, so your IDE and Typescript will | ||
provide you with necessary hints to focus on bringing your vision to life. | ||
- All of your endpoints can respond in a similar way. | ||
- The expected endpoint input and response types can be exported to the frontend, so you don't get confused about the | ||
field names when you implement the client for your API. | ||
- You can generate your API documentation in a Swagger / OpenAPI compatible format. | ||
# How it works | ||
## Technologies | ||
- [Typescript](https://www.typescriptlang.org/) first. | ||
- Web server — [Express.js](https://expressjs.com/). | ||
- Schema validation — [Zod 3.x](https://github.com/colinhacks/zod). | ||
- Webserver — [Express.js](https://expressjs.com/). | ||
- Logger — [Winston](https://github.com/winstonjs/winston). | ||
- Swagger - [OpenAPI 3.x](https://github.com/metadevpro/openapi3-ts) | ||
- Documenting - [OpenAPI 3.x](https://github.com/metadevpro/openapi3-ts) (formerly known as the Swagger Specification). | ||
- File uploads — [Express-FileUpload](https://github.com/richardgirges/express-fileupload) | ||
(based on [Busboy](https://github.com/mscdex/busboy)) | ||
# Concept | ||
## Concept | ||
The API operates object schemas for input and output, including unions and intersections of object schemas | ||
@@ -62,9 +85,9 @@ (`.or()`, `.and()`), but in general the API can [respond with any data type](#non-object-response) and | ||
Middlewares can handle validated inputs and the original `request`, for example, to perform the authentication or | ||
provide the endpoint's handler with some request properties like the actual method. The returns of middlewares are | ||
combined into the `options` parameter available to the next middlewares and the endpoint's handler. | ||
Middlewares can handle inputs and the `request` properties, like headers, for example, to perform the authentication or | ||
provide the endpoint with some properties like the actual request method. The returns of middlewares are combined into | ||
the `options` parameter available to the next connected middlewares and the endpoint's handler. | ||
The handler's parameter `input` combines the validated inputs of all connected middlewares along with the handler's one. | ||
The result that the handler returns goes to the `ResultHandler` which is responsible for transmission of the final | ||
response or possible error. | ||
The `input` parameter of the endpoint's handler consists of the inputs of all connected middlewares along with its own | ||
one. The output of the endpoint's handler goes to the `ResultHandler` which is responsible for transmission of the | ||
final response or possible error. | ||
@@ -105,3 +128,3 @@ All inputs and outputs are validated and there are also advanced powerful features like transformations and refinements. | ||
server: { | ||
listen: 8090, | ||
listen: 8090, // port or socket | ||
}, | ||
@@ -119,10 +142,21 @@ cors: true, | ||
In the basic case, you can just import and use the default factory: | ||
```typescript | ||
import {defaultEndpointsFactory} from './endpoints-factory'; | ||
// same as: new EndpointsFactory(defaultResultHandler) | ||
const endpointsFactory = defaultEndpointsFactory; | ||
import {defaultEndpointsFactory} from 'express-zod-api'; | ||
``` | ||
You can also instantly add middlewares to it using `.addMiddleware()` method. | ||
If you want to connect [middlewares](#create-a-middleware) to the default factory right away, you can do it the | ||
following way: | ||
```typescript | ||
import {defaultEndpointsFactory} from 'express-zod-api'; | ||
const endpointsFactory = defaultEndpointsFactory.addMiddleware( | ||
yourMiddleware | ||
); | ||
``` | ||
By the way, `defaultEndpointsFactory` is the same as `new EndpointsFactory(defaultResultHandler)`. | ||
Therefore, if you need to customize the response, see [ResultHandler](#resulthandler). | ||
## Create your first endpoint | ||
@@ -150,7 +184,10 @@ | ||
The endpoint can also handle multiple types of requests, this feature is available by using `methods` property that | ||
accepts an array. You can also add middlewares to the endpoint by using `.addMiddleware()` before `.build()`. | ||
Endpoints can also handle multiple types of requests, by using `methods` property instead of `method` that | ||
accepts an array. You can also add [middlewares](#create-a-middleware) to the endpoint by using `.addMiddleware()` | ||
before `.build()`. | ||
## Set up routing | ||
Connect your endpoint to the `/v1/setUser` route: | ||
```typescript | ||
@@ -165,3 +202,2 @@ import {Routing} from 'express-zod-api'; | ||
``` | ||
This implementation sets up `setUserEndpoint` to handle requests to the `/v1/setUser` route. | ||
@@ -381,3 +417,3 @@ ## Start your server | ||
handler: async ({input: {avatar}}) => { | ||
// avatar: {name, mv(), mimetype, encoding, data, truncated, size, ...} | ||
// avatar: {name, mv(), mimetype, data, size, ...} | ||
// avatar.truncated is true on failure | ||
@@ -418,8 +454,13 @@ return {...}; | ||
attachRouting(config, routing); | ||
const {notFoundHandler, logger} = attachRouting(config, routing); | ||
app.use(notFoundHandler); // optional | ||
app.listen(); | ||
logger.info('Glory to science!'); | ||
``` | ||
**Please note** that in this case you probably need to: parse `request.body`, call `app.listen()` and handle `404` | ||
errors yourself; | ||
errors yourself. In this regard `attachRouting()` provides you with `notFoundHandler` which you can optionally connect | ||
to your custom express app. | ||
@@ -426,0 +467,0 @@ ## Multiple schemas for a single route |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
153779
1536
570