From 7262cdb6ca6db8d74f22b5398f55c7b49cdb5792 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 7 Apr 2025 17:08:57 -0300 Subject: [PATCH 1/3] allow to load dynamic rest endpoint --- apps/meteor/app/api/server/router.ts | 160 +++++++++++++-------------- 1 file changed, 74 insertions(+), 86 deletions(-) diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index cfb0104de899f..78084181c7406 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -56,9 +56,19 @@ export class Router< [x: string]: unknown; } = NonNullable, > { - private middleware: (router: express.Router) => void = () => void 0; + // private middleware: (router: express.Router) => void = () => void 0; - constructor(readonly base: TBasePath) {} + public router; + + private innerRouter: express.Router; + + constructor(readonly base: TBasePath) { + // eslint-disable-next-line new-cap + this.router = express.Router(); + // eslint-disable-next-line new-cap + this.innerRouter = express.Router(); + this.router.use(this.base, this.innerRouter); + } public typedRoutes: Record> = {}; @@ -131,79 +141,75 @@ export class Router< > { const [middlewares, action] = splitArray(actions); - const prev = this.middleware; - this.middleware = (router: express.Router) => { - prev(router); - router[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (req, res) => { - if (options.query) { - const validatorFn = options.query; - if (typeof options.query === 'function' && !validatorFn(req.query)) { - return res.status(400).json({ - success: false, - errorType: 'error-invalid-params', - error: validatorFn.errors?.map((error: any) => error.message).join('\n '), - }); - } + this.innerRouter[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (req, res) => { + if (options.query) { + const validatorFn = options.query; + if (typeof options.query === 'function' && !validatorFn(req.query)) { + return res.status(400).json({ + success: false, + errorType: 'error-invalid-params', + error: validatorFn.errors?.map((error: any) => error.message).join('\n '), + }); } + } - if (options.body) { - const validatorFn = options.body; - if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || req.body)) { - return res.status(400).json({ - success: false, - errorType: 'error-invalid-params', - error: validatorFn.errors?.map((error: any) => error.message).join('\n '), - }); - } + if (options.body) { + const validatorFn = options.body; + if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || req.body)) { + return res.status(400).json({ + success: false, + errorType: 'error-invalid-params', + error: validatorFn.errors?.map((error: any) => error.message).join('\n '), + }); } + } - const { - body, - statusCode = 200, - headers = {}, - } = await action.apply( - { - urlParams: req.params, - queryParams: req.query, - bodyParams: (req as any).bodyParams || req.body, - request: req, - response: res, - } as any, - [req], - ); - if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) { - const responseValidatorFn = options?.response?.[statusCode]; - if (!responseValidatorFn && options.typed) { - throw new Error(`Missing response validator for endpoint ${req.method} - ${req.url} with status code ${statusCode}`); - } - if (responseValidatorFn && !responseValidatorFn(body) && options.typed) { - throw new Error( - `Invalid response for endpoint ${req.method} - ${req.url}. Error: ${responseValidatorFn.errors?.map((error: any) => error.message).join('\n ')}`, - ); - } + const { + body, + statusCode = 200, + headers = {}, + } = await action.apply( + { + urlParams: req.params, + queryParams: req.query, + bodyParams: (req as any).bodyParams || req.body, + request: req, + response: res, + } as any, + [req], + ); + if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) { + const responseValidatorFn = options?.response?.[statusCode]; + if (!responseValidatorFn && options.typed) { + throw new Error(`Missing response validator for endpoint ${req.method} - ${req.url} with status code ${statusCode}`); } + if (responseValidatorFn && !responseValidatorFn(body) && options.typed) { + throw new Error( + `Invalid response for endpoint ${req.method} - ${req.url}. Error: ${responseValidatorFn.errors?.map((error: any) => error.message).join('\n ')}`, + ); + } + } - const responseHeaders = Object.fromEntries( - Object.entries({ - ...res.header, - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache', - ...headers, - }).map(([key, value]) => [key.toLowerCase(), value]), - ); + const responseHeaders = Object.fromEntries( + Object.entries({ + ...res.header, + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + ...headers, + }).map(([key, value]) => [key.toLowerCase(), value]), + ); - res.writeHead(statusCode, responseHeaders); + res.writeHead(statusCode, responseHeaders); - if (responseHeaders['content-type']?.match(/json|javascript/) !== null) { - body !== undefined && res.write(JSON.stringify(body)); - } else { - body !== undefined && res.write(body); - } + if (responseHeaders['content-type']?.match(/json|javascript/) !== null) { + body !== undefined && res.write(JSON.stringify(body)); + } else { + body !== undefined && res.write(body); + } - res.end(); - }); - }; + res.end(); + }); this.registerTypedRoutes(method, subpath, options); return this; } @@ -281,31 +287,13 @@ export class Router< ...Object.fromEntries(Object.entries(innerRouter.typedRoutes).map(([path, routes]) => [`${this.base}${path}`, routes])), }; - const prev = this.middleware; - this.middleware = (router: express.Router) => { - prev(router); - router.use(innerRouter.router); - }; + this.innerRouter.use(innerRouter.router); } if (typeof innerRouter === 'function') { - const prev = this.middleware; - this.middleware = (router: express.Router) => { - prev(router); - router.use(innerRouter); - }; + this.innerRouter.use(innerRouter); } return this as any; } - - get router(): express.Router { - // eslint-disable-next-line new-cap - const router = express.Router(); - // eslint-disable-next-line new-cap - const innerRouter = express.Router(); - this.middleware(innerRouter); - router.use(this.base, innerRouter); - return router; - } } type Prettify = { From 6e8a971e1798df2f1d86e4295ad40b8fa6653b53 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:48:03 -0300 Subject: [PATCH 2/3] Add cs --- .changeset/serious-grapes-smell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/serious-grapes-smell.md diff --git a/.changeset/serious-grapes-smell.md b/.changeset/serious-grapes-smell.md new file mode 100644 index 0000000000000..ad30efee76014 --- /dev/null +++ b/.changeset/serious-grapes-smell.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue with dynamic API routes requiring a server restart to be operable. From 70f56395d1ea1c11c0ce5c76c8f88af32c935501 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 8 Apr 2025 08:58:21 -0300 Subject: [PATCH 3/3] review --- apps/meteor/app/api/server/router.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index 78084181c7406..514e86ecc872d 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -56,8 +56,6 @@ export class Router< [x: string]: unknown; } = NonNullable, > { - // private middleware: (router: express.Router) => void = () => void 0; - public router; private innerRouter: express.Router;