express-zod-api 21.0.0-beta.3 → 21.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -20
- package/README.md +58 -73
- package/dist/index.cjs +7 -7
- package/dist/index.d.cts +56 -68
- package/dist/index.d.ts +56 -68
- package/dist/index.js +7 -7
- package/migration/index.cjs +1 -1
- package/migration/index.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,28 +5,25 @@
|
|
|
5
5
|
### v21.0.0
|
|
6
6
|
|
|
7
7
|
- Minimum supported versions of `express`: 4.21.1 and 5.0.1 (fixed vulnerabilities);
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- The `
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- Other potentially breaking changes:
|
|
25
|
-
- The optional property `methods` of `EndpointsFactory::build()` must be non-empty array when used;
|
|
26
|
-
- The `Endpoint::getMethods()` method may now return `undefined`;
|
|
8
|
+
- Breaking changes to `createConfig()` argument:
|
|
9
|
+
- The `server` property renamed to `http` and made optional — (can now configure HTTPS only);
|
|
10
|
+
- These properties moved to the top level: `jsonParser`, `upload`, `compression`, `rawParser` and `beforeRouting`;
|
|
11
|
+
- Both `logger` and `getChildLogger` arguments of `beforeRouting` function are replaced with all-purpose `getLogger`.
|
|
12
|
+
- Breaking changes to `createServer()` resolved return:
|
|
13
|
+
- Both `httpServer` and `httpsServer` are combined into single `servers` property (array, same order).
|
|
14
|
+
- Breaking changes to `EndpointsFactory::build()` argument:
|
|
15
|
+
- Plural `methods`, `tags` and `scopes` properties replaced with singular `method`, `tag`, `scope` accordingly;
|
|
16
|
+
- The `method` property also made optional and can now be derived from `DependsOnMethod` or imply `GET` by default;
|
|
17
|
+
- When `method` is assigned with an array, it must be non-empty.
|
|
18
|
+
- Breaking changes to `positive` and `negative` propeties of `ResultHandler` constructor argument:
|
|
19
|
+
- Plural `statusCodes` and `mimeTypes` props within the values are replaced with singular `statusCode` and `mimeType`.
|
|
20
|
+
- Other breaking changes:
|
|
21
|
+
- The `serializer` property of `Documentation` and `Integration` constructor argument removed;
|
|
22
|
+
- The `originalError` property of `InputValidationError` and `OutputValidationError` removed (use `cause` instead);
|
|
23
|
+
- The `getStatusCodeFromError()` method removed (use the `ensureHttpError().statusCode` instead);
|
|
27
24
|
- The `testEndpoint()` method can no longer test CORS headers — that function moved to `Routing` traverse;
|
|
25
|
+
- For `Endpoint`: `getMethods()` may return `undefined`, `getMimeTypes()` removed, `getSchema()` variants reduced;
|
|
28
26
|
- Public properties `pairs`, `firstEndpoint` and `siblingMethods` of `DependsOnMethod` replaced with `entries`.
|
|
29
|
-
- Routing traverse improvement: performance +5%, memory consumption -17%.
|
|
30
27
|
- Consider the automated migration using the built-in ESLint rule.
|
|
31
28
|
|
|
32
29
|
```js
|
|
@@ -40,8 +37,71 @@ export default [
|
|
|
40
37
|
];
|
|
41
38
|
```
|
|
42
39
|
|
|
40
|
+
```ts
|
|
41
|
+
// The sample of new structure
|
|
42
|
+
const config = createConfig({
|
|
43
|
+
http: { listen: 80 }, // became optional
|
|
44
|
+
https: { listen: 443, options: {} },
|
|
45
|
+
upload: true,
|
|
46
|
+
compression: true,
|
|
47
|
+
beforeRouting: ({ app, getLogger }) => {
|
|
48
|
+
const logger = getLogger();
|
|
49
|
+
app.use((req, res, next) => {
|
|
50
|
+
const childLogger = getLogger(req);
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
const { servers } = await createServer(config, {});
|
|
55
|
+
```
|
|
56
|
+
|
|
43
57
|
## Version 20
|
|
44
58
|
|
|
59
|
+
### v20.22.0
|
|
60
|
+
|
|
61
|
+
- Featuring a helper to describe nested Routing for already assigned routes:
|
|
62
|
+
- Suppose you want to describe `Routing` for both `/v1/path` and `/v1/path/subpath` routes having Endpoints attached;
|
|
63
|
+
- Previously, an empty path segment was proposed for that purpose, but there is more elegant and readable way now;
|
|
64
|
+
- The `.nest()` method is available both on `Endpoint` and `DependsOnMethod` instances:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { Routing } from "express-zod-api";
|
|
68
|
+
|
|
69
|
+
// Describing routes /v1/path and /v1/path/subpath both having endpoints assigned:
|
|
70
|
+
const before: Routing = {
|
|
71
|
+
v1: {
|
|
72
|
+
path: {
|
|
73
|
+
"": endpointA,
|
|
74
|
+
subpath: endpointB,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const after: Routing = {
|
|
80
|
+
v1: {
|
|
81
|
+
path: endpointA.nest({
|
|
82
|
+
subpath: endpointB,
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### v20.21.2
|
|
89
|
+
|
|
90
|
+
- Fixed the example implementation in the generated client for endpoints using path params:
|
|
91
|
+
- The choice of parser was made based on the exported `const jsonEndpoints` indexed by `path`;
|
|
92
|
+
- The actual `path` used for the lookup already contained parameter substitutions so that JSON parser didn't work;
|
|
93
|
+
- The new example implementation suggests choosing the parser based on the actual `response.headers`;
|
|
94
|
+
- The issue was found and reported by [@HenriJ](https://github.com/HenriJ).
|
|
95
|
+
|
|
96
|
+
```diff
|
|
97
|
+
- const parser = `${method} ${path}` in jsonEndpoints ? "json" : "text";
|
|
98
|
+
+ const isJSON = response.headers
|
|
99
|
+
+ .get("content-type")
|
|
100
|
+
+ ?.startsWith("application/json");
|
|
101
|
+
- return response[parser]();
|
|
102
|
+
+ return response[isJSON ? "json" : "text"]();
|
|
103
|
+
```
|
|
104
|
+
|
|
45
105
|
### v20.21.1
|
|
46
106
|
|
|
47
107
|
- Performance tuning: `Routing` traverse made about 12 times faster.
|
package/README.md
CHANGED
|
@@ -32,17 +32,18 @@ Start your API server with I/O schema validation and custom middlewares in minut
|
|
|
32
32
|
13. [Enabling compression](#enabling-compression)
|
|
33
33
|
5. [Advanced features](#advanced-features)
|
|
34
34
|
1. [Customizing input sources](#customizing-input-sources)
|
|
35
|
-
2. [
|
|
36
|
-
3. [
|
|
37
|
-
4. [
|
|
38
|
-
5. [
|
|
39
|
-
6. [
|
|
40
|
-
7. [
|
|
41
|
-
8. [
|
|
42
|
-
9. [
|
|
43
|
-
10. [
|
|
44
|
-
11. [
|
|
45
|
-
12. [Testing
|
|
35
|
+
2. [Nested routes](#nested-routes)
|
|
36
|
+
3. [Route path params](#route-path-params)
|
|
37
|
+
4. [Multiple schemas for one route](#multiple-schemas-for-one-route)
|
|
38
|
+
5. [Response customization](#response-customization)
|
|
39
|
+
6. [Error handling](#error-handling)
|
|
40
|
+
7. [Production mode](#production-mode)
|
|
41
|
+
8. [Non-object response](#non-object-response) including file downloads
|
|
42
|
+
9. [File uploads](#file-uploads)
|
|
43
|
+
10. [Serving static files](#serving-static-files)
|
|
44
|
+
11. [Connect to your own express app](#connect-to-your-own-express-app)
|
|
45
|
+
12. [Testing endpoints](#testing-endpoints)
|
|
46
|
+
13. [Testing middlewares](#testing-middlewares)
|
|
46
47
|
6. [Special needs](#special-needs)
|
|
47
48
|
1. [Different responses for different status codes](#different-responses-for-different-status-codes)
|
|
48
49
|
2. [Array response](#array-response) for migrating legacy APIs
|
|
@@ -84,6 +85,7 @@ Therefore, many basic tasks can be accomplished faster and easier, in particular
|
|
|
84
85
|
|
|
85
86
|
These people contributed to the improvement of the framework by reporting bugs, making changes and suggesting ideas:
|
|
86
87
|
|
|
88
|
+
[<img src="https://github.com/HenriJ.png" alt="@HenriJ" width="50px" />](https://github.com/HenriJ)
|
|
87
89
|
[<img src="https://github.com/JonParton.png" alt="@JonParton" width="50px" />](https://github.com/JonParton)
|
|
88
90
|
[<img src="https://github.com/williamgcampbell.png" alt="@williamgcampbell" width="50px" />](https://github.com/williamgcampbell)
|
|
89
91
|
[<img src="https://github.com/t1nky.png" alt="@t1nky" width="50px" />](https://github.com/t1nky)
|
|
@@ -206,7 +208,7 @@ The endpoint responds with "Hello, World" or "Hello, {name}" if the name is supp
|
|
|
206
208
|
import { z } from "zod";
|
|
207
209
|
|
|
208
210
|
const helloWorldEndpoint = defaultEndpointsFactory.build({
|
|
209
|
-
// method: "get" (default) or
|
|
211
|
+
// method: "get" (default) or array ["get", "post", ...]
|
|
210
212
|
input: z.object({
|
|
211
213
|
name: z.string().optional(),
|
|
212
214
|
}),
|
|
@@ -286,12 +288,9 @@ const authMiddleware = new Middleware({
|
|
|
286
288
|
handler: async ({ input: { key }, request, logger }) => {
|
|
287
289
|
logger.debug("Checking the key and token");
|
|
288
290
|
const user = await db.Users.findOne({ key });
|
|
289
|
-
if (!user)
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
if (request.headers.token !== user.token) {
|
|
291
|
+
if (!user) throw createHttpError(401, "Invalid key");
|
|
292
|
+
if (request.headers.token !== user.token)
|
|
293
293
|
throw createHttpError(401, "Invalid token");
|
|
294
|
-
}
|
|
295
294
|
return { user }; // provides endpoints with options.user
|
|
296
295
|
},
|
|
297
296
|
});
|
|
@@ -303,10 +302,9 @@ By using `.addMiddleware()` method before `.build()` you can connect it to the e
|
|
|
303
302
|
const yourEndpoint = defaultEndpointsFactory
|
|
304
303
|
.addMiddleware(authMiddleware)
|
|
305
304
|
.build({
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
},
|
|
305
|
+
handler: async ({ options: { user } }) => {
|
|
306
|
+
// user is the one returned by authMiddleware
|
|
307
|
+
}, // ...
|
|
310
308
|
});
|
|
311
309
|
```
|
|
312
310
|
|
|
@@ -320,7 +318,7 @@ import { defaultEndpointsFactory } from "express-zod-api";
|
|
|
320
318
|
const factory = defaultEndpointsFactory
|
|
321
319
|
.addMiddleware(authMiddleware) // add Middleware instance or use shorter syntax:
|
|
322
320
|
.addMiddleware({
|
|
323
|
-
handler: async ({ options: { user } }) => ({}), //
|
|
321
|
+
handler: async ({ options: { user } }) => ({}), // user from authMiddleware
|
|
324
322
|
});
|
|
325
323
|
```
|
|
326
324
|
|
|
@@ -534,18 +532,14 @@ const updateUserEndpoint = defaultEndpointsFactory.build({
|
|
|
534
532
|
method: "post",
|
|
535
533
|
input: z.object({
|
|
536
534
|
userId: z.string(),
|
|
537
|
-
birthday: ez.dateIn(), // string -> Date
|
|
535
|
+
birthday: ez.dateIn(), // string -> Date in handler
|
|
538
536
|
}),
|
|
539
537
|
output: z.object({
|
|
540
|
-
createdAt: ez.dateOut(), // Date -> string
|
|
538
|
+
createdAt: ez.dateOut(), // Date -> string in response
|
|
539
|
+
}),
|
|
540
|
+
handler: async ({ input }) => ({
|
|
541
|
+
createdAt: new Date("2022-01-22"), // 2022-01-22T00:00:00.000Z
|
|
541
542
|
}),
|
|
542
|
-
handler: async ({ input }) => {
|
|
543
|
-
// input.birthday is Date
|
|
544
|
-
return {
|
|
545
|
-
// transmitted as "2022-01-22T00:00:00.000Z"
|
|
546
|
-
createdAt: new Date("2022-01-22"),
|
|
547
|
-
};
|
|
548
|
-
},
|
|
549
543
|
});
|
|
550
544
|
```
|
|
551
545
|
|
|
@@ -563,7 +557,6 @@ That function has several parameters and can be asynchronous.
|
|
|
563
557
|
import { createConfig } from "express-zod-api";
|
|
564
558
|
|
|
565
559
|
const config = createConfig({
|
|
566
|
-
// ... other options
|
|
567
560
|
cors: ({ defaultHeaders, request, endpoint, logger }) => ({
|
|
568
561
|
...defaultHeaders,
|
|
569
562
|
"Access-Control-Max-Age": "5000",
|
|
@@ -589,8 +582,7 @@ const config = createConfig({
|
|
|
589
582
|
key: fs.readFileSync("privkey.pem", "utf-8"),
|
|
590
583
|
},
|
|
591
584
|
listen: 443, // port, UNIX socket or options
|
|
592
|
-
},
|
|
593
|
-
// ... cors, logger, etc
|
|
585
|
+
}, // ... cors, logger, etc
|
|
594
586
|
});
|
|
595
587
|
|
|
596
588
|
// 'await' is only needed if you're going to use the returned entities.
|
|
@@ -717,14 +709,13 @@ In order to receive a compressed response the client should include the followin
|
|
|
717
709
|
|
|
718
710
|
You can customize the list of `request` properties that are combined into `input` that is being validated and available
|
|
719
711
|
to your endpoints and middlewares. The order here matters: each next item in the array has a higher priority than its
|
|
720
|
-
previous sibling.
|
|
712
|
+
previous sibling. The following arrangement is default:
|
|
721
713
|
|
|
722
714
|
```typescript
|
|
723
715
|
import { createConfig } from "express-zod-api";
|
|
724
716
|
|
|
725
717
|
createConfig({
|
|
726
718
|
inputSources: {
|
|
727
|
-
// the defaults are:
|
|
728
719
|
get: ["query", "params"],
|
|
729
720
|
post: ["body", "params", "files"],
|
|
730
721
|
put: ["body", "params"],
|
|
@@ -734,21 +725,32 @@ createConfig({
|
|
|
734
725
|
});
|
|
735
726
|
```
|
|
736
727
|
|
|
728
|
+
## Nested routes
|
|
729
|
+
|
|
730
|
+
Suppose you want to assign both `/v1/path` and `/v1/path/subpath` routes with Endpoints:
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
import { Routing } from "express-zod-api";
|
|
734
|
+
|
|
735
|
+
const routing: Routing = {
|
|
736
|
+
v1: {
|
|
737
|
+
path: endpointA.nest({
|
|
738
|
+
subpath: endpointB,
|
|
739
|
+
}),
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
```
|
|
743
|
+
|
|
737
744
|
## Route path params
|
|
738
745
|
|
|
739
|
-
You can
|
|
746
|
+
You can assign your Endpoint to a route like `/v1/user/:id` where `:id` is the path parameter:
|
|
740
747
|
|
|
741
748
|
```typescript
|
|
742
749
|
import { Routing } from "express-zod-api";
|
|
743
750
|
|
|
744
751
|
const routing: Routing = {
|
|
745
752
|
v1: {
|
|
746
|
-
user: {
|
|
747
|
-
// route path /v1/user/:id, where :id is the path param
|
|
748
|
-
":id": getUserEndpoint,
|
|
749
|
-
// use the empty string to represent /v1/user if needed:
|
|
750
|
-
// "": listAllUsersEndpoint,
|
|
751
|
-
},
|
|
753
|
+
user: { ":id": getUserEndpoint },
|
|
752
754
|
},
|
|
753
755
|
};
|
|
754
756
|
```
|
|
@@ -763,12 +765,8 @@ const getUserEndpoint = endpointsFactory.build({
|
|
|
763
765
|
// other inputs (in query):
|
|
764
766
|
withExtendedInformation: z.boolean().optional(),
|
|
765
767
|
}),
|
|
766
|
-
output: z.object({
|
|
767
|
-
|
|
768
|
-
}),
|
|
769
|
-
handler: async ({ input: { id } }) => {
|
|
770
|
-
// id is the route path param, number
|
|
771
|
-
},
|
|
768
|
+
output: z.object({}),
|
|
769
|
+
handler: async ({ input: { id } }) => ({}), // id is number,
|
|
772
770
|
});
|
|
773
771
|
```
|
|
774
772
|
|
|
@@ -821,7 +819,7 @@ import {
|
|
|
821
819
|
const yourResultHandler = new ResultHandler({
|
|
822
820
|
positive: (data) => ({
|
|
823
821
|
schema: z.object({ data }),
|
|
824
|
-
mimeType: "application/json", // optinal
|
|
822
|
+
mimeType: "application/json", // optinal or array
|
|
825
823
|
}),
|
|
826
824
|
negative: z.object({ error: z.string() }),
|
|
827
825
|
handler: ({ error, input, output, request, response, logger }) => {
|
|
@@ -904,17 +902,12 @@ const fileStreamingEndpointsFactory = new EndpointsFactory(
|
|
|
904
902
|
positive: { schema: ez.file("buffer"), mimeType: "image/*" },
|
|
905
903
|
negative: { schema: z.string(), mimeType: "text/plain" },
|
|
906
904
|
handler: ({ response, error, output }) => {
|
|
907
|
-
if (error)
|
|
908
|
-
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
if ("filename" in output) {
|
|
905
|
+
if (error) return void response.status(400).send(error.message);
|
|
906
|
+
if ("filename" in output)
|
|
912
907
|
fs.createReadStream(output.filename).pipe(
|
|
913
908
|
response.type(output.filename),
|
|
914
909
|
);
|
|
915
|
-
|
|
916
|
-
response.status(400).send("Filename is missing");
|
|
917
|
-
}
|
|
910
|
+
else response.status(400).send("Filename is missing");
|
|
918
911
|
},
|
|
919
912
|
}),
|
|
920
913
|
);
|
|
@@ -948,9 +941,7 @@ const config = createConfig({
|
|
|
948
941
|
limits: { fileSize: 51200 }, // 50 KB
|
|
949
942
|
limitError: createHttpError(413, "The file is too large"), // handled by errorHandler in config
|
|
950
943
|
beforeUpload: ({ request, logger }) => {
|
|
951
|
-
if (!canUpload(request))
|
|
952
|
-
throw createHttpError(403, "Not authorized");
|
|
953
|
-
}
|
|
944
|
+
if (!canUpload(request)) throw createHttpError(403, "Not authorized");
|
|
954
945
|
},
|
|
955
946
|
},
|
|
956
947
|
});
|
|
@@ -1094,7 +1085,7 @@ import { ResultHandler } from "express-zod-api";
|
|
|
1094
1085
|
|
|
1095
1086
|
new ResultHandler({
|
|
1096
1087
|
positive: (data) => ({
|
|
1097
|
-
|
|
1088
|
+
statusCode: [201, 202], // created or will be created
|
|
1098
1089
|
schema: z.object({ status: z.literal("created"), data }),
|
|
1099
1090
|
}),
|
|
1100
1091
|
negative: [
|
|
@@ -1103,7 +1094,7 @@ new ResultHandler({
|
|
|
1103
1094
|
schema: z.object({ status: z.literal("exists"), id: z.number().int() }),
|
|
1104
1095
|
},
|
|
1105
1096
|
{
|
|
1106
|
-
|
|
1097
|
+
statusCode: [400, 500], // validation or internal error
|
|
1107
1098
|
schema: z.object({ status: z.literal("error"), reason: z.string() }),
|
|
1108
1099
|
},
|
|
1109
1100
|
],
|
|
@@ -1283,9 +1274,7 @@ const exampleEndpoint = defaultEndpointsFactory.build({
|
|
|
1283
1274
|
.object({
|
|
1284
1275
|
id: z.number().describe("the ID of the user"),
|
|
1285
1276
|
})
|
|
1286
|
-
.example({
|
|
1287
|
-
id: 123,
|
|
1288
|
-
}),
|
|
1277
|
+
.example({ id: 123 }),
|
|
1289
1278
|
// ..., similarly for output and middlewares
|
|
1290
1279
|
});
|
|
1291
1280
|
```
|
|
@@ -1308,9 +1297,8 @@ import {
|
|
|
1308
1297
|
} from "express-zod-api";
|
|
1309
1298
|
|
|
1310
1299
|
const config = createConfig({
|
|
1311
|
-
// ..., use the simple or the advanced syntax:
|
|
1312
1300
|
tags: {
|
|
1313
|
-
users: "Everything about the users",
|
|
1301
|
+
users: "Everything about the users", // or advanced syntax:
|
|
1314
1302
|
files: {
|
|
1315
1303
|
description: "Everything about the files processing",
|
|
1316
1304
|
url: "https://example.com",
|
|
@@ -1325,8 +1313,7 @@ const taggedEndpointsFactory = new EndpointsFactory({
|
|
|
1325
1313
|
});
|
|
1326
1314
|
|
|
1327
1315
|
const exampleEndpoint = taggedEndpointsFactory.build({
|
|
1328
|
-
//
|
|
1329
|
-
tag: "users", // or tags: ["users", "files"]
|
|
1316
|
+
tag: "users", // or array ["users", "files"]
|
|
1330
1317
|
});
|
|
1331
1318
|
```
|
|
1332
1319
|
|
|
@@ -1364,12 +1351,10 @@ const ruleForClient: Producer = (
|
|
|
1364
1351
|
) => ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
1365
1352
|
|
|
1366
1353
|
new Documentation({
|
|
1367
|
-
/* config, routing, title, version */
|
|
1368
1354
|
brandHandling: { [myBrand]: ruleForDocs },
|
|
1369
1355
|
});
|
|
1370
1356
|
|
|
1371
1357
|
new Integration({
|
|
1372
|
-
/* routing */
|
|
1373
1358
|
brandHandling: { [myBrand]: ruleForClient },
|
|
1374
1359
|
});
|
|
1375
1360
|
```
|
|
@@ -1404,7 +1389,7 @@ const output = z.object({
|
|
|
1404
1389
|
});
|
|
1405
1390
|
|
|
1406
1391
|
endpointsFactory.build({
|
|
1407
|
-
|
|
1392
|
+
method,
|
|
1408
1393
|
input,
|
|
1409
1394
|
output,
|
|
1410
1395
|
handler: async (): Promise<z.input<typeof output>> => ({
|