diff --git a/package-lock.json b/package-lock.json index 3b1ab8f..1ee8f75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "api-machine", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "api-machine", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { - "auto-oas": "^1.2.1", + "auto-oas": "^1.3.0", "cors": "^2.8.5", "express": "^5.1.0", "swagger-ui-express": "^5.0.1", - "valsan": "^2.2.0" + "valsan": "^2.3.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -39,7 +39,7 @@ "typescript": "~5.4.2" }, "peerDependencies": { - "valsan": "^2.2.0" + "valsan": "^2.3.0" } }, "node_modules/@babel/code-frame": { @@ -1649,15 +1649,15 @@ } }, "node_modules/auto-oas": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/auto-oas/-/auto-oas-1.2.1.tgz", - "integrity": "sha512-eqfKTC60L4UHebdpX8uF5zDjVkrblpUGX01O8PW7IjUSdtFRvnZ354bBW/9dE7SAKeYzj/cOZlJCN+Emea8siw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/auto-oas/-/auto-oas-1.3.0.tgz", + "integrity": "sha512-tUeiDSSjmQvAPLPphkru5p/QJvw94pUJUDcFk3G+Fy+S9hBWGGdMauR9ebZJ5s5RODcUfib6ty4NR0selGEc7g==", "license": "MIT", "dependencies": { - "valsan": "^2.1.0" + "valsan": "^2.3.0" }, "peerDependencies": { - "valsan": "^2.1.0" + "valsan": "^2.3.0" } }, "node_modules/balanced-match": { @@ -1668,9 +1668,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.28", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", - "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", + "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2135,9 +2135,9 @@ } }, "node_modules/core-js": { - "version": "3.46.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz", - "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==", + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2335,9 +2335,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.254", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", - "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", + "version": "1.5.255", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.255.tgz", + "integrity": "sha512-Z9oIp4HrFF/cZkDPMpz2XSuVpc1THDpT4dlmATFlJUIBVCy9Vap5/rIXsASP1CscBacBqhabwh8vLctqBwEerQ==", "dev": true, "license": "ISC" }, @@ -3025,9 +3025,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -6423,9 +6423,9 @@ "license": "MIT" }, "node_modules/valsan": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/valsan/-/valsan-2.2.1.tgz", - "integrity": "sha512-vJjAA1CiWTiqR4Edukepy6kSfkgipg5ZrbOqcAWKN6kowA+0mSrGbVthQfQpNa/2+G/K+JPffe/GszvmJktMBg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/valsan/-/valsan-2.3.0.tgz", + "integrity": "sha512-365JCZqUuZBISYynUJwRzqP0FkLbJsr/I07DbvRPDyDwZY28P68HQOtdiOE2BaVCnfxOfX4RQWjU5IFlue0Vrg==", "license": "MIT" }, "node_modules/vary": { diff --git a/package.json b/package.json index 0b3a890..3d4416e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "api-machine", - "version": "1.1.0", + "version": "1.2.0", "description": "api-machine", "private": "true", "typescript-template": { @@ -22,11 +22,11 @@ "publish": "npm run script -- publish" }, "dependencies": { - "auto-oas": "^1.2.1", + "auto-oas": "^1.3.0", "cors": "^2.8.5", "express": "^5.1.0", "swagger-ui-express": "^5.0.1", - "valsan": "^2.2.0" + "valsan": "^2.3.0" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -52,7 +52,7 @@ "typescript": "~5.4.2" }, "peerDependencies": { - "valsan": "^2.2.0" + "valsan": "^2.3.0" }, "repository": { "type": "git", diff --git a/src/oas/oas-endpoint-component-converter.ts b/src/oas/oas-endpoint-component-converter.ts index cb1ae87..358aee7 100644 --- a/src/oas/oas-endpoint-component-converter.ts +++ b/src/oas/oas-endpoint-component-converter.ts @@ -1,4 +1,4 @@ -import { ObjectSanitizer } from 'valsan'; +import { ObjectSanitizer, ObjectValSan, ArrayValSan } from 'valsan'; import { SchemaObject } from 'auto-oas/oas/v3.1'; import { BaseApiEndpoint } from '../router'; @@ -41,7 +41,7 @@ export class OasEndpointComponentConverter { example, }: { name: string; - sanitizer: ObjectSanitizer; + sanitizer: ObjectSanitizer | ObjectValSan | ArrayValSan; // eslint-disable-next-line @typescript-eslint/no-explicit-any example?: any; }) { diff --git a/src/oas/oas-endpoint-converter.ts b/src/oas/oas-endpoint-converter.ts index dc65e7a..3cd3d30 100644 --- a/src/oas/oas-endpoint-converter.ts +++ b/src/oas/oas-endpoint-converter.ts @@ -1,4 +1,4 @@ -import { ObjectSanitizer } from 'valsan'; +import { ObjectSanitizer, ObjectValSan } from 'valsan'; import { PathItemObject, ParameterObject, @@ -122,7 +122,7 @@ export class OasEndpointConverter { example, }: { location: 'path' | 'query' | 'header'; - sanitizer?: ObjectSanitizer; + sanitizer?: ObjectSanitizer | ObjectValSan; // eslint-disable-next-line @typescript-eslint/no-explicit-any example?: any; }) { diff --git a/src/router/endpoint.ts b/src/router/endpoint.ts index 8e70318..949545f 100644 --- a/src/router/endpoint.ts +++ b/src/router/endpoint.ts @@ -10,6 +10,7 @@ import { ObjectSanitizer } from 'valsan/object-sanitizer'; import { BaseApiRoute } from './base'; import { validateRequest } from './validation-middleware'; import { BadRequestError, HTTPError, UnprocessableEntityError } from '../error'; +import { ArrayValSan, ObjectValSan } from 'valsan'; export type ApiRequest = ExpressRequest; export type ApiResponse = ExpressResponse; @@ -33,23 +34,23 @@ export abstract class BaseApiEndpoint extends BaseApiRoute { public method: EndpointMethod = EndpointMethod.GET; public statusCode: number = 200; - public body?: ObjectSanitizer; + public body?: ObjectSanitizer | ObjectValSan; // eslint-disable-next-line @typescript-eslint/no-explicit-any public bodyExample?: any; - public query?: ObjectSanitizer; + public query?: ObjectSanitizer | ObjectValSan; // eslint-disable-next-line @typescript-eslint/no-explicit-any public queryExample?: any; - public params?: ObjectSanitizer; + public params?: ObjectSanitizer | ObjectValSan; // eslint-disable-next-line @typescript-eslint/no-explicit-any public paramsExample?: any; - public headers?: ObjectSanitizer; + public headers?: ObjectSanitizer | ObjectValSan; // eslint-disable-next-line @typescript-eslint/no-explicit-any public headersExample?: any; - public response?: ObjectSanitizer; + public response?: ObjectSanitizer | ObjectValSan | ArrayValSan; // eslint-disable-next-line @typescript-eslint/no-explicit-any public responseExample?: any; diff --git a/src/router/validation-middleware.ts b/src/router/validation-middleware.ts index cf4dc80..4b35dd4 100644 --- a/src/router/validation-middleware.ts +++ b/src/router/validation-middleware.ts @@ -1,4 +1,9 @@ -import { ObjectSanitizer } from 'valsan'; +import { + ArrayValSan, + ObjectSanitizer, + ObjectValSan, + ObjectValSanOptions, +} from 'valsan'; import { ApiRequest, BaseApiEndpoint } from './endpoint'; import { UnprocessableEntityError } from '../error'; @@ -6,8 +11,11 @@ import { UnprocessableEntityError } from '../error'; * Runs a valsan ObjectSanitizer on a value, * throws with error details if validation fails. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function runSanitizer(sanitizer: ObjectSanitizer, value: any) { +export async function runSanitizer( + sanitizer: ObjectSanitizer | ObjectValSan | ArrayValSan, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any +) { const result = await sanitizer.run(value); if (!result.success) { @@ -35,6 +43,12 @@ export async function validateRequest( continue; } + if (part === 'headers' && 'options' in sanitizer) { + ( + sanitizer.options as ObjectValSanOptions + ).allowAdditionalProperties = true; + } + const sanitized = await runSanitizer(sanitizer, request[part]); if (part === 'query') { diff --git a/test/spec/api/openapi-server.ts b/test/spec/api/openapi-server.ts index d393e89..022aab3 100644 --- a/test/spec/api/openapi-server.ts +++ b/test/spec/api/openapi-server.ts @@ -1,5 +1,3 @@ -import { ObjectSanitizer } from 'valsan/object-sanitizer'; - import { NotFoundError, RestServer } from '../../../src'; import { BaseApiRouter, @@ -12,10 +10,12 @@ import { } from '../../../src/router'; import { + ArrayValSan, ComposedValSan, EmailValidator, IntegerValidator, LengthValidator, + ObjectValSan, } from 'valsan'; import { StringToNumberValSan } from 'valsan'; import { RangeValidator } from 'valsan'; @@ -37,9 +37,11 @@ class CreateUserEndpoint extends PostEndpoint { override path = '/users'; override description = 'Creates a new user'; - override body = new ObjectSanitizer({ - name: nameValidator, - email: emailValidator, + override body = new ObjectValSan({ + schema: { + name: nameValidator, + email: emailValidator, + }, }); override bodyExample = { @@ -47,8 +49,10 @@ class CreateUserEndpoint extends PostEndpoint { email: 'john.doe@example.com', }; - override response = new ObjectSanitizer({ - id: new LengthValidator({ minLength: 1, maxLength: 50 }), + override response = new ObjectValSan({ + schema: { + id: new LengthValidator({ minLength: 1, maxLength: 50 }), + }, }); override responseExample = { @@ -73,15 +77,19 @@ class GetUserEndpoint extends GetEndpoint { }; } - override params = new ObjectSanitizer({ - id: idValidator, + override params = new ObjectValSan({ + schema: { + id: idValidator, + }, }); - override headers = new ObjectSanitizer({ - 'x-request-id': new LengthValidator({ - minLength: 5, - maxLength: 50, - }), + override headers = new ObjectValSan({ + schema: { + 'x-request-id': new LengthValidator({ + minLength: 5, + maxLength: 50, + }), + }, }); override headersExample = { @@ -94,8 +102,10 @@ class GetUserEndpoint extends GetEndpoint { email: 'john.doe@example.com', }; - override response = new ObjectSanitizer({ - id: idValidator, + override response = new ObjectValSan({ + schema: { + id: idValidator, + }, }); async handle(request: ApiRequest) { @@ -112,23 +122,29 @@ class GetUserEndpoint extends GetEndpoint { class ListUsersEndpoint extends GetEndpoint { override path = '/users'; - override query = new ObjectSanitizer({ - limit: new ComposedValSan( - [ - new StringToNumberValSan(), - new IntegerValidator(), - new RangeValidator({ min: 1, max: 100 }), - ], - { isOptional: true } - ), - name: nameValidator.copy({ isOptional: true }), - email: emailValidator.copy({ isOptional: true }), + override query = new ObjectValSan({ + schema: { + limit: new ComposedValSan( + [ + new StringToNumberValSan(), + new IntegerValidator(), + new RangeValidator({ min: 1, max: 100 }), + ], + { isOptional: true } + ), + name: nameValidator.copy({ isOptional: true }), + email: emailValidator.copy({ isOptional: true }), + }, }); - override response = new ObjectSanitizer({ - limit: new RangeValidator({ min: 1, max: 100 }), - name: nameValidator, - email: emailValidator, + override response = new ArrayValSan({ + schema: new ObjectValSan({ + schema: { + limit: new RangeValidator({ min: 1, max: 100 }), + name: nameValidator, + email: emailValidator, + }, + }), }); override responseExample = [ @@ -151,13 +167,17 @@ class ListUsersEndpoint extends GetEndpoint { class UpdateUserEndpoint extends PatchEndpoint { override path = '/users/:id'; - override params = new ObjectSanitizer({ - id: idValidator, + override params = new ObjectValSan({ + schema: { + id: idValidator, + }, }); - override body = new ObjectSanitizer({ - name: nameValidator.copy({ isOptional: true }), - email: emailValidator.copy({ isOptional: true }), + override body = new ObjectValSan({ + schema: { + name: nameValidator.copy({ isOptional: true }), + email: emailValidator.copy({ isOptional: true }), + }, }); override bodyExample = { @@ -192,8 +212,10 @@ class UpdateUserEndpoint extends PatchEndpoint { class DeleteUserEndpoint extends DeleteEndpoint { override path = '/users/:id'; - override params = new ObjectSanitizer({ - id: idValidator, + override params = new ObjectValSan({ + schema: { + id: idValidator, + }, }); override getErrors() { @@ -242,9 +264,11 @@ const postNotFoundError = new NotFoundError('Post not found'); class CreatePostEndpoint extends PostEndpoint { override path = '/posts'; - override body = new ObjectSanitizer({ - title: new LengthValidator({ minLength: 2, maxLength: 100 }), - content: new LengthValidator({ minLength: 1, maxLength: 1000 }), + override body = new ObjectValSan({ + schema: { + title: new LengthValidator({ minLength: 2, maxLength: 100 }), + content: new LengthValidator({ minLength: 1, maxLength: 1000 }), + }, }); async handle(request: ApiRequest) { @@ -258,8 +282,10 @@ class CreatePostEndpoint extends PostEndpoint { class GetPostEndpoint extends GetEndpoint { override path = '/posts/:id'; - override params = new ObjectSanitizer({ - id: new LengthValidator({ minLength: 1, maxLength: 50 }), + override params = new ObjectValSan({ + schema: { + id: new LengthValidator({ minLength: 1, maxLength: 50 }), + }, }); override getErrors() { @@ -290,13 +316,17 @@ class ListPostsEndpoint extends GetEndpoint { class UpdatePostEndpoint extends PutEndpoint { override path = '/posts/:id'; - override params = new ObjectSanitizer({ - id: new LengthValidator({ minLength: 1, maxLength: 50 }), + override params = new ObjectValSan({ + schema: { + id: new LengthValidator({ minLength: 1, maxLength: 50 }), + }, }); - override body = new ObjectSanitizer({ - title: new LengthValidator({ minLength: 2, maxLength: 100 }), - content: new LengthValidator({ minLength: 1, maxLength: 1000 }), + override body = new ObjectValSan({ + schema: { + title: new LengthValidator({ minLength: 2, maxLength: 100 }), + content: new LengthValidator({ minLength: 1, maxLength: 1000 }), + }, }); override getErrors() { @@ -323,8 +353,10 @@ class UpdatePostEndpoint extends PutEndpoint { class DeletePostEndpoint extends DeleteEndpoint { override path = '/posts/:id'; - override params = new ObjectSanitizer({ - id: new LengthValidator({ minLength: 1, maxLength: 50 }), + override params = new ObjectValSan({ + schema: { + id: new LengthValidator({ minLength: 1, maxLength: 50 }), + }, }); override getErrors() { diff --git a/test/spec/endpoint/validation.server.ts b/test/spec/endpoint/validation.server.ts index 6d90366..74c1e3f 100644 --- a/test/spec/endpoint/validation.server.ts +++ b/test/spec/endpoint/validation.server.ts @@ -12,6 +12,7 @@ import { MinValidator, ComposedValSan, TrimSanitizer, + ObjectValSan, } from 'valsan'; // eslint-disable-next-line max-len import { NameValSan } from '../../../examples/complete-example/users/name-valsan'; @@ -25,11 +26,13 @@ export class ValidationRouter extends BaseApiRouter { class TestHeadersEndpoint extends PostEndpoint { override path = '/headers'; - override headers = new ObjectSanitizer({ - 'x-custom-header': new ComposedValSan([ - new TrimSanitizer(), - new LengthValidator({ minLength: 5 }), - ]), + override headers = new ObjectValSan({ + schema: { + 'x-custom-header': new ComposedValSan([ + new TrimSanitizer(), + new LengthValidator({ minLength: 5 }), + ]), + }, }); override async handle( diff --git a/test/spec/endpoint/validation.spec.ts b/test/spec/endpoint/validation.spec.ts index 984abd1..10aa959 100644 --- a/test/spec/endpoint/validation.spec.ts +++ b/test/spec/endpoint/validation.spec.ts @@ -105,7 +105,7 @@ describe('Validation', function () { it('should sanitize query params', async function () { const response = await fetch( - baseUrl + '/query-params?age=25&search=test+search ', + baseUrl + '/query-params?search=test+search ', { method: 'GET', headers: { 'Content-Type': 'application/json' },