From 8ca37ca38c6534bbebb7484c2506e2d66b25ccf2 Mon Sep 17 00:00:00 2001 From: Nicola Macoir Date: Mon, 13 Jan 2025 22:00:26 +0100 Subject: [PATCH] feat(endpoint): support regex in endpoint parameters --- packages/core/src/util/endpoint.ts | 16 +++++-- packages/core/src/util/endpoint.unit.test.ts | 49 +++++++++++++++++--- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/packages/core/src/util/endpoint.ts b/packages/core/src/util/endpoint.ts index 129ebd3..eac9b12 100644 --- a/packages/core/src/util/endpoint.ts +++ b/packages/core/src/util/endpoint.ts @@ -21,14 +21,20 @@ const methods = [ export type Method = (typeof methods)[number] type ExtractRouteParams = string extends Path - ? Record +? Record +: // eslint-disable-next-line @typescript-eslint/no-unused-vars + Path extends `${infer _Start}:${infer Param}(${string})/${infer Rest}` + ? { [K in Param | keyof ExtractRouteParams]: ZodString } : // eslint-disable-next-line @typescript-eslint/no-unused-vars - Path extends `${infer _Start}:${infer Param}/${infer Rest}` - ? { [K in Param | keyof ExtractRouteParams]?: ZodString } + Path extends `${infer _Start}:${infer Param}/${infer Rest}` + ? { [K in Param | keyof ExtractRouteParams]: ZodString } : // eslint-disable-next-line @typescript-eslint/no-unused-vars - Path extends `${infer _Start}:${infer Param}` + Path extends `${infer _Start}:${infer Param}(${string})` ? { [K in Param]: ZodString } - : ZodRawShape + : // eslint-disable-next-line @typescript-eslint/no-unused-vars + Path extends `${infer _Start}:${infer Param}` + ? { [K in Param]: ZodString } + : ZodRawShape; export type InputValidationSchema = ZodObject<{ params?: ZodObject> diff --git a/packages/core/src/util/endpoint.unit.test.ts b/packages/core/src/util/endpoint.unit.test.ts index ead73c0..64efb15 100644 --- a/packages/core/src/util/endpoint.unit.test.ts +++ b/packages/core/src/util/endpoint.unit.test.ts @@ -1,13 +1,15 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it, before, after } from 'node:test' -import { endpoint, endpointToExpressHandler } from './endpoint.js' +import { endpoint, endpointToExpressHandler, get } from './endpoint.js' import z from 'zod' import { zApiOutput, apiResponse } from './apiResponse.js' -import { type Response, type Request, type NextFunction } from 'express' +import express, { type Response, type Request, type NextFunction, type Express } from 'express' import sinon from 'sinon' import { NotImplementedError, ValidationError } from '@zhttp/errors' +import supertest from 'supertest'; import { expect } from 'chai' +import { bindControllerToApp, controller } from './controller.js' const promisifyExpressHandler = async ( handler: (req: Request, res: Response, next: NextFunction) => unknown, @@ -36,14 +38,17 @@ const promisifyExpressHandler = async ( describe('endpoint', () => { // Servertime is typically included in the api response, so we have to make sure the clock doesn't tick when checking responses - let clock: sinon.SinonFakeTimers + let clock: sinon.SinonFakeTimers; + let app: Express; + before(function () { - clock = sinon.useFakeTimers() - }) + clock = sinon.useFakeTimers(); + app = express(); + }); after(function () { - clock.restore() - }) + clock.restore(); + }); // This test doesn't actually expect anything, it's about the typing of the test itself and not running into errors when defining it it('Can be defined with correct typing', async () => { @@ -146,4 +151,34 @@ describe('endpoint', () => { expect(error).to.be.instanceOf(NotImplementedError) expect(response).to.be.undefined }) + + it('Can support regex in the endpoint', async () => { + const testController = controller('testController') + .description('A controller just to test regex support in endpoints') + .endpoints([ + get('/resources/:resourceId((?!except)[a-zA-Z0-9]{6})') + .description('Should be able to get a resource by id of 6 alphanumeric chars with exception of "except"') + .input( + z.object({ + params: z.object({ + resourceId: z.string(), + }), + }), + ) + .handler(async ({ params: { resourceId } }) => { + return apiResponse({ resourceId }); + }), + ]); + + bindControllerToApp(testController, app); + + const existingEndpoint = await supertest(app).get('/resources/abc123'); + expect(existingEndpoint?.status).to.eq(200); + + const nonExistingEndpoint = await supertest(app).get('/resources/abc123toolong'); + expect(nonExistingEndpoint?.status).to.eq(404); + + const nonMatchedExceptionEndpoint = await supertest(app).get('/resources/except'); + expect(nonMatchedExceptionEndpoint?.status).to.eq(404); + }); })