Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Selective parsers with child logger #1741

Merged
merged 32 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
585c333
Breaking: changing beforeUpload to a middleware.
RobinTail May 7, 2024
ddc7efc
The concept draft (not complete).
RobinTail May 7, 2024
0eb73dd
Ref: using app.use(path) for parsers required for the certain endpoint.
RobinTail May 7, 2024
bf4b737
Adjusting the tests.
RobinTail May 7, 2024
81ec8a6
Ref: extracting rawMover, with tests, moving test.
RobinTail May 7, 2024
9a3b21c
Avoid using parsers for OPTIONS method.
RobinTail May 7, 2024
43d8e17
Revert "Avoid using parsers for OPTIONS method."
RobinTail May 7, 2024
d1cd670
Remove todo.
RobinTail May 7, 2024
4a6f4e9
FEAT: using the request level logger on parsers and middlewares from …
RobinTail May 7, 2024
44fc73e
Reverting #1733 and cc3fff8 (partially).
RobinTail May 7, 2024
3686e3b
Ref: makeCommonEntities() returns handlers equipped with request leve…
RobinTail May 7, 2024
400f2b8
Ref: extracting createUploadMiddleware().
RobinTail May 7, 2024
186c614
Test for the new helper.
RobinTail May 7, 2024
b43484e
Moving the logger middleware to the top, combining last two .use().
RobinTail May 7, 2024
111903a
Adjusting ServerConfig: fix defaults, inline type for beforeRouting.
RobinTail May 7, 2024
2f7fb7e
REF: transforming beforeUpload into a similar type as it was before, …
RobinTail May 7, 2024
a61f511
Moving logger into the metaSymbol prop of locals.
RobinTail May 7, 2024
2da2c9f
Update src/config-type.ts
RobinTail May 7, 2024
facb055
Reducing diff.
RobinTail May 7, 2024
e7fa19a
Ref: extracting type Parsers.
RobinTail May 7, 2024
82988c7
Changelog: update for 19.0.0.
RobinTail May 8, 2024
e2b3ad8
REF: placing parsers into the app.METHOD call because app.use treats …
RobinTail May 8, 2024
0575f52
Todo for a future.
RobinTail May 8, 2024
5f3ecb3
Revert exporting ChildLoggerProvider.
RobinTail May 8, 2024
86e9fe0
REF: createUploadParsers() to take all jobs related to establish uplo…
RobinTail May 8, 2024
f5073bc
Ref: shortening constructing parsers dict.
RobinTail May 8, 2024
2125400
Changelog: notice on revert.
RobinTail May 8, 2024
8296f66
Changelog: migration advice on beforeUpload.
RobinTail May 8, 2024
5974700
Ref: reducing imports in server helpers.
RobinTail May 8, 2024
8034fbf
Ref: minor arrangement to commons made by makeCommonEntities().
RobinTail May 8, 2024
e7a7d7c
Minor: moving mocks import higher in test.
RobinTail May 8, 2024
f5ef3de
Minor: naming.
RobinTail May 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,47 @@
- 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`.
- The config option `server.upload.beforeUpload` changed:
- The assigned function now accepts `request` instead of `app` and being called only for eligible requests;
- Restricting upload can be achieved now by throwing an error from within.
- Request logging now reflects the actual path requested rather than the configured route:
- It is also placed in front of parsing.
- Featuring selective parsers with child loggers:
- There are three types of endpoints depending on their input schema: those with upload, those with raw, and others;
- Depending on the type, only the parsers needed for certain endpoint are processed;
- This reverts changes on muting uploader logs related to non-eligible requests made in v18.5.2 (all eligible now).

```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");
}
},
},
},
});
```

## Version 18

Expand Down
20 changes: 12 additions & 8 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;
RobinTail marked this conversation as resolved.
Show resolved Hide resolved
};

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,13 +131,13 @@ 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;
Expand All @@ -152,7 +156,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
34 changes: 20 additions & 14 deletions src/routing.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
import { IRouter } from "express";
import { IRouter, RequestHandler } from "express";
import { CommonConfig } from "./config-type";
import { ContentType } from "./content-type";
import { DependsOnMethod } from "./depends-on-method";
import { AbstractEndpoint } from "./endpoint";
import { AbstractLogger } from "./logger";
import { metaSymbol } from "./metadata";
import { walkRouting } from "./routing-walker";
import { ServeStatic } from "./serve-static";
import { LocalResponse } from "./server-helpers";

export interface Routing {
[SEGMENT: string]: Routing | DependsOnMethod | AbstractEndpoint | ServeStatic;
}

export type Parsers = Record<ContentType, RequestHandler[]>;

export const initRouting = ({
app,
rootLogger,
config,
routing,
parsers,
}: {
app: IRouter;
rootLogger: AbstractLogger;
config: CommonConfig;
routing: Routing;
parsers?: Parsers;
}) =>
walkRouting({
routing,
hasCors: !!config.cors,
onEndpoint: (endpoint, path, method, siblingMethods) => {
app[method](path, async (request, response) => {
const logger = config.childLoggerProvider
? await config.childLoggerProvider({ request, parent: rootLogger })
: rootLogger;
logger.info(`${request.method}: ${path}`);
await endpoint.execute({
request,
response,
logger,
config,
siblingMethods,
});
});
app[method](
path,
...(parsers?.[endpoint.getRequestType()] || []),
async (request, response: LocalResponse) =>
endpoint.execute({
request,
response,
logger: response.locals[metaSymbol]?.logger || rootLogger,
config,
siblingMethods,
}),
);
},
onStatic: (path, handler) => {
app.use(path, handler);
Expand Down
100 changes: 74 additions & 26 deletions src/server-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type fileUpload from "express-fileupload";
import { metaSymbol } from "./metadata";
import { loadPeer } from "./peer-helpers";
import { AnyResultHandlerDefinition } from "./result-handler";
import { AbstractLogger } from "./logger";
import { CommonConfig } from "./config-type";
import { ErrorRequestHandler, RequestHandler } from "express";
import { ServerConfig } from "./config-type";
import { ErrorRequestHandler, RequestHandler, Response } from "express";
import createHttpError, { isHttpError } from "http-errors";
import { lastResortHandler } from "./last-resort";
import { ResultHandlerError } from "./errors";
Expand All @@ -10,16 +13,16 @@ import { makeErrorFromAnything } from "./common-helpers";
interface HandlerCreatorParams {
errorHandler: AnyResultHandlerDefinition;
rootLogger: AbstractLogger;
getChildLogger: CommonConfig["childLoggerProvider"];
}

export type LocalResponse = Response<
unknown,
{ [metaSymbol]?: { logger: AbstractLogger } }
>;

export const createParserFailureHandler =
({
errorHandler,
rootLogger,
getChildLogger,
}: HandlerCreatorParams): ErrorRequestHandler =>
async (error, request, response, next) => {
({ errorHandler, rootLogger }: HandlerCreatorParams): ErrorRequestHandler =>
async (error, request, response: LocalResponse, next) => {
if (!error) {
return next();
}
Expand All @@ -32,26 +35,18 @@ export const createParserFailureHandler =
input: null,
output: null,
options: {},
logger: getChildLogger
? await getChildLogger({ request, parent: rootLogger })
: rootLogger,
logger: response.locals[metaSymbol]?.logger || rootLogger,
});
};

export const createNotFoundHandler =
({
errorHandler,
getChildLogger,
rootLogger,
}: HandlerCreatorParams): RequestHandler =>
async (request, response) => {
({ errorHandler, rootLogger }: HandlerCreatorParams): RequestHandler =>
async (request, response: LocalResponse) => {
const error = createHttpError(
404,
`Can not ${request.method} ${request.path}`,
);
const logger = getChildLogger
? await getChildLogger({ request, parent: rootLogger })
: rootLogger;
const logger = response.locals[metaSymbol]?.logger || rootLogger;
try {
errorHandler.handler({
request,
Expand Down Expand Up @@ -86,9 +81,62 @@ export const createUploadFailueHandler =
export const createUploadLogger = (
logger: AbstractLogger,
): Pick<Console, "log"> => ({
log: (message, ...rest) => {
if (!/not eligible/.test(message)) {
logger.debug(message, ...rest);
}
},
log: logger.debug.bind(logger),
});

export const createUploadParsers = async ({
rootLogger,
config,
}: {
rootLogger: AbstractLogger;
config: ServerConfig;
}): Promise<RequestHandler[]> => {
const uploader = await loadPeer<typeof fileUpload>("express-fileupload");
const { limitError, beforeUpload, ...options } = {
...(typeof config.server.upload === "object" && config.server.upload),
};
const parsers: RequestHandler[] = [];
parsers.push(async (request, response: LocalResponse, next) => {
const logger = response.locals[metaSymbol]?.logger || rootLogger;
try {
await beforeUpload?.({ request, logger });
} catch (error) {
return next(error);
}
uploader({
...options,
abortOnLimit: false,
parseNested: true,
logger: createUploadLogger(logger),
})(request, response, next);
});
if (limitError) {
parsers.push(createUploadFailueHandler(limitError));
}
return parsers;
};

export const moveRaw: RequestHandler = (req, {}, next) => {
if (Buffer.isBuffer(req.body)) {
req.body = { raw: req.body };
}
next();
};

/** @since v19 prints the actual path of the request, not a configured route */
export const createLoggingMiddleware =
({
rootLogger,
config,
}: {
rootLogger: AbstractLogger;
config: ServerConfig;
}): RequestHandler =>
async (request, response: LocalResponse, next) => {
const logger = config.childLoggerProvider
? await config.childLoggerProvider({ request, parent: rootLogger })
: rootLogger;
logger.info(`${request.method}: ${request.path}`);
response.locals[metaSymbol] = { logger };
next();
};
Loading