diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc2a122f..94ab8b4e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Version 18 +### v18.5.2 + +- Muted uploader logs related to non-eligible requests; +- Another performance improvement. + +### v18.5.1 + +- A small performance improvement for `Integration` and `Documentation`. + ### v18.5.0 - Major update on metadata: ~~`withMeta()`~~ is no longer required, deprecated and will be removed in v19: diff --git a/example/example.documentation.yaml b/example/example.documentation.yaml index bdf3ccfeb..6a7fe135f 100644 --- a/example/example.documentation.yaml +++ b/example/example.documentation.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Example API - version: 18.5.0 + version: 18.5.2 paths: /v1/user/retrieve: get: diff --git a/package.json b/package.json index e33be54cf..544e800fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-zod-api", - "version": "18.5.0", + "version": "18.5.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": { diff --git a/src/common-helpers.ts b/src/common-helpers.ts index 2c61ea3d2..005512b7b 100644 --- a/src/common-helpers.ts +++ b/src/common-helpers.ts @@ -8,14 +8,14 @@ import { InputValidationError, OutputValidationError } from "./errors"; import { AbstractLogger } from "./logger"; import { getMeta } from "./metadata"; import { AuxMethod, Method } from "./method"; -import { mimeMultipart } from "./mime"; +import { contentTypes } from "./content-type"; export type FlatObject = Record; 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 = { diff --git a/src/content-type.ts b/src/content-type.ts new file mode 100644 index 000000000..f3091727f --- /dev/null +++ b/src/content-type.ts @@ -0,0 +1,7 @@ +export const contentTypes = { + json: "application/json", + upload: "multipart/form-data", + raw: "application/octet-stream", +}; + +export type ContentType = keyof typeof contentTypes; diff --git a/src/documentation-helpers.ts b/src/documentation-helpers.ts index fb46335dd..e2de50fd7 100644 --- a/src/documentation-helpers.ts +++ b/src/documentation-helpers.ts @@ -187,7 +187,7 @@ export const depictDiscriminatedUnion: Depicter< > = ({ schema: { options, discriminator }, next }) => { return { discriminator: { propertyName: discriminator }, - oneOf: Array.from(options.values()).map(next), + oneOf: options.map(next), }; }; @@ -683,7 +683,7 @@ export const extractObjectSchema = ( subject instanceof z.ZodUnion || subject instanceof z.ZodDiscriminatedUnion ) { - return Array.from(subject.options.values()) + return subject.options .map((option) => extractObjectSchema(option, tfError)) .reduce((acc, option) => acc.merge(option.partial()), z.object({})); } else if (subject instanceof z.ZodEffects) { @@ -828,7 +828,7 @@ export const onEach: Depicter = ({ result.type = makeNullableType(prev); } if (examples.length) { - result.examples = Array.from(examples); + result.examples = examples.slice(); } return result; }; diff --git a/src/documentation.ts b/src/documentation.ts index c10f931da..620afc8ae 100644 --- a/src/documentation.ts +++ b/src/documentation.ts @@ -67,9 +67,8 @@ interface DocumentationParams { } export class Documentation extends OpenApiBuilder { - protected lastSecuritySchemaIds: Partial> = - {}; - protected lastOperationIdSuffixes: Record = {}; + protected lastSecuritySchemaIds = new Map(); + protected lastOperationIdSuffixes = new Map(); protected makeRef( name: string, @@ -88,28 +87,27 @@ export class Documentation extends OpenApiBuilder { protected ensureUniqOperationId( path: string, method: Method, - userDefinedOperationId?: string, + userDefined?: string, ) { - if (userDefinedOperationId) { - assert( - !(userDefinedOperationId in this.lastOperationIdSuffixes), + const operationId = userDefined || makeCleanId(method, path); + let lastSuffix = this.lastOperationIdSuffixes.get(operationId); + if (lastSuffix === undefined) { + this.lastOperationIdSuffixes.set(operationId, 1); + return operationId; + } + if (userDefined) { + assert.fail( new DocumentationError({ - message: `Duplicated operationId: "${userDefinedOperationId}"`, + message: `Duplicated operationId: "${userDefined}"`, method, isResponse: false, path, }), ); - this.lastOperationIdSuffixes[userDefinedOperationId] = 1; - return userDefinedOperationId; - } - const operationId = makeCleanId(method, path); - if (operationId in this.lastOperationIdSuffixes) { - this.lastOperationIdSuffixes[operationId]++; - return `${operationId}${this.lastOperationIdSuffixes[operationId]}`; } - this.lastOperationIdSuffixes[operationId] = 1; - return operationId; + lastSuffix++; + this.lastOperationIdSuffixes.set(operationId, lastSuffix); + return `${operationId}${lastSuffix}`; } protected ensureUniqSecuritySchemaName(subject: SecuritySchemeObject) { @@ -122,11 +120,9 @@ export class Documentation extends OpenApiBuilder { return name; } } - this.lastSecuritySchemaIds[subject.type] = - (this.lastSecuritySchemaIds?.[subject.type] || 0) + 1; - return `${subject.type.toUpperCase()}_${ - this.lastSecuritySchemaIds[subject.type] - }`; + const nextId = (this.lastSecuritySchemaIds.get(subject.type) || 0) + 1; + this.lastSecuritySchemaIds.set(subject.type, nextId); + return `${subject.type.toUpperCase()}_${nextId}`; } public constructor({ diff --git a/src/endpoint.ts b/src/endpoint.ts index 04b7038ea..564b8a817 100644 --- a/src/endpoint.ts +++ b/src/endpoint.ts @@ -26,7 +26,7 @@ import { AbstractLogger } from "./logger"; import { LogicalContainer, combineContainers } from "./logical-container"; import { AuxMethod, Method } from "./method"; import { AnyMiddlewareDef } from "./middleware"; -import { mimeJson, mimeMultipart, mimeRaw } from "./mime"; +import { ContentType, contentTypes } from "./content-type"; import { AnyResultHandlerDefinition } from "./result-handler"; import { Security } from "./security"; @@ -61,6 +61,7 @@ export abstract class AbstractEndpoint { public abstract getScopes(): string[]; public abstract getTags(): string[]; public abstract getOperationId(method: Method): string | undefined; + public abstract getRequestType(): ContentType; } export class Endpoint< @@ -81,6 +82,7 @@ export class Endpoint< readonly #scopes: SCO[]; readonly #tags: TAG[]; readonly #getOperationId: (method: Method) => string | undefined; + readonly #requestType: ContentType; constructor({ methods, @@ -128,10 +130,13 @@ export class Endpoint< this.#responses = { positive: normalizeApiResponse( resultHandler.getPositiveResponse(outputSchema), - { mimeTypes: [mimeJson], statusCodes: [defaultStatusCodes.positive] }, + { + mimeTypes: [contentTypes.json], + statusCodes: [defaultStatusCodes.positive], + }, ), negative: normalizeApiResponse(resultHandler.getNegativeResponse(), { - mimeTypes: [mimeJson], + mimeTypes: [contentTypes.json], statusCodes: [defaultStatusCodes.negative], }), }; @@ -143,12 +148,13 @@ export class Endpoint< ), ); } + this.#requestType = hasUpload(inputSchema) + ? "upload" + : hasRaw(inputSchema) + ? "raw" + : "json"; this.#mimeTypes = { - input: hasUpload(inputSchema) - ? [mimeMultipart] - : hasRaw(inputSchema) - ? [mimeRaw] - : [mimeJson], + input: [contentTypes[this.#requestType]], positive: this.#responses.positive.flatMap(({ mimeTypes }) => mimeTypes), negative: this.#responses.negative.flatMap(({ mimeTypes }) => mimeTypes), }; @@ -178,6 +184,10 @@ export class Endpoint< return this.#mimeTypes[variant]; } + public override getRequestType() { + return this.#requestType; + } + public override getResponses(variant: ResponseVariant) { return this.#responses[variant]; } diff --git a/src/integration-helpers.ts b/src/integration-helpers.ts index 91fc7be28..71135a61c 100644 --- a/src/integration-helpers.ts +++ b/src/integration-helpers.ts @@ -1,5 +1,6 @@ import ts from "typescript"; import { chain, toPairs } from "ramda"; +import { Method } from "./method"; export const f = ts.factory; @@ -74,10 +75,10 @@ export const makeEmptyInitializingConstructor = ( params: ts.ParameterDeclaration[], ) => f.createConstructorDeclaration(undefined, params, f.createBlock([])); -export const makeQuotedProp = (name: string, ref: string) => +export const makeInterfaceProp = (name: string, ref: string) => f.createPropertySignature( undefined, - `"${name}"`, + name, undefined, f.createTypeReferenceNode(ref), ); @@ -205,3 +206,5 @@ export const makeObjectKeysReducer = ( initial, ], ); + +export const quoteProp = (...parts: [Method, string]) => `"${parts.join(" ")}"`; diff --git a/src/integration.ts b/src/integration.ts index b623ea7b8..2b189429b 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -10,6 +10,7 @@ import { makeConst, makeEmptyInitializingConstructor, makeIndexedPromise, + makeInterfaceProp, makeObjectKeysReducer, makeParam, makeParams, @@ -18,17 +19,17 @@ import { makePublicLiteralType, makePublicReadonlyProp, makePublicType, - makeQuotedProp, makeRecord, makeTemplateType, makeTypeParams, parametricIndexNode, protectedReadonlyModifier, + quoteProp, spacingMiddle, } from "./integration-helpers"; import { defaultSerializer, makeCleanId } from "./common-helpers"; import { Method, methods } from "./method"; -import { mimeJson } from "./mime"; +import { contentTypes } from "./content-type"; import { loadPeer } from "./peer-helpers"; import { Routing } from "./routing"; import { walkRouting } from "./routing-walker"; @@ -38,13 +39,6 @@ import type Prettier from "prettier"; type IOKind = "input" | "response" | "positive" | "negative"; -interface Registry { - [METHOD_PATH: string]: Partial> & { - isJson: boolean; - tags: string[]; - }; -} - interface IntegrationParams { routing: Routing; /** @@ -95,9 +89,15 @@ interface FormattedPrintingOptions { export class Integration { protected program: ts.Node[] = []; protected usage: Array = []; - protected registry: Registry = {}; + protected registry = new Map< + { method: Method; path: string }, + Partial> & { + isJson: boolean; + tags: string[]; + } + >(); protected paths: string[] = []; - protected aliases: Record = {}; + protected aliases = new Map(); protected ids = { pathType: f.createIdentifier("Path"), methodType: f.createIdentifier("Method"), @@ -127,14 +127,18 @@ export class Integration { exampleImplementationConst: f.createIdentifier("exampleImplementation"), clientConst: f.createIdentifier("client"), } satisfies Record; - protected interfaces: { id: ts.Identifier; kind: IOKind }[] = []; + protected interfaces: Array<{ + id: ts.Identifier; + kind: IOKind; + props: ts.PropertySignature[]; + }> = []; protected getAlias(name: string): ts.TypeReferenceNode | undefined { - return name in this.aliases ? f.createTypeReferenceNode(name) : undefined; + return this.aliases.has(name) ? f.createTypeReferenceNode(name) : undefined; } protected makeAlias(name: string, type: ts.TypeNode): ts.TypeReferenceNode { - this.aliases[name] = createTypeAlias(type, name); + this.aliases.set(name, createTypeAlias(type, name)); return this.getAlias(name)!; } @@ -208,19 +212,24 @@ export class Integration { this.program.push(createTypeAlias(genericResponse, genericResponseId)); if (method !== "options") { this.paths.push(path); - this.registry[`${method} ${path}`] = { - input: inputId, - positive: positiveResponseId, - negative: negativeResponseId, - response: genericResponseId, - isJson: endpoint.getMimeTypes("positive").includes(mimeJson), - tags: endpoint.getTags(), - }; + this.registry.set( + { method, path }, + { + input: inputId, + positive: positiveResponseId, + negative: negativeResponseId, + response: genericResponseId, + isJson: endpoint + .getMimeTypes("positive") + .includes(contentTypes.json), + tags: endpoint.getTags(), + }, + ); } }, }); - this.program.unshift(...Object.values(this.aliases)); + this.program.unshift(...this.aliases.values()); // export type Path = "/v1/user/retrieve" | ___; this.program.push(makePublicLiteralType(this.ids.pathType, this.paths)); @@ -246,33 +255,53 @@ export class Integration { this.interfaces.push({ id: this.ids.inputInterface, kind: "input", + props: [], }); if (splitResponse) { this.interfaces.push( - { id: this.ids.posResponseInterface, kind: "positive" }, - { id: this.ids.negResponseInterface, kind: "negative" }, + { id: this.ids.posResponseInterface, kind: "positive", props: [] }, + { id: this.ids.negResponseInterface, kind: "negative", props: [] }, ); } - this.interfaces.push({ id: this.ids.responseInterface, kind: "response" }); + this.interfaces.push({ + id: this.ids.responseInterface, + kind: "response", + props: [], + }); - // export interface Input ___ { "get /v1/user/retrieve": GetV1UserRetrieveInput; } - for (const { id, kind } of this.interfaces) { - this.program.push( - makePublicExtendedInterface( - id, - extenderClause, - Object.keys(this.registry) - .map((methodPath) => { - const reference = this.registry[methodPath][kind]; - return reference - ? makeQuotedProp(methodPath, reference) - : undefined; - }) - .filter( - (entry): entry is ts.PropertySignature => entry !== undefined, + // Single walk through the registry for making properties for the next three objects + const jsonEndpoints: ts.PropertyAssignment[] = []; + const endpointTags: ts.PropertyAssignment[] = []; + for (const [{ method, path }, { isJson, tags, ...rest }] of this.registry) { + const propName = quoteProp(method, path); + // "get /v1/user/retrieve": GetV1UserRetrieveInput + for (const face of this.interfaces) { + if (face.kind in rest) { + face.props.push(makeInterfaceProp(propName, rest[face.kind]!)); + } + } + if (variant !== "types") { + if (isJson) { + // "get /v1/user/retrieve": true + jsonEndpoints.push( + f.createPropertyAssignment(propName, f.createTrue()), + ); + } + // "get /v1/user/retrieve": ["users"] + endpointTags.push( + f.createPropertyAssignment( + propName, + f.createArrayLiteralExpression( + tags.map((tag) => f.createStringLiteral(tag)), ), - ), - ); + ), + ); + } + } + + // export interface Input ___ { "get /v1/user/retrieve": GetV1UserRetrieveInput; } + for (const { id, props } of this.interfaces) { + this.program.push(makePublicExtendedInterface(id, extenderClause, props)); } if (variant === "types") { @@ -284,13 +313,7 @@ export class Integration { exportModifier, makeConst( this.ids.jsonEndpointsConst, - f.createObjectLiteralExpression( - Object.keys(this.registry) - .filter((methodPath) => this.registry[methodPath].isJson) - .map((methodPath) => - f.createPropertyAssignment(`"${methodPath}"`, f.createTrue()), - ), - ), + f.createObjectLiteralExpression(jsonEndpoints), ), ); @@ -299,18 +322,7 @@ export class Integration { exportModifier, makeConst( this.ids.endpointTagsConst, - f.createObjectLiteralExpression( - Object.keys(this.registry).map((methodPath) => - f.createPropertyAssignment( - `"${methodPath}"`, - f.createArrayLiteralExpression( - this.registry[methodPath].tags.map((tag) => - f.createStringLiteral(tag), - ), - ), - ), - ), - ), + f.createObjectLiteralExpression(endpointTags), ), ); @@ -472,7 +484,7 @@ export class Integration { f.createObjectLiteralExpression([ f.createPropertyAssignment( f.createStringLiteral("Content-Type"), - f.createStringLiteral(mimeJson), + f.createStringLiteral(contentTypes.json), ), ]), undefined, diff --git a/src/method.ts b/src/method.ts index 8d0edfe80..83ff789d9 100644 --- a/src/method.ts +++ b/src/method.ts @@ -1,3 +1,13 @@ -export type Method = "get" | "post" | "put" | "delete" | "patch"; -export const methods: Method[] = ["get", "post", "put", "delete", "patch"]; -export type AuxMethod = "options"; +import type { IRouter } from "express"; + +export const methods = [ + "get", + "post", + "put", + "delete", + "patch", +] satisfies Array; + +export type Method = (typeof methods)[number]; + +export type AuxMethod = Extract; diff --git a/src/mime.ts b/src/mime.ts deleted file mode 100644 index c5f495c4d..000000000 --- a/src/mime.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const mimeJson = "application/json"; -export const mimeMultipart = "multipart/form-data"; -export const mimeRaw = "application/octet-stream"; diff --git a/src/server-helpers.ts b/src/server-helpers.ts index 5c5c91611..eebfbcf63 100644 --- a/src/server-helpers.ts +++ b/src/server-helpers.ts @@ -82,3 +82,13 @@ export const createUploadFailueHandler = } next(); }; + +export const createUploadLogger = ( + logger: AbstractLogger, +): Pick => ({ + log: (message, ...rest) => { + if (!/not eligible/.test(message)) { + logger.debug(message, ...rest); + } + }, +}); diff --git a/src/server.ts b/src/server.ts index de0b4bf37..6c326616d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -12,6 +12,7 @@ import { createNotFoundHandler, createParserFailureHandler, createUploadFailueHandler, + createUploadLogger, } from "./server-helpers"; import { getStartupLogo } from "./startup-logo"; @@ -67,7 +68,7 @@ export const createServer = async (config: ServerConfig, routing: Routing) => { ...derivedConfig, abortOnLimit: false, parseNested: true, - logger: { log: rootLogger.debug.bind(rootLogger) }, + logger: createUploadLogger(rootLogger), }), ); if (limitError) { diff --git a/src/testing.ts b/src/testing.ts index d1e128f34..78dda1420 100644 --- a/src/testing.ts +++ b/src/testing.ts @@ -3,7 +3,7 @@ import http from "node:http"; import { CommonConfig } from "./config-type"; import { AbstractEndpoint } from "./endpoint"; import { AbstractLogger } from "./logger"; -import { mimeJson } from "./mime"; +import { contentTypes } from "./content-type"; import { loadAlternativePeer } from "./peer-helpers"; /** @@ -25,7 +25,7 @@ export const makeRequestMock = >({ }) => ({ method: "GET", - header: fnMethod(() => mimeJson), + header: fnMethod(() => contentTypes.json), ...requestProps, }) as { method: string } & Record<"header", MockOverrides> & REQ; diff --git a/tests/unit/content-type.spec.ts b/tests/unit/content-type.spec.ts new file mode 100644 index 000000000..511834b83 --- /dev/null +++ b/tests/unit/content-type.spec.ts @@ -0,0 +1,12 @@ +import { contentTypes } from "../../src/content-type"; +import { describe, expect, test } from "vitest"; + +describe("contentTypes", () => { + test("should has predefined properties", () => { + expect(contentTypes).toEqual({ + json: "application/json", + upload: "multipart/form-data", + raw: "application/octet-stream", + }); + }); +}); diff --git a/tests/unit/documentation.spec.ts b/tests/unit/documentation.spec.ts index 82f7e362e..c62794568 100644 --- a/tests/unit/documentation.spec.ts +++ b/tests/unit/documentation.spec.ts @@ -11,7 +11,7 @@ import { ez, } from "../../src"; import { expectType } from "tsd"; -import { mimeJson } from "../../src/mime"; +import { contentTypes } from "../../src/content-type"; import { z } from "zod"; import { givePort } from "../helpers"; import { describe, expect, test, vi } from "vitest"; @@ -791,7 +791,7 @@ describe("Documentation", () => { const resultHandler = createResultHandler({ getPositiveResponse: (output) => ({ schema: z.object({ status: z.literal("OK"), result: output }), - mimeTypes: [mimeJson, "text/vnd.yaml"], + mimeTypes: [contentTypes.json, "text/vnd.yaml"], statusCode: 201, }), getNegativeResponse: () => ({ diff --git a/tests/unit/endpoint.spec.ts b/tests/unit/endpoint.spec.ts index d32469361..0d5d35602 100644 --- a/tests/unit/endpoint.spec.ts +++ b/tests/unit/endpoint.spec.ts @@ -365,6 +365,26 @@ describe("Endpoint", () => { ); }); + describe("getRequestType()", () => { + test.each([ + { input: z.object({}), expected: "json" }, + { input: ez.raw(), expected: "raw" }, + { input: z.object({ file: ez.upload() }), expected: "upload" }, + ])( + "should return the assigned one upon constructing", + ({ input, expected }) => { + const factory = new EndpointsFactory(defaultResultHandler); + const endpoint = factory.build({ + method: "get", + input, + output: z.object({}), + handler: vi.fn(), + }); + expect(endpoint.getRequestType()).toEqual(expected); + }, + ); + }); + describe(".getOperationId()", () => { test("should return undefined if its not defined upon creaton", () => { expect( diff --git a/tests/unit/mime.spec.ts b/tests/unit/mime.spec.ts deleted file mode 100644 index 8e6a3afa2..000000000 --- a/tests/unit/mime.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { mimeJson, mimeMultipart, mimeRaw } from "../../src/mime"; -import { describe, expect, test } from "vitest"; - -describe("Mime", () => { - test("should export predefined types", () => { - expect(mimeJson).toBe("application/json"); - expect(mimeMultipart).toBe("multipart/form-data"); - expect(mimeRaw).toBe("application/octet-stream"); - }); -}); diff --git a/tests/unit/server-helpers.spec.ts b/tests/unit/server-helpers.spec.ts index 662f1a071..ee29c7807 100644 --- a/tests/unit/server-helpers.spec.ts +++ b/tests/unit/server-helpers.spec.ts @@ -2,18 +2,23 @@ import { createNotFoundHandler, createParserFailureHandler, createUploadFailueHandler, + createUploadLogger, } from "../../src/server-helpers"; import { describe, expect, test, vi } from "vitest"; -import { createLogger, defaultResultHandler } from "../../src"; +import { defaultResultHandler } from "../../src"; import { Request, Response } from "express"; import assert from "node:assert/strict"; -import { makeRequestMock, makeResponseMock } from "../../src/testing"; +import { + makeLoggerMock, + makeRequestMock, + makeResponseMock, +} from "../../src/testing"; import createHttpError from "http-errors"; describe("Server helpers", () => { describe("createParserFailureHandler()", () => { test("the handler should call next if there is no error", () => { - const rootLogger = createLogger({ level: "silent" }); + const rootLogger = makeLoggerMock({ fnMethod: vi.fn }); const handler = createParserFailureHandler({ errorHandler: defaultResultHandler, rootLogger, @@ -33,7 +38,7 @@ describe("Server helpers", () => { const errorHandler = { ...defaultResultHandler, handler: vi.fn() }; const handler = createParserFailureHandler({ errorHandler, - rootLogger: createLogger({ level: "silent" }), + rootLogger: makeLoggerMock({ fnMethod: vi.fn }), getChildLogger: ({ parent }) => ({ ...parent, isChild: true }), }); await handler( @@ -61,7 +66,7 @@ describe("Server helpers", () => { }; const handler = createNotFoundHandler({ errorHandler, - rootLogger: createLogger({ level: "silent" }), + rootLogger: makeLoggerMock({ fnMethod: vi.fn }), getChildLogger: async ({ parent }) => ({ ...parent, isChild: true }), }); const next = vi.fn(); @@ -101,7 +106,7 @@ describe("Server helpers", () => { }); test("should call Last Resort Handler in case of ResultHandler is faulty", () => { - const rootLogger = createLogger({ level: "silent" }); + const rootLogger = makeLoggerMock({ fnMethod: vi.fn }); const errorHandler = { ...defaultResultHandler, handler: vi.fn().mockImplementation(() => assert.fail("I am faulty")), @@ -142,12 +147,8 @@ describe("Server helpers", () => { const error = new Error("Too heavy"); test.each([ - { - files: { one: { truncated: true } }, - }, - { - files: { one: [{ truncated: false }, { truncated: true }] }, - }, + { files: { one: { truncated: true } } }, + { files: { one: [{ truncated: false }, { truncated: true }] } }, ])("should handle truncated files by calling next with error %#", (req) => { const handler = createUploadFailueHandler(error); const next = vi.fn(); @@ -167,4 +168,23 @@ describe("Server helpers", () => { expect(next).toHaveBeenCalledWith(); }); }); + + describe("createUploadLogger", () => { + const rootLogger = makeLoggerMock({ fnMethod: vi.fn }); + const uploadLogger = createUploadLogger(rootLogger); + + test("should mute 'not eligible' message", () => { + uploadLogger.log( + "Express-file-upload: Request is not eligible for file upload!", + ); + expect(rootLogger.debug).not.toHaveBeenCalled(); + }); + + test("should debug other messages", () => { + uploadLogger.log("Express-file-upload: Busboy finished parsing request."); + expect(rootLogger.debug).toHaveBeenCalledWith( + "Express-file-upload: Busboy finished parsing request.", + ); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index a2d724860..c5553d82d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -796,9 +796,9 @@ integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@*", "@types/node@^20.8.4": - version "20.12.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.8.tgz#35897bf2bfe3469847ab04634636de09552e8256" - integrity sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w== + version "20.12.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.11.tgz#c4ef00d3507000d17690643278a60dc55a9dc9be" + integrity sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw== dependencies: undici-types "~5.26.4" @@ -4396,9 +4396,9 @@ tsup@^8.0.0: tree-kill "^1.2.2" tsx@^4.6.2: - version "4.9.1" - resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.9.1.tgz#e8eb1ce2766ce9067d577fba237a3cf884b229fa" - integrity sha512-CqSJaYyZ6GEqnGtPuMPQHvUwRGU6VHSVF+RDxoOmRg/XD4aF0pD973tKhoUYGQtdcoCHcSOGk34ioFaP+vYcMQ== + version "4.9.3" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.9.3.tgz#801ca18ca22b3d2f7acd89d4b888aa2425ea1302" + integrity sha512-czVbetlILiyJZI5zGlj2kw9vFiSeyra9liPD4nG+Thh4pKTi0AmMEQ8zdV/L2xbIVKrIqif4sUNrsMAOksx9Zg== dependencies: esbuild "~0.20.2" get-tsconfig "^4.7.3" @@ -4766,6 +4766,6 @@ yocto-queue@^1.0.0: integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== zod@^3.23.0: - version "3.23.6" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.6.tgz#c08a977e2255dab1fdba933651584a05fcbf19e1" - integrity sha512-RTHJlZhsRbuA8Hmp/iNL7jnfc4nZishjsanDAfEY1QpDQZCahUp3xDzl+zfweE9BklxMUcgBgS1b7Lvie/ZVwA== + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==