Skip to content

Commit

Permalink
Merge branch 'make-v19' into try-eslint9
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinTail committed May 10, 2024
2 parents e192bf4 + 51a55ba commit 8ed4805
Show file tree
Hide file tree
Showing 61 changed files with 855 additions and 605 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ yarn-error.log
coverage
tests/**/yarn.lock
tests/**/quick-start.ts
tests/issue952/*.d.ts
88 changes: 80 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,86 @@

### v19.0.0

- Minimum supported versions:
- Node: 18.18.0 or 20.9.0,
- `zod`: 3.23.0.
- The deprecated ~~`withMeta()`~~ is removed:
- See the changes to [v18.5.0](#v1850) on details.
- Several public methods and properties exposing arrays from class instances made readonly and frozen:
- On `Endpoint`: `.getMethods()`, `.getMimeTypes()`, `.getResponses()`, `.getScopes()`, `.getTags()`,
- On `DependsOnMethod`: `.pairs`, `.siblingMethods`.
- **Breaking changes**:
- Increased minimum supported versions:
- Node: 18.18.0 or 20.9.0;
- `zod`: 3.23.0.
- Removed the deprecated ~~`withMeta()`~~ is removed (see [v18.5.0](#v1850) for details);
- Freezed the arrays returned by the following methods or exposed by properties that supposed to be readonly:
- For `Endpoint` class: `getMethods()`, `getMimeTypes()`, `getResponses()`, `getScopes()`, `getTags()`;
- For `DependsOnMethod` class: `pairs`, `siblingMethods`.
- Changed the `ServerConfig` option `server.upload.beforeUpload`:
- The assigned function now accepts `request` instead of `app` and being called only for eligible requests;
- Restricting the upload can be achieved now by throwing an error from within.
- Changed interface for `ez.raw()`: additional properties should be supplied as its argument, not via `.extend()`.
- Features:
- New configurable level `info` for built-in logger (higher than `debug`, but lower than `warn`);
- Selective parsers equipped with a child logger:
- There are 3 types of endpoints depending on their input schema: having `ez.upload()`, having `ez.raw()`, others;
- Depending on that type, only the parsers needed for certain endpoint are processed;
- This makes all requests eligible for the assigned parsers and reverts changes made in [v18.5.2](#v1852).
- Non-breaking significant changes:
- Request logging reflects the actual path instead of the configured route, and it's placed in front of parsing:
- The severity of those messaged reduced from `info` to `debug`;
- The debug messages from uploader are enabled by default when the logger level is set to `debug`;
- Specifying `rawParser` in config is no longer needed to enable the feature.
- How to migrate confidently:
- Upgrade Node to latest version of 18.x, 20.x or 22.x;
- Upgrade `zod` to its latest version of 3.x;
- Avoid mutating the readonly arrays;
- If you're using ~~`withMeta()`~~:
- Remove it and unwrap your schemas — you can use `.example()` method directly.
- If you're using `ez.raw().extend()` for additional properties:
- Supply them directly as an argument to `ez.raw()` — see the example below.
- If you're using `beforeUpload` in your config:
- Adjust the implementation according to the example below.
- If you're having `rawParser: express.raw()` in your config:
- You can now remove this line (it's the default value now), unless you're having any customizations.

```ts
import createHttpError from "http-errors";
import { createConfig } from "express-zod-api";

const before = createConfig({
server: {
upload: {
beforeUpload: ({ app, logger }) => {
app.use((req, res, next) => {
if (req.is("multipart/form-data") && !canUpload(req)) {
return next(createHttpError(403, "Not authorized"));
}
next();
});
},
},
},
});

const after = createConfig({
server: {
upload: {
beforeUpload: ({ request, logger }) => {
if (!canUpload(request)) {
throw createHttpError(403, "Not authorized");
}
},
},
},
});
```

```ts
import { z } from "zod";
import { ez } from "express-zod-api";

const before = ez.raw().extend({
pathParameter: z.string(),
});

const after = ez.raw({
pathParameter: z.string(),
});
```

## Version 18

Expand Down
45 changes: 13 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -783,16 +783,8 @@ const config = createConfig({
```

Refer to [documentation](https://www.npmjs.com/package/express-fileupload#available-options) on available options.
Some options are forced in order to ensure the correct workflow:

```json5
{
abortOnLimit: false,
parseNested: true,
logger: {}, // the configured logger, using its .debug() method
}
```

Some options are forced in order to ensure the correct workflow: `abortOnLimit: false`, `parseNested: true`, `logger`
is assigned with `.debug()` method of the configured logger, and `debug` is enabled by default.
The `limitHandler` option is replaced by the `limitError` one. You can also connect an additional middleware for
restricting the ability to upload using the `beforeUpload` option. So the configuration for the limited and restricted
upload might look this way:
Expand All @@ -805,13 +797,10 @@ const config = createConfig({
upload: {
limits: { fileSize: 51200 }, // 50 KB
limitError: createHttpError(413, "The file is too large"), // handled by errorHandler in config
beforeUpload: ({ app, logger }) => {
app.use((req, res, next) => {
if (req.is("multipart/form-data") && !canUpload(req)) {
return next(createHttpError(403, "Not authorized"));
}
next();
});
beforeUpload: ({ request, logger }) => {
if (!canUpload(request)) {
throw createHttpError(403, "Not authorized");
}
},
},
},
Expand Down Expand Up @@ -1001,26 +990,18 @@ defaultEndpointsFactory.build({
## Accepting raw data

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

```typescript
import express from "express";
import { createConfig, defaultEndpointsFactory, ez } from "express-zod-api";

const config = createConfig({
server: {
rawParser: express.raw(), // enables the feature
},
});
import { defaultEndpointsFactory, ez } from "express-zod-api";

const rawAcceptingEndpoint = defaultEndpointsFactory.build({
method: "post",
input: ez
.raw() // accepts the featured { raw: Buffer }
.extend({}), // for additional inputs, like route params, if needed
input: ez.raw({
/* the place for additional inputs, like route params, if needed */
}),
output: z.object({ length: z.number().int().nonnegative() }),
handler: async ({ input: { raw } }) => ({
length: raw.length, // raw is Buffer
Expand Down
1 change: 0 additions & 1 deletion example/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export const config = createConfig({
server: {
listen: 8090,
upload: {
debug: true,
limits: { fileSize: 51200 },
limitError: createHttpError(413, "The file is too large"), // affects uploadAvatarEndpoint
},
Expand Down
6 changes: 3 additions & 3 deletions example/endpoints/accept-raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { taggedEndpointsFactory } from "../factories";
export const rawAcceptingEndpoint = taggedEndpointsFactory.build({
method: "post",
tag: "files",
input: ez
.raw() // requires to enable rawParser option in server config
.extend({}), // additional inputs, route params for example, if needed
input: ez.raw({
/* the place for additional inputs, like route params, if needed */
}),
output: z.object({ length: z.number().int().nonnegative() }),
handler: async ({ input: { raw } }) => ({
length: raw.length, // input.raw is populated automatically when rawParser is set in config
Expand Down
2 changes: 1 addition & 1 deletion example/example.documentation.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: Example API
version: 18.5.1
version: 19.0.0-beta.2
paths:
/v1/user/retrieve:
get:
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "express-zod-api",
"version": "18.5.1",
"version": "19.0.0-beta.2",
"description": "A Typescript library to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -42,6 +42,7 @@
"install_hooks": "husky"
},
"type": "module",
"sideEffects": true,
"main": "dist/index.cjs",
"types": "dist/index.d.ts",
"module": "dist/index.js",
Expand Down Expand Up @@ -144,7 +145,7 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-unicorn": "^52.0.0",
"eslint-plugin-unicorn": "^53.0.0",
"express": "^4.18.2",
"express-fileupload": "^1.4.3",
"http-errors": "^2.0.0",
Expand Down
10 changes: 5 additions & 5 deletions src/common-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import { z } from "zod";
import { CommonConfig, InputSource, InputSources } from "./config-type";
import { InputValidationError, OutputValidationError } from "./errors";
import { AbstractLogger } from "./logger";
import { getMeta } from "./metadata";
import { metaSymbol } from "./metadata";
import { AuxMethod, Method } from "./method";
import { mimeMultipart } from "./mime";
import { contentTypes } from "./content-type";

export type FlatObject = Record<string, unknown>;

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

export const defaultInputSources: InputSources = {
Expand Down Expand Up @@ -130,7 +130,7 @@ export const getExamples = <
* */
validate?: boolean;
}): ReadonlyArray<V extends "parsed" ? z.output<T> : z.input<T>> => {
const examples = getMeta(schema, "examples") || [];
const examples = schema._def[metaSymbol]?.examples || [];
if (!validate && variant === "original") {
return examples;
}
Expand Down
26 changes: 14 additions & 12 deletions src/config-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export interface CommonConfig<TAG extends string = string> {
tags?: TagsConfig<TAG>;
}

type BeforeUpload = (params: {
request: Request;
logger: AbstractLogger;
}) => void | Promise<void>;

type UploadOptions = Pick<
fileUpload.Options,
| "createParentPath"
Expand All @@ -95,20 +100,19 @@ type UploadOptions = Pick<
* */
limitError?: Error;
/**
* @desc A code to execute before connecting the upload middleware.
* @desc It can be used to connect a middleware that restricts the ability to upload.
* @desc A handler to execute before uploading — it can be used for restrictions by throwing an error.
* @default undefined
* @example ({ app }) => { app.use( ... ); }
* @example ({ request }) => { throw createHttpError(403, "Not authorized"); }
* */
beforeUpload?: AppExtension;
beforeUpload?: BeforeUpload;
};

type CompressionOptions = Pick<
compression.CompressionOptions,
"threshold" | "level" | "strategy" | "chunkSize" | "memLevel"
>;

type AppExtension = (params: {
type BeforeRouting = (params: {
app: IRouter;
logger: AbstractLogger;
}) => void | Promise<void>;
Expand All @@ -127,21 +131,19 @@ export interface ServerConfig<TAG extends string = string>
jsonParser?: RequestHandler;
/**
* @desc Enable or configure uploads handling.
* @default false
* @default undefined
* @requires express-fileupload
* */
upload?: boolean | UploadOptions;
/**
* @desc Enable or configure response compression.
* @default false
* @default undefined
* @requires compression
*/
compression?: boolean | CompressionOptions;
/**
* @desc Enables parsing certain request payloads into raw Buffers (application/octet-stream by default)
* @desc When enabled, use ez.raw() as input schema to get input.raw in Endpoint's handler
* @default undefined
* @example express.raw()
* @desc Custom raw parser (assigns Buffer to request body)
* @default express.raw()
* @link https://expressjs.com/en/4x/api.html#express.raw
* */
rawParser?: RequestHandler;
Expand All @@ -152,7 +154,7 @@ export interface ServerConfig<TAG extends string = string>
* @default undefined
* @example ({ app }) => { app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); }
* */
beforeRouting?: AppExtension;
beforeRouting?: BeforeRouting;
};
/** @desc Enables HTTPS server as well. */
https?: {
Expand Down
7 changes: 7 additions & 0 deletions src/content-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const contentTypes = {
json: "application/json",
upload: "multipart/form-data",
raw: "application/octet-stream",
};

export type ContentType = keyof typeof contentTypes;
13 changes: 7 additions & 6 deletions src/date-in-schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { z } from "zod";
import { proprietary } from "./metadata";
import { isValidDate } from "./schema-helpers";

export const ezDateInKind = "DateIn";
export const ezDateInBrand = Symbol("DateIn");

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

return proprietary(
ezDateInKind,
schema.transform((str) => new Date(str)).pipe(z.date().refine(isValidDate)),
);
return schema
.transform((str) => new Date(str))
.pipe(z.date().refine(isValidDate))
.brand(ezDateInBrand as symbol);
};

export type DateInSchema = ReturnType<typeof dateIn>;
17 changes: 8 additions & 9 deletions src/date-out-schema.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { z } from "zod";
import { proprietary } from "./metadata";
import { isValidDate } from "./schema-helpers";

export const ezDateOutKind = "DateOut";
export const ezDateOutBrand = Symbol("DateOut");

export const dateOut = () =>
proprietary(
ezDateOutKind,
z
.date()
.refine(isValidDate)
.transform((date) => date.toISOString()),
);
z
.date()
.refine(isValidDate)
.transform((date) => date.toISOString())
.brand(ezDateOutBrand as symbol);

export type DateOutSchema = ReturnType<typeof dateOut>;
Loading

0 comments on commit 8ed4805

Please sign in to comment.