Skip to content

Commit af5ce58

Browse files
committed
feat: enhance ExpressServer with schema-aware route registration and normalization functions
1 parent 709c8b4 commit af5ce58

1 file changed

Lines changed: 92 additions & 19 deletions

File tree

adminforth/servers/express.ts

Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ async function parseExpressCookie(req): Promise<
5555
}
5656

5757
const EXPRESS_ROUTE_SCHEMA = Symbol('adminforth.express.withSchema');
58+
const EXPRESS_REGEXP_LEADING_SLASH_RE = /^\\\//;
59+
const EXPRESS_REGEXP_OPTIONAL_TRAILING_SLASH_RE = /\\\/\?\(\?=\\\/\|\$\)\$$/;
60+
const EXPRESS_REGEXP_PARAM_CAPTURE_RE = /\(\?:\(\[\^\\\/]\+\?\)\)/g;
61+
const EXPRESS_REGEXP_ESCAPED_SLASH_RE = /\\\//g;
62+
const EXPRESS_REGEXP_TRAILING_DOLLAR_RE = /\$$/;
63+
const EXPRESS_REGEXP_LEADING_CARET_RE = /^\^/;
5864

5965
type RegisteredExpressRouteSchema = IAdminForthExpressRouteSchema & {
6066
request?: AnySchemaObject;
@@ -91,6 +97,16 @@ function normalizeExpressRuntimeSchema(schema: unknown): AnySchemaObject | undef
9197
return schema as AnySchemaObject;
9298
}
9399

100+
function normalizeExpressLayerRegexpSource(regexpSource: string): string {
101+
return regexpSource
102+
.replace(EXPRESS_REGEXP_LEADING_SLASH_RE, '/')
103+
.replace(EXPRESS_REGEXP_OPTIONAL_TRAILING_SLASH_RE, '')
104+
.replace(EXPRESS_REGEXP_PARAM_CAPTURE_RE, ':param')
105+
.replace(EXPRESS_REGEXP_ESCAPED_SLASH_RE, '/')
106+
.replace(EXPRESS_REGEXP_TRAILING_DOLLAR_RE, '')
107+
.replace(EXPRESS_REGEXP_LEADING_CARET_RE, '');
108+
}
109+
94110
const respondNoServer = (title, explanation) => {
95111
return `
96112
<!DOCTYPE html>
@@ -255,7 +271,10 @@ class ExpressServer implements IExpressHttpServer {
255271
serve(app) {
256272
this.expressApp = app;
257273
this.patchSchemaAwareRouteRegistration();
258-
this.registerSchemaAwareExistingRoutes();
274+
const stack = (this.expressApp as any)?._router?.stack;
275+
if (Array.isArray(stack)) {
276+
this.registerSchemaAwareStack(stack, '');
277+
}
259278
this.setupWsServer();
260279
this.adminforth.setupEndpoints(this);
261280
this.setupOpenApiRoutes();
@@ -374,24 +393,25 @@ class ExpressServer implements IExpressHttpServer {
374393
}) as any;
375394
});
376395

377-
this.schemaAwareRouteRegistrationPatched = true;
378-
}
379-
380-
registerSchemaAwareExistingRoutes() {
381-
const stack = (this.expressApp as any)?._router?.stack;
382-
if (!Array.isArray(stack)) {
383-
return;
396+
const originalUse = this.expressApp.use?.bind(this.expressApp);
397+
if (originalUse) {
398+
this.expressApp.use = ((...args) => {
399+
const [firstArg, ...restArgs] = args;
400+
const path = typeof firstArg === 'string' || Array.isArray(firstArg)
401+
? firstArg
402+
: '';
403+
const handlers = path ? restArgs : args;
404+
405+
this.flattenHandlers(handlers).forEach((handler) => {
406+
if (Array.isArray((handler as any)?.stack)) {
407+
this.registerSchemaAwareStack((handler as any).stack, path);
408+
}
409+
});
410+
return originalUse(...args);
411+
}) as any;
384412
}
385413

386-
stack.forEach((layer) => {
387-
if (!layer.route) {
388-
return;
389-
}
390-
391-
const methods = Object.keys(layer.route.methods || {}).filter((method) => layer.route.methods[method]);
392-
const handlers = (layer.route.stack || []).map((routeLayer) => routeLayer.handle);
393-
this.registerSchemaAwareRoute(methods, layer.route.path, handlers);
394-
});
414+
this.schemaAwareRouteRegistrationPatched = true;
395415
}
396416

397417
registerSchemaAwareRoute(methods, path, handlers) {
@@ -433,8 +453,61 @@ class ExpressServer implements IExpressHttpServer {
433453
});
434454
}
435455

456+
registerSchemaAwareStack(stack, prefix) {
457+
const prefixes = this.flattenPaths(prefix);
458+
459+
stack.forEach((layer) => {
460+
if (layer.route) {
461+
const methods = Object.keys(layer.route.methods || {}).filter((method) => layer.route.methods[method]);
462+
const handlers = (layer.route.stack || []).map((routeLayer) => routeLayer.handle);
463+
this.registerSchemaAwareRoute(methods, this.combineRoutePaths(prefixes, layer.route.path), handlers);
464+
return;
465+
}
466+
467+
const nestedStack = layer.handle?.stack;
468+
if (!Array.isArray(nestedStack)) {
469+
return;
470+
}
471+
472+
const layerPath = this.extractLayerPath(layer);
473+
const nestedPrefix = this.combineRoutePaths(prefixes, layerPath);
474+
this.registerSchemaAwareStack(nestedStack, nestedPrefix);
475+
});
476+
}
477+
478+
combineRoutePaths(prefixes, paths) {
479+
return prefixes.flatMap((prefix) => this.flattenPaths(paths).map((path) => {
480+
if (!prefix) return path || '/';
481+
if (!path || path === '/') return prefix;
482+
return `${prefix.endsWith('/') ? prefix.slice(0, -1) : prefix}${path.startsWith('/') ? path : `/${path}`}`;
483+
}));
484+
}
485+
486+
extractLayerPath(layer) {
487+
if (typeof layer.path === 'string') {
488+
return layer.path;
489+
}
490+
491+
const regexpSource = layer.regexp?.source;
492+
if (typeof regexpSource !== 'string') {
493+
return '';
494+
}
495+
496+
if (layer.regexp?.fast_slash) {
497+
return '';
498+
}
499+
500+
return normalizeExpressLayerRegexpSource(regexpSource);
501+
}
502+
436503
flattenHandlers(handlers) {
437-
return handlers.flatMap((handler) => Array.isArray(handler) ? this.flattenHandlers(handler) : [handler]);
504+
return handlers.flat(Infinity);
505+
}
506+
507+
flattenPaths(paths) {
508+
const flattened = (Array.isArray(paths) ? paths : [paths]).flat(Infinity);
509+
const stringPaths = flattened.filter((path): path is string => typeof path === 'string');
510+
return stringPaths.length ? stringPaths : [''];
438511
}
439512

440513
setupOpenApiRoutes() {
@@ -590,4 +663,4 @@ class ExpressServer implements IExpressHttpServer {
590663

591664
}
592665

593-
export default ExpressServer;
666+
export default ExpressServer;

0 commit comments

Comments
 (0)