Skip to content

Commit 60de804

Browse files
committed
Merge branch 'make-v19' into try-eslint9
2 parents e192bf4 + 51a55ba commit 60de804

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+852
-577
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ yarn-error.log
66
coverage
77
tests/**/yarn.lock
88
tests/**/quick-start.ts
9+
tests/issue952/*.d.ts

Diff for: CHANGELOG.md

+80-8
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,86 @@
44

55
### v19.0.0
66

7-
- Minimum supported versions:
8-
- Node: 18.18.0 or 20.9.0,
9-
- `zod`: 3.23.0.
10-
- The deprecated ~~`withMeta()`~~ is removed:
11-
- See the changes to [v18.5.0](#v1850) on details.
12-
- Several public methods and properties exposing arrays from class instances made readonly and frozen:
13-
- On `Endpoint`: `.getMethods()`, `.getMimeTypes()`, `.getResponses()`, `.getScopes()`, `.getTags()`,
14-
- On `DependsOnMethod`: `.pairs`, `.siblingMethods`.
7+
- **Breaking changes**:
8+
- Increased minimum supported versions:
9+
- Node: 18.18.0 or 20.9.0;
10+
- `zod`: 3.23.0.
11+
- Removed the deprecated ~~`withMeta()`~~ is removed (see [v18.5.0](#v1850) for details);
12+
- Freezed the arrays returned by the following methods or exposed by properties that supposed to be readonly:
13+
- For `Endpoint` class: `getMethods()`, `getMimeTypes()`, `getResponses()`, `getScopes()`, `getTags()`;
14+
- For `DependsOnMethod` class: `pairs`, `siblingMethods`.
15+
- Changed the `ServerConfig` option `server.upload.beforeUpload`:
16+
- The assigned function now accepts `request` instead of `app` and being called only for eligible requests;
17+
- Restricting the upload can be achieved now by throwing an error from within.
18+
- Changed interface for `ez.raw()`: additional properties should be supplied as its argument, not via `.extend()`.
19+
- Features:
20+
- New configurable level `info` for built-in logger (higher than `debug`, but lower than `warn`);
21+
- Selective parsers equipped with a child logger:
22+
- There are 3 types of endpoints depending on their input schema: having `ez.upload()`, having `ez.raw()`, others;
23+
- Depending on that type, only the parsers needed for certain endpoint are processed;
24+
- This makes all requests eligible for the assigned parsers and reverts changes made in [v18.5.2](#v1852).
25+
- Non-breaking significant changes:
26+
- Request logging reflects the actual path instead of the configured route, and it's placed in front of parsing:
27+
- The severity of those messaged reduced from `info` to `debug`;
28+
- The debug messages from uploader are enabled by default when the logger level is set to `debug`;
29+
- Specifying `rawParser` in config is no longer needed to enable the feature.
30+
- How to migrate confidently:
31+
- Upgrade Node to latest version of 18.x, 20.x or 22.x;
32+
- Upgrade `zod` to its latest version of 3.x;
33+
- Avoid mutating the readonly arrays;
34+
- If you're using ~~`withMeta()`~~:
35+
- Remove it and unwrap your schemas — you can use `.example()` method directly.
36+
- If you're using `ez.raw().extend()` for additional properties:
37+
- Supply them directly as an argument to `ez.raw()` — see the example below.
38+
- If you're using `beforeUpload` in your config:
39+
- Adjust the implementation according to the example below.
40+
- If you're having `rawParser: express.raw()` in your config:
41+
- You can now remove this line (it's the default value now), unless you're having any customizations.
42+
43+
```ts
44+
import createHttpError from "http-errors";
45+
import { createConfig } from "express-zod-api";
46+
47+
const before = createConfig({
48+
server: {
49+
upload: {
50+
beforeUpload: ({ app, logger }) => {
51+
app.use((req, res, next) => {
52+
if (req.is("multipart/form-data") && !canUpload(req)) {
53+
return next(createHttpError(403, "Not authorized"));
54+
}
55+
next();
56+
});
57+
},
58+
},
59+
},
60+
});
61+
62+
const after = createConfig({
63+
server: {
64+
upload: {
65+
beforeUpload: ({ request, logger }) => {
66+
if (!canUpload(request)) {
67+
throw createHttpError(403, "Not authorized");
68+
}
69+
},
70+
},
71+
},
72+
});
73+
```
74+
75+
```ts
76+
import { z } from "zod";
77+
import { ez } from "express-zod-api";
78+
79+
const before = ez.raw().extend({
80+
pathParameter: z.string(),
81+
});
82+
83+
const after = ez.raw({
84+
pathParameter: z.string(),
85+
});
86+
```
1587

1688
## Version 18
1789

Diff for: README.md

+13-32
Original file line numberDiff line numberDiff line change
@@ -783,16 +783,8 @@ const config = createConfig({
783783
```
784784

785785
Refer to [documentation](https://www.npmjs.com/package/express-fileupload#available-options) on available options.
786-
Some options are forced in order to ensure the correct workflow:
787-
788-
```json5
789-
{
790-
abortOnLimit: false,
791-
parseNested: true,
792-
logger: {}, // the configured logger, using its .debug() method
793-
}
794-
```
795-
786+
Some options are forced in order to ensure the correct workflow: `abortOnLimit: false`, `parseNested: true`, `logger`
787+
is assigned with `.debug()` method of the configured logger, and `debug` is enabled by default.
796788
The `limitHandler` option is replaced by the `limitError` one. You can also connect an additional middleware for
797789
restricting the ability to upload using the `beforeUpload` option. So the configuration for the limited and restricted
798790
upload might look this way:
@@ -805,13 +797,10 @@ const config = createConfig({
805797
upload: {
806798
limits: { fileSize: 51200 }, // 50 KB
807799
limitError: createHttpError(413, "The file is too large"), // handled by errorHandler in config
808-
beforeUpload: ({ app, logger }) => {
809-
app.use((req, res, next) => {
810-
if (req.is("multipart/form-data") && !canUpload(req)) {
811-
return next(createHttpError(403, "Not authorized"));
812-
}
813-
next();
814-
});
800+
beforeUpload: ({ request, logger }) => {
801+
if (!canUpload(request)) {
802+
throw createHttpError(403, "Not authorized");
803+
}
815804
},
816805
},
817806
},
@@ -1001,26 +990,18 @@ defaultEndpointsFactory.build({
1001990
## Accepting raw data
1002991

1003992
Some APIs may require an endpoint to be able to accept and process raw data, such as streaming or uploading a binary
1004-
file as an entire body of request. In order to enable this feature you need to set the `rawParser` config feature to
1005-
`express.raw()`. See also its options [in Express.js documentation](https://expressjs.com/en/4x/api.html#express.raw).
1006-
The raw data is placed into `request.body.raw` property, having type `Buffer`. Then use the proprietary `ez.raw()`
1007-
schema (which is an alias for `z.object({ raw: ez.file("buffer") })`) as the input schema of your endpoint.
993+
file as an entire body of request. Use the proprietary `ez.raw()` schema as the input schema of your endpoint.
994+
The default parser in this case is `express.raw()`. You can customize it by assigning the `rawParser` option in config.
995+
The raw data is placed into `request.body.raw` property, having type `Buffer`.
1008996

1009997
```typescript
1010-
import express from "express";
1011-
import { createConfig, defaultEndpointsFactory, ez } from "express-zod-api";
1012-
1013-
const config = createConfig({
1014-
server: {
1015-
rawParser: express.raw(), // enables the feature
1016-
},
1017-
});
998+
import { defaultEndpointsFactory, ez } from "express-zod-api";
1018999

10191000
const rawAcceptingEndpoint = defaultEndpointsFactory.build({
10201001
method: "post",
1021-
input: ez
1022-
.raw() // accepts the featured { raw: Buffer }
1023-
.extend({}), // for additional inputs, like route params, if needed
1002+
input: ez.raw({
1003+
/* the place for additional inputs, like route params, if needed */
1004+
}),
10241005
output: z.object({ length: z.number().int().nonnegative() }),
10251006
handler: async ({ input: { raw } }) => ({
10261007
length: raw.length, // raw is Buffer

Diff for: example/config.ts

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export const config = createConfig({
1313
server: {
1414
listen: 8090,
1515
upload: {
16-
debug: true,
1716
limits: { fileSize: 51200 },
1817
limitError: createHttpError(413, "The file is too large"), // affects uploadAvatarEndpoint
1918
},

Diff for: example/endpoints/accept-raw.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { taggedEndpointsFactory } from "../factories";
55
export const rawAcceptingEndpoint = taggedEndpointsFactory.build({
66
method: "post",
77
tag: "files",
8-
input: ez
9-
.raw() // requires to enable rawParser option in server config
10-
.extend({}), // additional inputs, route params for example, if needed
8+
input: ez.raw({
9+
/* the place for additional inputs, like route params, if needed */
10+
}),
1111
output: z.object({ length: z.number().int().nonnegative() }),
1212
handler: async ({ input: { raw } }) => ({
1313
length: raw.length, // input.raw is populated automatically when rawParser is set in config

Diff for: example/example.documentation.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
openapi: 3.1.0
22
info:
33
title: Example API
4-
version: 18.5.1
4+
version: 19.0.0-beta.2
55
paths:
66
/v1/user/retrieve:
77
get:

Diff for: package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "express-zod-api",
3-
"version": "18.5.1",
3+
"version": "19.0.0-beta.2",
44
"description": "A Typescript library to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.",
55
"license": "MIT",
66
"repository": {
@@ -42,6 +42,7 @@
4242
"install_hooks": "husky"
4343
},
4444
"type": "module",
45+
"sideEffects": true,
4546
"main": "dist/index.cjs",
4647
"types": "dist/index.d.ts",
4748
"module": "dist/index.js",
@@ -144,7 +145,7 @@
144145
"eslint-config-prettier": "^9.0.0",
145146
"eslint-plugin-import": "^2.28.1",
146147
"eslint-plugin-prettier": "^5.0.0",
147-
"eslint-plugin-unicorn": "^52.0.0",
148+
"eslint-plugin-unicorn": "^53.0.0",
148149
"express": "^4.18.2",
149150
"express-fileupload": "^1.4.3",
150151
"http-errors": "^2.0.0",

Diff for: src/common-helpers.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import { z } from "zod";
66
import { CommonConfig, InputSource, InputSources } from "./config-type";
77
import { InputValidationError, OutputValidationError } from "./errors";
88
import { AbstractLogger } from "./logger";
9-
import { getMeta } from "./metadata";
9+
import { metaSymbol } from "./metadata";
1010
import { AuxMethod, Method } from "./method";
11-
import { mimeMultipart } from "./mime";
11+
import { contentTypes } from "./content-type";
1212

1313
export type FlatObject = Record<string, unknown>;
1414

1515
const areFilesAvailable = (request: Request): boolean => {
1616
const contentType = request.header("content-type") || "";
17-
const isMultipart = contentType.toLowerCase().startsWith(mimeMultipart);
18-
return "files" in request && isMultipart;
17+
const isUpload = contentType.toLowerCase().startsWith(contentTypes.upload);
18+
return "files" in request && isUpload;
1919
};
2020

2121
export const defaultInputSources: InputSources = {
@@ -130,7 +130,7 @@ export const getExamples = <
130130
* */
131131
validate?: boolean;
132132
}): ReadonlyArray<V extends "parsed" ? z.output<T> : z.input<T>> => {
133-
const examples = getMeta(schema, "examples") || [];
133+
const examples = schema._def[metaSymbol]?.examples || [];
134134
if (!validate && variant === "original") {
135135
return examples;
136136
}

Diff for: src/config-type.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export interface CommonConfig<TAG extends string = string> {
7575
tags?: TagsConfig<TAG>;
7676
}
7777

78+
type BeforeUpload = (params: {
79+
request: Request;
80+
logger: AbstractLogger;
81+
}) => void | Promise<void>;
82+
7883
type UploadOptions = Pick<
7984
fileUpload.Options,
8085
| "createParentPath"
@@ -95,20 +100,19 @@ type UploadOptions = Pick<
95100
* */
96101
limitError?: Error;
97102
/**
98-
* @desc A code to execute before connecting the upload middleware.
99-
* @desc It can be used to connect a middleware that restricts the ability to upload.
103+
* @desc A handler to execute before uploading — it can be used for restrictions by throwing an error.
100104
* @default undefined
101-
* @example ({ app }) => { app.use( ... ); }
105+
* @example ({ request }) => { throw createHttpError(403, "Not authorized"); }
102106
* */
103-
beforeUpload?: AppExtension;
107+
beforeUpload?: BeforeUpload;
104108
};
105109

106110
type CompressionOptions = Pick<
107111
compression.CompressionOptions,
108112
"threshold" | "level" | "strategy" | "chunkSize" | "memLevel"
109113
>;
110114

111-
type AppExtension = (params: {
115+
type BeforeRouting = (params: {
112116
app: IRouter;
113117
logger: AbstractLogger;
114118
}) => void | Promise<void>;
@@ -127,21 +131,19 @@ export interface ServerConfig<TAG extends string = string>
127131
jsonParser?: RequestHandler;
128132
/**
129133
* @desc Enable or configure uploads handling.
130-
* @default false
134+
* @default undefined
131135
* @requires express-fileupload
132136
* */
133137
upload?: boolean | UploadOptions;
134138
/**
135139
* @desc Enable or configure response compression.
136-
* @default false
140+
* @default undefined
137141
* @requires compression
138142
*/
139143
compression?: boolean | CompressionOptions;
140144
/**
141-
* @desc Enables parsing certain request payloads into raw Buffers (application/octet-stream by default)
142-
* @desc When enabled, use ez.raw() as input schema to get input.raw in Endpoint's handler
143-
* @default undefined
144-
* @example express.raw()
145+
* @desc Custom raw parser (assigns Buffer to request body)
146+
* @default express.raw()
145147
* @link https://expressjs.com/en/4x/api.html#express.raw
146148
* */
147149
rawParser?: RequestHandler;
@@ -152,7 +154,7 @@ export interface ServerConfig<TAG extends string = string>
152154
* @default undefined
153155
* @example ({ app }) => { app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); }
154156
* */
155-
beforeRouting?: AppExtension;
157+
beforeRouting?: BeforeRouting;
156158
};
157159
/** @desc Enables HTTPS server as well. */
158160
https?: {

Diff for: src/content-type.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const contentTypes = {
2+
json: "application/json",
3+
upload: "multipart/form-data",
4+
raw: "application/octet-stream",
5+
};
6+
7+
export type ContentType = keyof typeof contentTypes;

Diff for: src/date-in-schema.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { z } from "zod";
2-
import { proprietary } from "./metadata";
32
import { isValidDate } from "./schema-helpers";
43

5-
export const ezDateInKind = "DateIn";
4+
export const ezDateInBrand = Symbol("DateIn");
65

76
export const dateIn = () => {
87
const schema = z.union([
@@ -11,8 +10,10 @@ export const dateIn = () => {
1110
z.string().datetime({ local: true }),
1211
]);
1312

14-
return proprietary(
15-
ezDateInKind,
16-
schema.transform((str) => new Date(str)).pipe(z.date().refine(isValidDate)),
17-
);
13+
return schema
14+
.transform((str) => new Date(str))
15+
.pipe(z.date().refine(isValidDate))
16+
.brand(ezDateInBrand as symbol);
1817
};
18+
19+
export type DateInSchema = ReturnType<typeof dateIn>;

Diff for: src/date-out-schema.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { z } from "zod";
2-
import { proprietary } from "./metadata";
32
import { isValidDate } from "./schema-helpers";
43

5-
export const ezDateOutKind = "DateOut";
4+
export const ezDateOutBrand = Symbol("DateOut");
65

76
export const dateOut = () =>
8-
proprietary(
9-
ezDateOutKind,
10-
z
11-
.date()
12-
.refine(isValidDate)
13-
.transform((date) => date.toISOString()),
14-
);
7+
z
8+
.date()
9+
.refine(isValidDate)
10+
.transform((date) => date.toISOString())
11+
.brand(ezDateOutBrand as symbol);
12+
13+
export type DateOutSchema = ReturnType<typeof dateOut>;

0 commit comments

Comments
 (0)