@emeryld/rrroutes-server 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -62,11 +62,29 @@ function createServerDebugEmitter(option) {
62
62
  }
63
63
  var keyOf = (leaf) => `${leaf.method.toUpperCase()} ${leaf.path}`;
64
64
  var CTX_SYMBOL = Symbol.for("typedLeaves.ctx");
65
+ var AFTER_HANDLER_NEXT_SYMBOL = Symbol.for("typedLeaves.afterHandlerNext");
66
+ function setAfterHandlerNext(res, value) {
67
+ res.locals[AFTER_HANDLER_NEXT_SYMBOL] = value;
68
+ }
69
+ function handlerInvokedNext(res) {
70
+ return Boolean(res.locals[AFTER_HANDLER_NEXT_SYMBOL]);
71
+ }
65
72
  function getCtx(res) {
66
73
  return res.locals[CTX_SYMBOL];
67
74
  }
68
75
  function adaptCtxMw(mw) {
69
- return (req, res, next) => mw({ req, res, next, ctx: getCtx(res) });
76
+ return (req, res, next) => {
77
+ try {
78
+ const result = mw({ req, res, next, ctx: getCtx(res) });
79
+ if (result && typeof result.then === "function") {
80
+ return result.catch((err) => next(err));
81
+ }
82
+ return result;
83
+ } catch (err) {
84
+ next(err);
85
+ return void 0;
86
+ }
87
+ };
70
88
  }
71
89
  var defaultSend = (res, data) => {
72
90
  res.json(data);
@@ -101,16 +119,16 @@ function collectRoutesFromStack(appOrRouter) {
101
119
  return result;
102
120
  }
103
121
  function createRouteServer(router, config) {
104
- const validateOutput = config?.validateOutput ?? true;
105
- const send = config?.send ?? defaultSend;
106
- const logger = config?.logger;
107
- const { emit: emitDebug, mode: debugMode } = createServerDebugEmitter(config?.debug);
122
+ const validateOutput = config.validateOutput ?? true;
123
+ const send = config.send ?? defaultSend;
124
+ const logger = config.logger;
125
+ const { emit: emitDebug, mode: debugMode } = createServerDebugEmitter(config.debug);
108
126
  const isVerboseDebug = debugMode === "complete";
109
127
  const decorateDebugEvent = (event, details) => {
110
128
  if (!isVerboseDebug || !details) return event;
111
129
  return { ...event, ...details };
112
130
  };
113
- const ctxMw = config?.buildCtx ? async (req, res, next) => {
131
+ const ctxMw = async (req, res, next) => {
114
132
  try {
115
133
  const ctx = await config.buildCtx(req, res);
116
134
  res.locals[CTX_SYMBOL] = ctx;
@@ -119,18 +137,18 @@ function createRouteServer(router, config) {
119
137
  logger?.error?.("buildCtx error", err);
120
138
  next(err);
121
139
  }
122
- } : void 0;
123
- const globalMws = (config?.global ?? []).map((mw) => adaptCtxMw(mw));
140
+ };
141
+ const globalMws = (config.global ?? []).map((mw) => adaptCtxMw(mw));
124
142
  const registered = getRegisteredRouteStore(router);
125
143
  const buildDerived = (leaf) => {
126
144
  const derived = [];
127
- const decision = config?.fromCfg?.when?.(leaf.cfg, leaf) ?? {};
145
+ const decision = config.fromCfg?.when?.(leaf.cfg, leaf) ?? {};
128
146
  const needsAuth = typeof decision.auth === "boolean" ? decision.auth : !!leaf.cfg.authenticated;
129
- if (needsAuth && config?.fromCfg?.auth) {
147
+ if (needsAuth && config.fromCfg?.auth) {
130
148
  const authMw = resolveAuth(config.fromCfg.auth, leaf);
131
149
  if (authMw) derived.push(authMw);
132
150
  }
133
- if (config?.fromCfg?.upload && Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0) {
151
+ if (config.fromCfg?.upload && Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0) {
134
152
  derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles, leaf));
135
153
  }
136
154
  return derived;
@@ -158,6 +176,13 @@ function createRouteServer(router, config) {
158
176
  let body;
159
177
  let responsePayload;
160
178
  let hasResponsePayload = false;
179
+ setAfterHandlerNext(res, false);
180
+ let handlerCalledNext = false;
181
+ const downstreamNext = (err) => {
182
+ handlerCalledNext = true;
183
+ setAfterHandlerNext(res, true);
184
+ next(err);
185
+ };
161
186
  try {
162
187
  logger?.info?.(`${methodUpper}@${path} (${requestUrl})`);
163
188
  const ctx = res.locals[CTX_SYMBOL];
@@ -184,7 +209,7 @@ function createRouteServer(router, config) {
184
209
  result = await def.handler({
185
210
  req,
186
211
  res,
187
- next,
212
+ next: downstreamNext,
188
213
  ctx,
189
214
  params,
190
215
  query,
@@ -194,12 +219,16 @@ function createRouteServer(router, config) {
194
219
  logger?.error?.("Handler error", e);
195
220
  throw e;
196
221
  }
197
- if (!res.headersSent && result !== void 0) {
198
- const out = validateOutput && leaf.cfg.outputSchema ? leaf.cfg.outputSchema.parse(result) : result;
199
- responsePayload = out;
200
- hasResponsePayload = true;
201
- logger?.verbose?.(`${methodUpper}@${path} result`, out);
202
- send(res, out);
222
+ if (!res.headersSent) {
223
+ if (result !== void 0) {
224
+ const out = validateOutput && leaf.cfg.outputSchema ? leaf.cfg.outputSchema.parse(result) : result;
225
+ responsePayload = out;
226
+ hasResponsePayload = true;
227
+ logger?.verbose?.(`${methodUpper}@${path} result`, out);
228
+ send(res, out);
229
+ } else if (!handlerCalledNext) {
230
+ next();
231
+ }
203
232
  }
204
233
  emitDebug(
205
234
  decorateDebugEvent(
@@ -238,7 +267,16 @@ function createRouteServer(router, config) {
238
267
  next(err);
239
268
  }
240
269
  };
241
- const after = (def?.after ?? []).map((mw) => adaptCtxMw(mw));
270
+ const after = (def?.after ?? []).map((mw) => {
271
+ const adapted = adaptCtxMw(mw);
272
+ return (req, res, next) => {
273
+ if (!handlerInvokedNext(res)) {
274
+ next();
275
+ return;
276
+ }
277
+ adapted(req, res, next);
278
+ };
279
+ });
242
280
  router[method](path, ...before, wrapped, ...after);
243
281
  registered.add(key);
244
282
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/routesV3.server.ts"],"sourcesContent":["export * from './routesV3.server';\n","/**\n * routesV3.server.ts\n * -----------------------------------------------------------------------------\n * Bind an Express router/app to a `finalize(...)` registry of AnyLeafs.\n * - Fully typed handlers (params/query/body/output)\n * - Zod parsing + optional output validation\n * - buildCtx runs as a middleware *first*, before all other middlewares\n * - Global, per-route, and cfg-derived middlewares (auth, uploads)\n * - Helper to warn about unimplemented routes\n * - DX helpers to use `ctx` in any middleware with proper types\n */\n\nimport type * as express from 'express';\nimport type { RequestHandler, Router } from 'express';\nimport type { ZodType } from 'zod';\nimport {\n FileField,\n HttpMethod,\n MethodCfg,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\n\nexport type { AnyLeaf } from '@emeryld/rrroutes-contract';\n\n/** Shape expected from optional logger implementations. */\nexport type LoggerLike = {\n info?: (...args: any[]) => void;\n warn?: (...args: any[]) => void;\n error?: (...args: any[]) => void;\n debug?: (...args: any[]) => void;\n verbose?: (...args: any[]) => void;\n system?: (...args: any[]) => void;\n log?: (...args: any[]) => void;\n};\n\n// Debug logging --------------------------------------------------------------\nexport type RouteServerDebugMode = 'minimal' | 'complete';\n\nexport type RouteServerDebugEvent =\n | {\n type: 'request';\n stage: 'start' | 'success' | 'error';\n method: Uppercase<HttpMethod>;\n path: string;\n url: string;\n durationMs?: number;\n params?: unknown;\n query?: unknown;\n body?: unknown;\n output?: unknown;\n error?: unknown;\n }\n | {\n type: 'register';\n method: Uppercase<HttpMethod>;\n path: string;\n };\n\nexport type RouteServerDebugLogger = (event: RouteServerDebugEvent) => void;\n\nexport type RouteServerDebugOptions =\n | boolean\n | RouteServerDebugLogger\n | RouteServerDebugMode\n | {\n enabled?: boolean;\n logger?: RouteServerDebugLogger;\n mode?: RouteServerDebugMode;\n };\n\nconst noopServerDebug: RouteServerDebugLogger = () => {};\n\nconst defaultServerDebug: RouteServerDebugLogger = (event: RouteServerDebugEvent) => {\n if (typeof console === 'undefined') return;\n const fn = console.debug ?? console.log;\n fn?.call(console, '[rrroutes-server]', event);\n};\n\ntype ServerDebugEmitter = {\n emit: RouteServerDebugLogger;\n mode: RouteServerDebugMode;\n};\n\nfunction createServerDebugEmitter(option?: RouteServerDebugOptions): ServerDebugEmitter {\n const disabled: ServerDebugEmitter = { emit: noopServerDebug, mode: 'minimal' };\n if (!option) return disabled;\n if (option === true || option === 'minimal') {\n return { emit: defaultServerDebug, mode: 'minimal' };\n }\n if (option === 'complete') {\n return { emit: defaultServerDebug, mode: 'complete' };\n }\n if (typeof option === 'function') {\n return { emit: option, mode: 'minimal' };\n }\n if (option.enabled === false) {\n return { emit: noopServerDebug, mode: option.mode ?? 'minimal' };\n }\n return {\n emit: option.logger ?? defaultServerDebug,\n mode: option.mode ?? 'minimal',\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Keys + leaf helpers (derive keys from byKey to avoid template-literal pitfalls)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Keys like \"GET /v1/foo\" that *actually* exist in the registry */\nexport type KeysOfRegistry<R extends { byKey: Record<string, AnyLeaf> }> = keyof R['byKey'] &\n string;\n\ntype MethodFromKey<K extends string> = K extends `${infer M} ${string}` ? Lowercase<M> : never;\ntype PathFromKey<K extends string> = K extends `${string} ${infer P}` ? P : never;\n\n/** Given a registry and a key, pick the exact leaf for that method+path */\nexport type LeafFromKey<R extends { all: readonly AnyLeaf[] }, K extends string> = Extract<\n R['all'][number],\n { method: MethodFromKey<K> & HttpMethod; path: PathFromKey<K> }\n>;\n\n/** Optional-ify types if your core returns `never` when a schema isn't defined */\ntype Maybe<T> = [T] extends [never] ? undefined : T;\n\n/** Typed params argument exposed to handlers. */\nexport type ArgParams<L extends AnyLeaf> = Maybe<InferParams<L>>;\n/** Typed query argument exposed to handlers. */\nexport type ArgQuery<L extends AnyLeaf> = Maybe<InferQuery<L>>;\n/** Typed body argument exposed to handlers. */\nexport type ArgBody<L extends AnyLeaf> = Maybe<InferBody<L>>;\n\n/**\n * Convenience to compute a `\"METHOD /path\"` key from a leaf.\n * @param leaf Leaf describing the route.\n * @returns Uppercase method + path key.\n */\nexport const keyOf = (leaf: AnyLeaf) => `${leaf.method.toUpperCase()} ${leaf.path}` as const;\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Context typing & DX helpers (so ctx is usable in *any* middleware)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Unique symbol used to stash ctx on res.locals.\n * (Symbols are safer than string keys against collisions.)\n */\nexport const CTX_SYMBOL: unique symbol = Symbol.for('typedLeaves.ctx');\n\n/** Response type that *has* a ctx on locals for DX in middlewares */\nexport type ResponseWithCtx<Ctx> =\n // Replace locals with an intersection that guarantees CTX_SYMBOL exists\n Omit<express.Response, 'locals'> & {\n locals: express.Response['locals'] & { [CTX_SYMBOL]: Ctx };\n };\n\n/** A middleware signature that can *use* ctx via `res.locals[CTX_SYMBOL]` */\nexport type CtxRequestHandler<Ctx> = (args: {\n req: express.Request;\n res: express.Response;\n next: express.NextFunction;\n ctx: Ctx;\n}) => any;\n\n/**\n * Safely read ctx from any Response.\n * @param res Express response whose locals contain the ctx symbol.\n * @returns Strongly typed context object.\n */\nexport function getCtx<Ctx = unknown>(res: express.Response): Ctx {\n return (res.locals as any)[CTX_SYMBOL] as Ctx;\n}\n\n/**\n * Wrap a ctx-typed middleware to a plain RequestHandler (for arrays, etc.).\n * @param mw Middleware that expects a typed response with ctx available.\n * @returns Standard Express request handler.\n */\nfunction adaptCtxMw<Ctx>(mw: CtxRequestHandler<Ctx>): RequestHandler {\n return (req, res, next) => mw({ req, res, next, ctx: getCtx<Ctx>(res) });\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Controller types — object form only (simpler, clearer typings)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Typed route handler for a specific leaf */\nexport type Handler<L extends AnyLeaf, Ctx = unknown> = (args: {\n req: express.Request;\n res: express.Response;\n next: express.NextFunction;\n ctx: Ctx;\n params: ArgParams<L>;\n query: ArgQuery<L>;\n body: ArgBody<L>;\n}) => Promise<InferOutput<L>> | InferOutput<L>;\n\n/** Route definition for one key */\nexport type RouteDef<L extends AnyLeaf, Ctx = unknown> = {\n /** Middlewares before the handler (run after buildCtx/global/derived) */\n use?: Array<CtxRequestHandler<Ctx>>;\n /** Middlewares after the handler *if* it calls next() */\n after?: Array<CtxRequestHandler<Ctx>>;\n /** Your business logic */\n handler: Handler<L, Ctx>;\n};\n\n/** Map of registry keys -> route defs */\nexport type ControllerMap<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n> = {\n [P in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, P>, Ctx>;\n};\n\nexport type PartialControllerMap<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n> = Partial<ControllerMap<R, Ctx>>;\n\n// ──────────────────────────────────────────────────────────────────────────────\n/** Options + derivation helpers */\n// ──────────────────────────────────────────────────────────────────────────────\n\nexport type RouteServerConfig<Ctx = unknown> = {\n /**\n * Build a request-scoped context. We wrap this in a middleware that runs\n * *first* (before global/derived/route middlewares), and stash it on\n * `res.locals[CTX_SYMBOL]` so *all* later middlewares can use it.\n */\n buildCtx?: (req: express.Request, res: express.Response) => Ctx | Promise<Ctx>;\n\n /**\n * Global middlewares for every bound route (run *after* buildCtx).\n * You can write them as ctx-aware middlewares for great DX.\n */\n global?: Array<CtxRequestHandler<Ctx>>;\n\n /**\n * Derive middleware from MethodCfg.\n * - `auth` runs when cfg.authenticated === true (or `when` overrides)\n * - `upload` runs when cfg.bodyFiles has entries\n */\n fromCfg?: {\n auth?: RequestHandler | ((leaf: AnyLeaf) => RequestHandler);\n when?: (cfg: MethodCfg, leaf: AnyLeaf) => { auth?: boolean } | void;\n upload?: (files: FileField[] | undefined, leaf: AnyLeaf) => RequestHandler[];\n };\n\n /** Validate handler return values with outputSchema (default: true) */\n validateOutput?: boolean;\n\n /** Custom responder (default: res.json(data)) */\n send?: (res: express.Response, data: unknown) => void;\n\n /** Optional logger hooks */\n logger?: LoggerLike;\n\n /** Optional debug logging for request lifecycle (minimal/complete). */\n debug?: RouteServerDebugOptions;\n};\n\n/** Default JSON responder (typed to avoid implicit-any diagnostics) */\nconst defaultSend: (res: express.Response, data: unknown) => void = (res, data) => {\n res.json(data as any);\n};\n\n/**\n * Normalize `auth` into a RequestHandler (avoids union-narrowing issues).\n * @param auth Static middleware or factory returning one for the current leaf.\n * @param leaf Leaf being registered.\n * @returns Request handler or undefined when no auth is required.\n */\nfunction resolveAuth(\n auth: RequestHandler | ((leaf: AnyLeaf) => RequestHandler) | undefined,\n leaf: AnyLeaf,\n): RequestHandler | undefined {\n if (!auth) return undefined;\n return (auth as (l: AnyLeaf) => RequestHandler).length === 1\n ? (auth as (l: AnyLeaf) => RequestHandler)(leaf)\n : (auth as RequestHandler);\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Core builder\n// ──────────────────────────────────────────────────────────────────────────────\n\nconst REGISTERED_ROUTES_SYMBOL = Symbol.for('routesV3.registeredRoutes');\n\ntype RegisteredRouteStore = Set<string>;\n\n/**\n * Retrieve or initialize the shared store of registered route keys.\n * @param router Express router/application that carries previously registered keys.\n * @returns Set of string keys describing registered routes.\n */\nfunction getRegisteredRouteStore(router: Router | express.Application): RegisteredRouteStore {\n const existing = (router as any)[REGISTERED_ROUTES_SYMBOL] as RegisteredRouteStore | undefined;\n if (existing) return existing;\n const store: RegisteredRouteStore = new Set();\n (router as any)[REGISTERED_ROUTES_SYMBOL] = store;\n return store;\n}\n\n/**\n * Inspect the Express layer stack to discover already-registered routes.\n * @param appOrRouter Express application or router to inspect.\n * @returns All keys in the form `\"METHOD /path\"` found on the stack.\n */\nfunction collectRoutesFromStack(appOrRouter: Router | express.Application): string[] {\n const result: string[] = [];\n const stack: any[] =\n (appOrRouter as any).stack ??\n ((appOrRouter as any)._router ? (appOrRouter as any)._router.stack : undefined) ??\n [];\n\n if (!Array.isArray(stack)) return result;\n\n for (const layer of stack) {\n const route = layer && layer.route;\n if (!route) continue;\n\n const paths = Array.isArray(route.path) ? route.path : [route.path];\n const methodEntries = Object.entries(route.methods ?? {}).filter(([, enabled]) => enabled);\n\n for (const path of paths) {\n for (const [method] of methodEntries) {\n result.push(`${method.toUpperCase()} ${path}`);\n }\n }\n }\n\n return result;\n}\n\n/** Runtime helpers returned by `createRouteServer`. */\nexport type RouteServer<Ctx = unknown> = {\n router: Router | express.Application;\n register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>): void;\n registerControllers<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n controllers: PartialControllerMap<R, Ctx>,\n ): void;\n warnMissingControllers<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n logger: { warn: (...args: any[]) => void },\n ): void;\n getRegisteredKeys(): string[];\n};\n\n/**\n * Create an Express binding helper that keeps routes and controllers in sync.\n * @param router Express router or app to register handlers on.\n * @param config Optional configuration controlling ctx building, auth, uploads, etc.\n * @returns Object with helpers to register controllers and inspect registered keys.\n */\nexport function createRouteServer<Ctx = unknown>(\n router: Router | express.Application,\n config?: RouteServerConfig<Ctx>,\n): RouteServer<Ctx> {\n const validateOutput = config?.validateOutput ?? true;\n const send = config?.send ?? defaultSend;\n const logger = config?.logger;\n const { emit: emitDebug, mode: debugMode } = createServerDebugEmitter(config?.debug);\n const isVerboseDebug = debugMode === 'complete';\n const decorateDebugEvent = <T extends RouteServerDebugEvent>(\n event: T,\n details?: Partial<RouteServerDebugEvent>,\n ): RouteServerDebugEvent => {\n if (!isVerboseDebug || !details) return event;\n return { ...event, ...details } as RouteServerDebugEvent;\n };\n\n const ctxMw: RequestHandler | undefined = config?.buildCtx\n ? async (req, res, next) => {\n try {\n const ctx = await config.buildCtx!(req, res);\n (res.locals as any)[CTX_SYMBOL] = ctx;\n next();\n } catch (err) {\n logger?.error?.('buildCtx error', err);\n next(err as any);\n }\n }\n : undefined;\n\n const globalMws = (config?.global ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n const registered = getRegisteredRouteStore(router);\n\n const buildDerived = (leaf: AnyLeaf): RequestHandler[] => {\n const derived: RequestHandler[] = [];\n const decision = config?.fromCfg?.when?.(leaf.cfg, leaf) ?? {};\n const needsAuth = typeof decision.auth === 'boolean' ? decision.auth : !!leaf.cfg.authenticated;\n\n if (needsAuth && config?.fromCfg?.auth) {\n const authMw = resolveAuth(config.fromCfg.auth, leaf);\n if (authMw) derived.push(authMw);\n }\n\n if (\n config?.fromCfg?.upload &&\n Array.isArray(leaf.cfg.bodyFiles) &&\n leaf.cfg.bodyFiles.length > 0\n ) {\n derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles, leaf));\n }\n\n return derived;\n };\n\n /** Register a single leaf/controller pair on the underlying router. */\n function register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>) {\n const method = leaf.method as HttpMethod;\n const methodUpper = method.toUpperCase() as Uppercase<HttpMethod>;\n const path = leaf.path as string;\n const key = keyOf(leaf);\n emitDebug({ type: 'register', method: methodUpper, path });\n\n const routeSpecific = (def?.use ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n const derived = buildDerived(leaf);\n const before: RequestHandler[] = [\n ...(ctxMw ? [ctxMw] : []),\n ...globalMws,\n ...derived,\n ...routeSpecific,\n ];\n\n const wrapped: RequestHandler = async (req, res, next) => {\n const requestUrl = req.originalUrl ?? path;\n const startedAt = Date.now();\n emitDebug({ type: 'request', stage: 'start', method: methodUpper, path, url: requestUrl });\n let params: ArgParams<L> | undefined;\n let query: ArgQuery<L> | undefined;\n let body: ArgBody<L> | undefined;\n let responsePayload: InferOutput<L> | undefined;\n let hasResponsePayload = false;\n\n try {\n logger?.info?.(`${methodUpper}@${path} (${requestUrl})`);\n\n const ctx = (res.locals as any)[CTX_SYMBOL] as Ctx;\n\n params = (\n leaf.cfg.paramsSchema\n ? (leaf.cfg.paramsSchema as ZodType).parse(req.params)\n : Object.keys(req.params || {}).length\n ? (req.params as any)\n : undefined\n ) as ArgParams<L>;\n\n try {\n query = leaf.cfg.querySchema\n ? (leaf.cfg.querySchema as ZodType).parse(req.query)\n : Object.keys(req.query || {}).length\n ? (req.query as any)\n : undefined;\n } catch (e) {\n logger?.error?.('Query parsing error', {\n path,\n method: methodUpper,\n error: e,\n raw: JSON.stringify(req.query),\n });\n throw e;\n }\n\n body = (\n leaf.cfg.bodySchema\n ? (leaf.cfg.bodySchema as ZodType).parse(req.body)\n : req.body !== undefined\n ? (req.body as any)\n : undefined\n ) as ArgBody<L>;\n\n logger?.verbose?.(`${methodUpper}@${path} (${requestUrl})`, {\n params,\n query,\n body,\n });\n\n let result;\n try {\n result = await def.handler({\n req,\n res,\n next,\n ctx,\n params: params as ArgParams<L>,\n query: query as ArgQuery<L>,\n body: body as ArgBody<L>,\n });\n } catch (e) {\n logger?.error?.('Handler error', e);\n throw e;\n }\n\n if (!res.headersSent && result !== undefined) {\n const out =\n validateOutput && leaf.cfg.outputSchema\n ? (leaf.cfg.outputSchema as ZodType).parse(result)\n : result;\n responsePayload = out as InferOutput<L>;\n hasResponsePayload = true;\n logger?.verbose?.(`${methodUpper}@${path} result`, out);\n send(res, out);\n }\n\n emitDebug(\n decorateDebugEvent(\n {\n type: 'request',\n stage: 'success',\n method: methodUpper,\n path,\n url: requestUrl,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? {\n params,\n query,\n body,\n ...(hasResponsePayload ? { output: responsePayload } : {}),\n }\n : undefined,\n ),\n );\n } catch (err) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'request',\n stage: 'error',\n method: methodUpper,\n path,\n url: requestUrl,\n durationMs: Date.now() - startedAt,\n error: err,\n },\n isVerboseDebug ? { params, query, body } : undefined,\n ),\n );\n logger?.error?.('Route error', err);\n next(err as any);\n }\n };\n\n const after = (def?.after ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n (router as any)[method](path, ...before, wrapped, ...after);\n registered.add(key);\n }\n\n /**\n * Register controller definitions for the provided keys.\n * @param registry Finalized registry of leaves.\n * @param controllers Partial controller map keyed by `\"METHOD /path\"`.\n */\n function registerControllers<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n >(registry: R, controllers: PartialControllerMap<R, Ctx>) {\n (Object.keys(controllers) as Array<KeysOfRegistry<R>>).forEach((key) => {\n const leaf = registry.byKey[key] as unknown as LeafFromKey<R, typeof key> | undefined;\n if (!leaf) {\n logger?.warn?.(`No leaf found for controller key: ${key}. Not registering route.`);\n return;\n }\n const def = controllers[key];\n if (!def) return;\n register(leaf as LeafFromKey<R, typeof key>, def);\n });\n }\n\n /**\n * Warn about leaves that do not have a registered controller.\n * @param registry Finalized registry of leaves.\n * @param warnLogger Logger used for warning output.\n */\n function warnMissing<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n warnLogger: { warn: (...args: any[]) => void },\n ) {\n const registeredFromStore = new Set<string>(Array.from(registered));\n if (registeredFromStore.size === 0) {\n collectRoutesFromStack(router).forEach((key) => registeredFromStore.add(key));\n }\n for (const leaf of registry.all) {\n const key = keyOf(leaf);\n if (!registeredFromStore.has(key)) {\n warnLogger.warn(`No controller registered for route: ${key}`);\n }\n }\n }\n\n return {\n router,\n register,\n registerControllers,\n warnMissingControllers: warnMissing,\n getRegisteredKeys: () => Array.from(registered),\n };\n}\n\n/**\n * Bind only the controllers that are present in the provided map.\n * @param router Express router or app.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param controllers Partial map of controllers keyed by `\"METHOD /path\"`.\n * @param config Optional route server configuration.\n * @returns The same router instance for chaining.\n */\nexport function bindExpressRoutes<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n>(\n router: Router | express.Application,\n registry: R,\n controllers: PartialControllerMap<R, Ctx>,\n config?: RouteServerConfig<Ctx>,\n) {\n const server = createRouteServer<Ctx>(router, config);\n server.registerControllers(registry, controllers);\n return router;\n}\n\n/**\n * Bind controllers for every leaf. Missing entries fail at compile time.\n * @param router Express router or app.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param controllers Complete map of controllers keyed by `\"METHOD /path\"`.\n * @param config Optional route server configuration.\n * @returns The same router instance for chaining.\n */\nexport function bindAll<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n>(\n router: Router | express.Application,\n registry: R,\n controllers: { [K in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, K>, Ctx> },\n config?: RouteServerConfig<Ctx>,\n) {\n const server = createRouteServer<Ctx>(router, config);\n server.registerControllers(registry, controllers);\n return router;\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// DX helpers\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Helper for great IntelliSense when authoring controller maps.\n * @returns Function that enforces key names while preserving partial flexibility.\n */\nexport const defineControllers =\n <R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }, Ctx = unknown>() =>\n <M extends PartialControllerMap<R, Ctx>>(m: M) =>\n m;\n\n/**\n * Wrap a plain RequestHandler as an auth factory compatible with `fromCfg.auth`.\n * @param mw Middleware invoked for any leaf that requires authentication.\n * @param _leaf Leaf metadata (ignored, but provided to match factory signature).\n * @returns Factory that ignores the leaf and returns the same middleware.\n */\nexport const asLeafAuth =\n (mw: RequestHandler) =>\n (_leaf: AnyLeaf): RequestHandler =>\n mw;\n\n/**\n * Warn about leaves that don't have controllers.\n * Call this during startup to surface missing routes.\n * @param router Express router or app to inspect.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param logger Logger where warnings are emitted.\n */\nexport function warnMissingControllers<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n>(router: Router | express.Application, registry: R, logger: { warn: (...args: any[]) => void }) {\n const registeredStore = (router as any)[REGISTERED_ROUTES_SYMBOL] as Set<string> | undefined;\n const initial = registeredStore ? Array.from(registeredStore) : collectRoutesFromStack(router);\n const registeredKeys = new Set<string>(initial);\n\n for (const leaf of registry.all) {\n const k = keyOf(leaf);\n if (!registeredKeys.has(k)) {\n logger.warn(`No controller registered for route: ${k}`);\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/routesV3.server.ts"],"sourcesContent":["export * from './routesV3.server';\n","/**\n * routesV3.server.ts\n * -----------------------------------------------------------------------------\n * Bind an Express router/app to a `finalize(...)` registry of AnyLeafs.\n * - Fully typed handlers (params/query/body/output)\n * - Zod parsing + optional output validation\n * - buildCtx runs as a middleware *first*, before all other middlewares\n * - Global, per-route, and cfg-derived middlewares (auth, uploads)\n * - Helper to warn about unimplemented routes\n * - DX helpers to use `ctx` in any middleware with proper types\n */\n\nimport type * as express from 'express';\nimport type { RequestHandler, Router } from 'express';\nimport type { ZodType } from 'zod';\nimport {\n FileField,\n HttpMethod,\n MethodCfg,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\n\nexport type { AnyLeaf } from '@emeryld/rrroutes-contract';\n\n/** Shape expected from optional logger implementations. */\nexport type LoggerLike = {\n info?: (...args: any[]) => void;\n warn?: (...args: any[]) => void;\n error?: (...args: any[]) => void;\n debug?: (...args: any[]) => void;\n verbose?: (...args: any[]) => void;\n system?: (...args: any[]) => void;\n log?: (...args: any[]) => void;\n};\n\n// Debug logging --------------------------------------------------------------\nexport type RouteServerDebugMode = 'minimal' | 'complete';\n\nexport type RouteServerDebugEvent =\n | {\n type: 'request';\n stage: 'start' | 'success' | 'error';\n method: Uppercase<HttpMethod>;\n path: string;\n url: string;\n durationMs?: number;\n params?: unknown;\n query?: unknown;\n body?: unknown;\n output?: unknown;\n error?: unknown;\n }\n | {\n type: 'register';\n method: Uppercase<HttpMethod>;\n path: string;\n };\n\nexport type RouteServerDebugLogger = (event: RouteServerDebugEvent) => void;\n\nexport type RouteServerDebugOptions =\n | boolean\n | RouteServerDebugLogger\n | RouteServerDebugMode\n | {\n enabled?: boolean;\n logger?: RouteServerDebugLogger;\n mode?: RouteServerDebugMode;\n };\n\nconst noopServerDebug: RouteServerDebugLogger = () => {};\n\nconst defaultServerDebug: RouteServerDebugLogger = (event: RouteServerDebugEvent) => {\n if (typeof console === 'undefined') return;\n const fn = console.debug ?? console.log;\n fn?.call(console, '[rrroutes-server]', event);\n};\n\ntype ServerDebugEmitter = {\n emit: RouteServerDebugLogger;\n mode: RouteServerDebugMode;\n};\n\nfunction createServerDebugEmitter(option?: RouteServerDebugOptions): ServerDebugEmitter {\n const disabled: ServerDebugEmitter = { emit: noopServerDebug, mode: 'minimal' };\n if (!option) return disabled;\n if (option === true || option === 'minimal') {\n return { emit: defaultServerDebug, mode: 'minimal' };\n }\n if (option === 'complete') {\n return { emit: defaultServerDebug, mode: 'complete' };\n }\n if (typeof option === 'function') {\n return { emit: option, mode: 'minimal' };\n }\n if (option.enabled === false) {\n return { emit: noopServerDebug, mode: option.mode ?? 'minimal' };\n }\n return {\n emit: option.logger ?? defaultServerDebug,\n mode: option.mode ?? 'minimal',\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Keys + leaf helpers (derive keys from byKey to avoid template-literal pitfalls)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Keys like \"GET /v1/foo\" that *actually* exist in the registry */\nexport type KeysOfRegistry<R extends { byKey: Record<string, AnyLeaf> }> = keyof R['byKey'] &\n string;\n\ntype MethodFromKey<K extends string> = K extends `${infer M} ${string}` ? Lowercase<M> : never;\ntype PathFromKey<K extends string> = K extends `${string} ${infer P}` ? P : never;\n\n/** Given a registry and a key, pick the exact leaf for that method+path */\nexport type LeafFromKey<R extends { all: readonly AnyLeaf[] }, K extends string> = Extract<\n R['all'][number],\n { method: MethodFromKey<K> & HttpMethod; path: PathFromKey<K> }\n>;\n\n/** Optional-ify types if your core returns `never` when a schema isn't defined */\ntype Maybe<T> = [T] extends [never] ? undefined : T;\n\n/** Typed params argument exposed to handlers. */\nexport type ArgParams<L extends AnyLeaf> = Maybe<InferParams<L>>;\n/** Typed query argument exposed to handlers. */\nexport type ArgQuery<L extends AnyLeaf> = Maybe<InferQuery<L>>;\n/** Typed body argument exposed to handlers. */\nexport type ArgBody<L extends AnyLeaf> = Maybe<InferBody<L>>;\n\n/**\n * Convenience to compute a `\"METHOD /path\"` key from a leaf.\n * @param leaf Leaf describing the route.\n * @returns Uppercase method + path key.\n */\nexport const keyOf = (leaf: AnyLeaf) => `${leaf.method.toUpperCase()} ${leaf.path}` as const;\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Context typing & DX helpers (so ctx is usable in *any* middleware)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Unique symbol used to stash ctx on res.locals.\n * (Symbols are safer than string keys against collisions.)\n */\nexport const CTX_SYMBOL: unique symbol = Symbol.for('typedLeaves.ctx');\n\nconst AFTER_HANDLER_NEXT_SYMBOL: unique symbol = Symbol.for('typedLeaves.afterHandlerNext');\n\nfunction setAfterHandlerNext(res: express.Response, value: boolean) {\n (res.locals as any)[AFTER_HANDLER_NEXT_SYMBOL] = value;\n}\n\nfunction handlerInvokedNext(res: express.Response): boolean {\n return Boolean((res.locals as any)[AFTER_HANDLER_NEXT_SYMBOL]);\n}\n\n/** Response type that *has* a ctx on locals for DX in middlewares */\nexport type ResponseWithCtx<Ctx> =\n // Replace locals with an intersection that guarantees CTX_SYMBOL exists\n Omit<express.Response, 'locals'> & {\n locals: express.Response['locals'] & { [CTX_SYMBOL]: Ctx };\n };\n\n/** A middleware signature that can *use* ctx via `res.locals[CTX_SYMBOL]` */\nexport type CtxRequestHandler<Ctx> = (args: {\n req: express.Request;\n res: express.Response;\n next: express.NextFunction;\n ctx: Ctx;\n}) => any;\n\n/**\n * Safely read ctx from any Response.\n * @param res Express response whose locals contain the ctx symbol.\n * @returns Strongly typed context object.\n */\nexport function getCtx<Ctx = unknown>(res: express.Response): Ctx {\n return (res.locals as any)[CTX_SYMBOL] as Ctx;\n}\n\n/**\n * Wrap a ctx-typed middleware to a plain RequestHandler (for arrays, etc.).\n * @param mw Middleware that expects a typed response with ctx available.\n * @returns Standard Express request handler.\n */\nfunction adaptCtxMw<Ctx>(mw: CtxRequestHandler<Ctx>): RequestHandler {\n return (req, res, next) => {\n try {\n const result = mw({ req, res, next, ctx: getCtx<Ctx>(res) });\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n return (result as Promise<unknown>).catch((err) => next(err));\n }\n return result as any;\n } catch (err) {\n next(err as any);\n return undefined;\n }\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Controller types — object form only (simpler, clearer typings)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Typed route handler for a specific leaf */\nexport type Handler<L extends AnyLeaf, Ctx = unknown> = (args: {\n req: express.Request;\n res: express.Response;\n next: express.NextFunction;\n ctx: Ctx;\n params: ArgParams<L>;\n query: ArgQuery<L>;\n body: ArgBody<L>;\n}) => Promise<InferOutput<L>> | InferOutput<L>;\n\n/** Route definition for one key */\nexport type RouteDef<L extends AnyLeaf, Ctx = unknown> = {\n /** Middlewares before the handler (run after buildCtx/global/derived) */\n use?: Array<CtxRequestHandler<Ctx>>;\n /** Middlewares after the handler *if* it calls next() */\n after?: Array<CtxRequestHandler<Ctx>>;\n /** Your business logic */\n handler: Handler<L, Ctx>;\n};\n\n/** Map of registry keys -> route defs */\nexport type ControllerMap<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n> = {\n [P in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, P>, Ctx>;\n};\n\nexport type PartialControllerMap<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n> = Partial<ControllerMap<R, Ctx>>;\n\n// ──────────────────────────────────────────────────────────────────────────────\n/** Options + derivation helpers */\n// ──────────────────────────────────────────────────────────────────────────────\n\nexport type RouteServerConfig<Ctx = unknown> = {\n /**\n * Build a request-scoped context. We wrap this in a middleware that runs\n * *first* (before global/derived/route middlewares), and stash it on\n * `res.locals[CTX_SYMBOL]` so *all* later middlewares can use it.\n */\n buildCtx: (req: express.Request, res: express.Response) => Ctx | Promise<Ctx>;\n\n /**\n * Global middlewares for every bound route (run *after* buildCtx).\n * You can write them as ctx-aware middlewares for great DX.\n */\n global?: Array<CtxRequestHandler<Ctx>>;\n\n /**\n * Derive middleware from MethodCfg.\n * - `auth` runs when cfg.authenticated === true (or `when` overrides)\n * - `upload` runs when cfg.bodyFiles has entries\n */\n fromCfg?: {\n auth?: RequestHandler | ((leaf: AnyLeaf) => RequestHandler);\n when?: (cfg: MethodCfg, leaf: AnyLeaf) => { auth?: boolean } | void;\n upload?: (files: FileField[] | undefined, leaf: AnyLeaf) => RequestHandler[];\n };\n\n /** Validate handler return values with outputSchema (default: true) */\n validateOutput?: boolean;\n\n /** Custom responder (default: res.json(data)) */\n send?: (res: express.Response, data: unknown) => void;\n\n /** Optional logger hooks */\n logger?: LoggerLike;\n\n /** Optional debug logging for request lifecycle (minimal/complete). */\n debug?: RouteServerDebugOptions;\n};\n\n/** Default JSON responder (typed to avoid implicit-any diagnostics) */\nconst defaultSend: (res: express.Response, data: unknown) => void = (res, data) => {\n res.json(data as any);\n};\n\n/**\n * Normalize `auth` into a RequestHandler (avoids union-narrowing issues).\n * @param auth Static middleware or factory returning one for the current leaf.\n * @param leaf Leaf being registered.\n * @returns Request handler or undefined when no auth is required.\n */\nfunction resolveAuth(\n auth: RequestHandler | ((leaf: AnyLeaf) => RequestHandler) | undefined,\n leaf: AnyLeaf,\n): RequestHandler | undefined {\n if (!auth) return undefined;\n return (auth as (l: AnyLeaf) => RequestHandler).length === 1\n ? (auth as (l: AnyLeaf) => RequestHandler)(leaf)\n : (auth as RequestHandler);\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Core builder\n// ──────────────────────────────────────────────────────────────────────────────\n\nconst REGISTERED_ROUTES_SYMBOL = Symbol.for('routesV3.registeredRoutes');\n\ntype RegisteredRouteStore = Set<string>;\n\n/**\n * Retrieve or initialize the shared store of registered route keys.\n * @param router Express router/application that carries previously registered keys.\n * @returns Set of string keys describing registered routes.\n */\nfunction getRegisteredRouteStore(router: Router): RegisteredRouteStore {\n const existing = (router as any)[REGISTERED_ROUTES_SYMBOL] as RegisteredRouteStore | undefined;\n if (existing) return existing;\n const store: RegisteredRouteStore = new Set();\n (router as any)[REGISTERED_ROUTES_SYMBOL] = store;\n return store;\n}\n\n/**\n * Inspect the Express layer stack to discover already-registered routes.\n * @param appOrRouter Express application or router to inspect.\n * @returns All keys in the form `\"METHOD /path\"` found on the stack.\n */\nfunction collectRoutesFromStack(appOrRouter: Router): string[] {\n const result: string[] = [];\n const stack: any[] =\n (appOrRouter as any).stack ??\n ((appOrRouter as any)._router ? (appOrRouter as any)._router.stack : undefined) ??\n [];\n\n if (!Array.isArray(stack)) return result;\n\n for (const layer of stack) {\n const route = layer && layer.route;\n if (!route) continue;\n\n const paths = Array.isArray(route.path) ? route.path : [route.path];\n const methodEntries = Object.entries(route.methods ?? {}).filter(([, enabled]) => enabled);\n\n for (const path of paths) {\n for (const [method] of methodEntries) {\n result.push(`${method.toUpperCase()} ${path}`);\n }\n }\n }\n\n return result;\n}\n\n/** Runtime helpers returned by `createRouteServer`. */\nexport type RouteServer<Ctx = unknown> = {\n router: Router;\n register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>): void;\n registerControllers<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n controllers: PartialControllerMap<R, Ctx>,\n ): void;\n warnMissingControllers<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n logger: { warn: (...args: any[]) => void },\n ): void;\n getRegisteredKeys(): string[];\n};\n\n/**\n * Create an Express binding helper that keeps routes and controllers in sync.\n * @param router Express router or app to register handlers on.\n * @param config Optional configuration controlling ctx building, auth, uploads, etc.\n * @returns Object with helpers to register controllers and inspect registered keys.\n */\nexport function createRouteServer<Ctx = unknown>(\n router: Router,\n config: RouteServerConfig<Ctx>,\n): RouteServer<Ctx> {\n const validateOutput = config.validateOutput ?? true;\n const send = config.send ?? defaultSend;\n const logger = config.logger;\n const { emit: emitDebug, mode: debugMode } = createServerDebugEmitter(config.debug);\n const isVerboseDebug = debugMode === 'complete';\n const decorateDebugEvent = <T extends RouteServerDebugEvent>(\n event: T,\n details?: Partial<RouteServerDebugEvent>,\n ): RouteServerDebugEvent => {\n if (!isVerboseDebug || !details) return event;\n return { ...event, ...details } as RouteServerDebugEvent;\n };\n\n const ctxMw: RequestHandler | undefined = async (req, res, next) => {\n try {\n const ctx = await config.buildCtx!(req, res);\n (res.locals as any)[CTX_SYMBOL] = ctx;\n next();\n } catch (err) {\n logger?.error?.('buildCtx error', err);\n next(err as any);\n }\n };\n\n const globalMws = (config.global ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n const registered = getRegisteredRouteStore(router);\n\n const buildDerived = (leaf: AnyLeaf): RequestHandler[] => {\n const derived: RequestHandler[] = [];\n const decision = config.fromCfg?.when?.(leaf.cfg, leaf) ?? {};\n const needsAuth = typeof decision.auth === 'boolean' ? decision.auth : !!leaf.cfg.authenticated;\n\n if (needsAuth && config.fromCfg?.auth) {\n const authMw = resolveAuth(config.fromCfg.auth, leaf);\n if (authMw) derived.push(authMw);\n }\n\n if (\n config.fromCfg?.upload &&\n Array.isArray(leaf.cfg.bodyFiles) &&\n leaf.cfg.bodyFiles.length > 0\n ) {\n derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles, leaf));\n }\n\n return derived;\n };\n\n /** Register a single leaf/controller pair on the underlying router. */\n function register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>) {\n const method = leaf.method as HttpMethod;\n const methodUpper = method.toUpperCase() as Uppercase<HttpMethod>;\n const path = leaf.path as string;\n const key = keyOf(leaf);\n emitDebug({ type: 'register', method: methodUpper, path });\n\n const routeSpecific = (def?.use ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n const derived = buildDerived(leaf);\n const before: RequestHandler[] = [\n ...(ctxMw ? [ctxMw] : []),\n ...globalMws,\n ...derived,\n ...routeSpecific,\n ];\n\n const wrapped: RequestHandler = async (req, res, next) => {\n const requestUrl = req.originalUrl ?? path;\n const startedAt = Date.now();\n emitDebug({ type: 'request', stage: 'start', method: methodUpper, path, url: requestUrl });\n let params: ArgParams<L> | undefined;\n let query: ArgQuery<L> | undefined;\n let body: ArgBody<L> | undefined;\n let responsePayload: InferOutput<L> | undefined;\n let hasResponsePayload = false;\n setAfterHandlerNext(res, false);\n let handlerCalledNext = false;\n const downstreamNext: express.NextFunction = (err?: any) => {\n handlerCalledNext = true;\n setAfterHandlerNext(res, true);\n next(err);\n };\n\n try {\n logger?.info?.(`${methodUpper}@${path} (${requestUrl})`);\n\n const ctx = (res.locals as any)[CTX_SYMBOL] as Ctx;\n\n params = (\n leaf.cfg.paramsSchema\n ? (leaf.cfg.paramsSchema as ZodType).parse(req.params)\n : Object.keys(req.params || {}).length\n ? (req.params as any)\n : undefined\n ) as ArgParams<L>;\n\n try {\n query = leaf.cfg.querySchema\n ? (leaf.cfg.querySchema as ZodType).parse(req.query)\n : Object.keys(req.query || {}).length\n ? (req.query as any)\n : undefined;\n } catch (e) {\n logger?.error?.('Query parsing error', {\n path,\n method: methodUpper,\n error: e,\n raw: JSON.stringify(req.query),\n });\n throw e;\n }\n\n body = (\n leaf.cfg.bodySchema\n ? (leaf.cfg.bodySchema as ZodType).parse(req.body)\n : req.body !== undefined\n ? (req.body as any)\n : undefined\n ) as ArgBody<L>;\n\n logger?.verbose?.(`${methodUpper}@${path} (${requestUrl})`, {\n params,\n query,\n body,\n });\n\n let result;\n try {\n result = await def.handler({\n req,\n res,\n next: downstreamNext,\n ctx,\n params: params as ArgParams<L>,\n query: query as ArgQuery<L>,\n body: body as ArgBody<L>,\n });\n } catch (e) {\n logger?.error?.('Handler error', e);\n throw e;\n }\n\n if (!res.headersSent) {\n if (result !== undefined) {\n const out =\n validateOutput && leaf.cfg.outputSchema\n ? (leaf.cfg.outputSchema as ZodType).parse(result)\n : result;\n responsePayload = out as InferOutput<L>;\n hasResponsePayload = true;\n logger?.verbose?.(`${methodUpper}@${path} result`, out);\n send(res, out);\n } else if (!handlerCalledNext) {\n next();\n }\n }\n\n emitDebug(\n decorateDebugEvent(\n {\n type: 'request',\n stage: 'success',\n method: methodUpper,\n path,\n url: requestUrl,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? {\n params,\n query,\n body,\n ...(hasResponsePayload ? { output: responsePayload } : {}),\n }\n : undefined,\n ),\n );\n } catch (err) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'request',\n stage: 'error',\n method: methodUpper,\n path,\n url: requestUrl,\n durationMs: Date.now() - startedAt,\n error: err,\n },\n isVerboseDebug ? { params, query, body } : undefined,\n ),\n );\n logger?.error?.('Route error', err);\n next(err as any);\n }\n };\n\n const after = (def?.after ?? []).map((mw) => {\n const adapted = adaptCtxMw<Ctx>(mw);\n return (req: express.Request, res: express.Response, next: express.NextFunction) => {\n if (!handlerInvokedNext(res)) {\n next();\n return;\n }\n adapted(req, res, next);\n };\n });\n (router as any)[method](path, ...before, wrapped, ...after);\n registered.add(key);\n }\n\n /**\n * Register controller definitions for the provided keys.\n * @param registry Finalized registry of leaves.\n * @param controllers Partial controller map keyed by `\"METHOD /path\"`.\n */\n function registerControllers<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n >(registry: R, controllers: PartialControllerMap<R, Ctx>) {\n (Object.keys(controllers) as Array<KeysOfRegistry<R>>).forEach((key) => {\n const leaf = registry.byKey[key] as unknown as LeafFromKey<R, typeof key> | undefined;\n if (!leaf) {\n logger?.warn?.(`No leaf found for controller key: ${key}. Not registering route.`);\n return;\n }\n const def = controllers[key];\n if (!def) return;\n register(leaf as LeafFromKey<R, typeof key>, def);\n });\n }\n\n /**\n * Warn about leaves that do not have a registered controller.\n * @param registry Finalized registry of leaves.\n * @param warnLogger Logger used for warning output.\n */\n function warnMissing<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n warnLogger: { warn: (...args: any[]) => void },\n ) {\n const registeredFromStore = new Set<string>(Array.from(registered));\n if (registeredFromStore.size === 0) {\n collectRoutesFromStack(router).forEach((key) => registeredFromStore.add(key));\n }\n for (const leaf of registry.all) {\n const key = keyOf(leaf);\n if (!registeredFromStore.has(key)) {\n warnLogger.warn(`No controller registered for route: ${key}`);\n }\n }\n }\n\n return {\n router,\n register,\n registerControllers,\n warnMissingControllers: warnMissing,\n getRegisteredKeys: () => Array.from(registered),\n };\n}\n\n/**\n * Bind only the controllers that are present in the provided map.\n * @param router Express router or app.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param controllers Partial map of controllers keyed by `\"METHOD /path\"`.\n * @param config Optional route server configuration.\n * @returns The same router instance for chaining.\n */\nexport function bindExpressRoutes<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n>(\n router: Router,\n registry: R,\n controllers: PartialControllerMap<R, Ctx>,\n config: RouteServerConfig<Ctx>,\n) {\n const server = createRouteServer<Ctx>(router, config);\n server.registerControllers(registry, controllers);\n return router;\n}\n\n/**\n * Bind controllers for every leaf. Missing entries fail at compile time.\n * @param router Express router or app.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param controllers Complete map of controllers keyed by `\"METHOD /path\"`.\n * @param config Optional route server configuration.\n * @returns The same router instance for chaining.\n */\nexport function bindAll<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n>(\n router: Router,\n registry: R,\n controllers: { [K in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, K>, Ctx> },\n config: RouteServerConfig<Ctx>,\n) {\n const server = createRouteServer<Ctx>(router, config);\n server.registerControllers(registry, controllers);\n return router;\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// DX helpers\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Helper for great IntelliSense when authoring controller maps.\n * @returns Function that enforces key names while preserving partial flexibility.\n */\nexport const defineControllers =\n <R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }, Ctx = unknown>() =>\n <M extends PartialControllerMap<R, Ctx>>(m: M) =>\n m;\n\n/**\n * Wrap a plain RequestHandler as an auth factory compatible with `fromCfg.auth`.\n * @param mw Middleware invoked for any leaf that requires authentication.\n * @param _leaf Leaf metadata (ignored, but provided to match factory signature).\n * @returns Factory that ignores the leaf and returns the same middleware.\n */\nexport const asLeafAuth =\n (mw: RequestHandler) =>\n (_leaf: AnyLeaf): RequestHandler =>\n mw;\n\n/**\n * Warn about leaves that don't have controllers.\n * Call this during startup to surface missing routes.\n * @param router Express router or app to inspect.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param logger Logger where warnings are emitted.\n */\nexport function warnMissingControllers<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n>(router: Router, registry: R, logger: { warn: (...args: any[]) => void }) {\n const registeredStore = (router as any)[REGISTERED_ROUTES_SYMBOL] as Set<string> | undefined;\n const initial = registeredStore ? Array.from(registeredStore) : collectRoutesFromStack(router);\n const registeredKeys = new Set<string>(initial);\n\n for (const leaf of registry.all) {\n const k = keyOf(leaf);\n if (!registeredKeys.has(k)) {\n logger.warn(`No controller registered for route: ${k}`);\n }\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -28,11 +28,29 @@ function createServerDebugEmitter(option) {
28
28
  }
29
29
  var keyOf = (leaf) => `${leaf.method.toUpperCase()} ${leaf.path}`;
30
30
  var CTX_SYMBOL = Symbol.for("typedLeaves.ctx");
31
+ var AFTER_HANDLER_NEXT_SYMBOL = Symbol.for("typedLeaves.afterHandlerNext");
32
+ function setAfterHandlerNext(res, value) {
33
+ res.locals[AFTER_HANDLER_NEXT_SYMBOL] = value;
34
+ }
35
+ function handlerInvokedNext(res) {
36
+ return Boolean(res.locals[AFTER_HANDLER_NEXT_SYMBOL]);
37
+ }
31
38
  function getCtx(res) {
32
39
  return res.locals[CTX_SYMBOL];
33
40
  }
34
41
  function adaptCtxMw(mw) {
35
- return (req, res, next) => mw({ req, res, next, ctx: getCtx(res) });
42
+ return (req, res, next) => {
43
+ try {
44
+ const result = mw({ req, res, next, ctx: getCtx(res) });
45
+ if (result && typeof result.then === "function") {
46
+ return result.catch((err) => next(err));
47
+ }
48
+ return result;
49
+ } catch (err) {
50
+ next(err);
51
+ return void 0;
52
+ }
53
+ };
36
54
  }
37
55
  var defaultSend = (res, data) => {
38
56
  res.json(data);
@@ -67,16 +85,16 @@ function collectRoutesFromStack(appOrRouter) {
67
85
  return result;
68
86
  }
69
87
  function createRouteServer(router, config) {
70
- const validateOutput = config?.validateOutput ?? true;
71
- const send = config?.send ?? defaultSend;
72
- const logger = config?.logger;
73
- const { emit: emitDebug, mode: debugMode } = createServerDebugEmitter(config?.debug);
88
+ const validateOutput = config.validateOutput ?? true;
89
+ const send = config.send ?? defaultSend;
90
+ const logger = config.logger;
91
+ const { emit: emitDebug, mode: debugMode } = createServerDebugEmitter(config.debug);
74
92
  const isVerboseDebug = debugMode === "complete";
75
93
  const decorateDebugEvent = (event, details) => {
76
94
  if (!isVerboseDebug || !details) return event;
77
95
  return { ...event, ...details };
78
96
  };
79
- const ctxMw = config?.buildCtx ? async (req, res, next) => {
97
+ const ctxMw = async (req, res, next) => {
80
98
  try {
81
99
  const ctx = await config.buildCtx(req, res);
82
100
  res.locals[CTX_SYMBOL] = ctx;
@@ -85,18 +103,18 @@ function createRouteServer(router, config) {
85
103
  logger?.error?.("buildCtx error", err);
86
104
  next(err);
87
105
  }
88
- } : void 0;
89
- const globalMws = (config?.global ?? []).map((mw) => adaptCtxMw(mw));
106
+ };
107
+ const globalMws = (config.global ?? []).map((mw) => adaptCtxMw(mw));
90
108
  const registered = getRegisteredRouteStore(router);
91
109
  const buildDerived = (leaf) => {
92
110
  const derived = [];
93
- const decision = config?.fromCfg?.when?.(leaf.cfg, leaf) ?? {};
111
+ const decision = config.fromCfg?.when?.(leaf.cfg, leaf) ?? {};
94
112
  const needsAuth = typeof decision.auth === "boolean" ? decision.auth : !!leaf.cfg.authenticated;
95
- if (needsAuth && config?.fromCfg?.auth) {
113
+ if (needsAuth && config.fromCfg?.auth) {
96
114
  const authMw = resolveAuth(config.fromCfg.auth, leaf);
97
115
  if (authMw) derived.push(authMw);
98
116
  }
99
- if (config?.fromCfg?.upload && Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0) {
117
+ if (config.fromCfg?.upload && Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0) {
100
118
  derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles, leaf));
101
119
  }
102
120
  return derived;
@@ -124,6 +142,13 @@ function createRouteServer(router, config) {
124
142
  let body;
125
143
  let responsePayload;
126
144
  let hasResponsePayload = false;
145
+ setAfterHandlerNext(res, false);
146
+ let handlerCalledNext = false;
147
+ const downstreamNext = (err) => {
148
+ handlerCalledNext = true;
149
+ setAfterHandlerNext(res, true);
150
+ next(err);
151
+ };
127
152
  try {
128
153
  logger?.info?.(`${methodUpper}@${path} (${requestUrl})`);
129
154
  const ctx = res.locals[CTX_SYMBOL];
@@ -150,7 +175,7 @@ function createRouteServer(router, config) {
150
175
  result = await def.handler({
151
176
  req,
152
177
  res,
153
- next,
178
+ next: downstreamNext,
154
179
  ctx,
155
180
  params,
156
181
  query,
@@ -160,12 +185,16 @@ function createRouteServer(router, config) {
160
185
  logger?.error?.("Handler error", e);
161
186
  throw e;
162
187
  }
163
- if (!res.headersSent && result !== void 0) {
164
- const out = validateOutput && leaf.cfg.outputSchema ? leaf.cfg.outputSchema.parse(result) : result;
165
- responsePayload = out;
166
- hasResponsePayload = true;
167
- logger?.verbose?.(`${methodUpper}@${path} result`, out);
168
- send(res, out);
188
+ if (!res.headersSent) {
189
+ if (result !== void 0) {
190
+ const out = validateOutput && leaf.cfg.outputSchema ? leaf.cfg.outputSchema.parse(result) : result;
191
+ responsePayload = out;
192
+ hasResponsePayload = true;
193
+ logger?.verbose?.(`${methodUpper}@${path} result`, out);
194
+ send(res, out);
195
+ } else if (!handlerCalledNext) {
196
+ next();
197
+ }
169
198
  }
170
199
  emitDebug(
171
200
  decorateDebugEvent(
@@ -204,7 +233,16 @@ function createRouteServer(router, config) {
204
233
  next(err);
205
234
  }
206
235
  };
207
- const after = (def?.after ?? []).map((mw) => adaptCtxMw(mw));
236
+ const after = (def?.after ?? []).map((mw) => {
237
+ const adapted = adaptCtxMw(mw);
238
+ return (req, res, next) => {
239
+ if (!handlerInvokedNext(res)) {
240
+ next();
241
+ return;
242
+ }
243
+ adapted(req, res, next);
244
+ };
245
+ });
208
246
  router[method](path, ...before, wrapped, ...after);
209
247
  registered.add(key);
210
248
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/routesV3.server.ts"],"sourcesContent":["/**\n * routesV3.server.ts\n * -----------------------------------------------------------------------------\n * Bind an Express router/app to a `finalize(...)` registry of AnyLeafs.\n * - Fully typed handlers (params/query/body/output)\n * - Zod parsing + optional output validation\n * - buildCtx runs as a middleware *first*, before all other middlewares\n * - Global, per-route, and cfg-derived middlewares (auth, uploads)\n * - Helper to warn about unimplemented routes\n * - DX helpers to use `ctx` in any middleware with proper types\n */\n\nimport type * as express from 'express';\nimport type { RequestHandler, Router } from 'express';\nimport type { ZodType } from 'zod';\nimport {\n FileField,\n HttpMethod,\n MethodCfg,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\n\nexport type { AnyLeaf } from '@emeryld/rrroutes-contract';\n\n/** Shape expected from optional logger implementations. */\nexport type LoggerLike = {\n info?: (...args: any[]) => void;\n warn?: (...args: any[]) => void;\n error?: (...args: any[]) => void;\n debug?: (...args: any[]) => void;\n verbose?: (...args: any[]) => void;\n system?: (...args: any[]) => void;\n log?: (...args: any[]) => void;\n};\n\n// Debug logging --------------------------------------------------------------\nexport type RouteServerDebugMode = 'minimal' | 'complete';\n\nexport type RouteServerDebugEvent =\n | {\n type: 'request';\n stage: 'start' | 'success' | 'error';\n method: Uppercase<HttpMethod>;\n path: string;\n url: string;\n durationMs?: number;\n params?: unknown;\n query?: unknown;\n body?: unknown;\n output?: unknown;\n error?: unknown;\n }\n | {\n type: 'register';\n method: Uppercase<HttpMethod>;\n path: string;\n };\n\nexport type RouteServerDebugLogger = (event: RouteServerDebugEvent) => void;\n\nexport type RouteServerDebugOptions =\n | boolean\n | RouteServerDebugLogger\n | RouteServerDebugMode\n | {\n enabled?: boolean;\n logger?: RouteServerDebugLogger;\n mode?: RouteServerDebugMode;\n };\n\nconst noopServerDebug: RouteServerDebugLogger = () => {};\n\nconst defaultServerDebug: RouteServerDebugLogger = (event: RouteServerDebugEvent) => {\n if (typeof console === 'undefined') return;\n const fn = console.debug ?? console.log;\n fn?.call(console, '[rrroutes-server]', event);\n};\n\ntype ServerDebugEmitter = {\n emit: RouteServerDebugLogger;\n mode: RouteServerDebugMode;\n};\n\nfunction createServerDebugEmitter(option?: RouteServerDebugOptions): ServerDebugEmitter {\n const disabled: ServerDebugEmitter = { emit: noopServerDebug, mode: 'minimal' };\n if (!option) return disabled;\n if (option === true || option === 'minimal') {\n return { emit: defaultServerDebug, mode: 'minimal' };\n }\n if (option === 'complete') {\n return { emit: defaultServerDebug, mode: 'complete' };\n }\n if (typeof option === 'function') {\n return { emit: option, mode: 'minimal' };\n }\n if (option.enabled === false) {\n return { emit: noopServerDebug, mode: option.mode ?? 'minimal' };\n }\n return {\n emit: option.logger ?? defaultServerDebug,\n mode: option.mode ?? 'minimal',\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Keys + leaf helpers (derive keys from byKey to avoid template-literal pitfalls)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Keys like \"GET /v1/foo\" that *actually* exist in the registry */\nexport type KeysOfRegistry<R extends { byKey: Record<string, AnyLeaf> }> = keyof R['byKey'] &\n string;\n\ntype MethodFromKey<K extends string> = K extends `${infer M} ${string}` ? Lowercase<M> : never;\ntype PathFromKey<K extends string> = K extends `${string} ${infer P}` ? P : never;\n\n/** Given a registry and a key, pick the exact leaf for that method+path */\nexport type LeafFromKey<R extends { all: readonly AnyLeaf[] }, K extends string> = Extract<\n R['all'][number],\n { method: MethodFromKey<K> & HttpMethod; path: PathFromKey<K> }\n>;\n\n/** Optional-ify types if your core returns `never` when a schema isn't defined */\ntype Maybe<T> = [T] extends [never] ? undefined : T;\n\n/** Typed params argument exposed to handlers. */\nexport type ArgParams<L extends AnyLeaf> = Maybe<InferParams<L>>;\n/** Typed query argument exposed to handlers. */\nexport type ArgQuery<L extends AnyLeaf> = Maybe<InferQuery<L>>;\n/** Typed body argument exposed to handlers. */\nexport type ArgBody<L extends AnyLeaf> = Maybe<InferBody<L>>;\n\n/**\n * Convenience to compute a `\"METHOD /path\"` key from a leaf.\n * @param leaf Leaf describing the route.\n * @returns Uppercase method + path key.\n */\nexport const keyOf = (leaf: AnyLeaf) => `${leaf.method.toUpperCase()} ${leaf.path}` as const;\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Context typing & DX helpers (so ctx is usable in *any* middleware)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Unique symbol used to stash ctx on res.locals.\n * (Symbols are safer than string keys against collisions.)\n */\nexport const CTX_SYMBOL: unique symbol = Symbol.for('typedLeaves.ctx');\n\n/** Response type that *has* a ctx on locals for DX in middlewares */\nexport type ResponseWithCtx<Ctx> =\n // Replace locals with an intersection that guarantees CTX_SYMBOL exists\n Omit<express.Response, 'locals'> & {\n locals: express.Response['locals'] & { [CTX_SYMBOL]: Ctx };\n };\n\n/** A middleware signature that can *use* ctx via `res.locals[CTX_SYMBOL]` */\nexport type CtxRequestHandler<Ctx> = (args: {\n req: express.Request;\n res: express.Response;\n next: express.NextFunction;\n ctx: Ctx;\n}) => any;\n\n/**\n * Safely read ctx from any Response.\n * @param res Express response whose locals contain the ctx symbol.\n * @returns Strongly typed context object.\n */\nexport function getCtx<Ctx = unknown>(res: express.Response): Ctx {\n return (res.locals as any)[CTX_SYMBOL] as Ctx;\n}\n\n/**\n * Wrap a ctx-typed middleware to a plain RequestHandler (for arrays, etc.).\n * @param mw Middleware that expects a typed response with ctx available.\n * @returns Standard Express request handler.\n */\nfunction adaptCtxMw<Ctx>(mw: CtxRequestHandler<Ctx>): RequestHandler {\n return (req, res, next) => mw({ req, res, next, ctx: getCtx<Ctx>(res) });\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Controller types — object form only (simpler, clearer typings)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Typed route handler for a specific leaf */\nexport type Handler<L extends AnyLeaf, Ctx = unknown> = (args: {\n req: express.Request;\n res: express.Response;\n next: express.NextFunction;\n ctx: Ctx;\n params: ArgParams<L>;\n query: ArgQuery<L>;\n body: ArgBody<L>;\n}) => Promise<InferOutput<L>> | InferOutput<L>;\n\n/** Route definition for one key */\nexport type RouteDef<L extends AnyLeaf, Ctx = unknown> = {\n /** Middlewares before the handler (run after buildCtx/global/derived) */\n use?: Array<CtxRequestHandler<Ctx>>;\n /** Middlewares after the handler *if* it calls next() */\n after?: Array<CtxRequestHandler<Ctx>>;\n /** Your business logic */\n handler: Handler<L, Ctx>;\n};\n\n/** Map of registry keys -> route defs */\nexport type ControllerMap<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n> = {\n [P in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, P>, Ctx>;\n};\n\nexport type PartialControllerMap<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n> = Partial<ControllerMap<R, Ctx>>;\n\n// ──────────────────────────────────────────────────────────────────────────────\n/** Options + derivation helpers */\n// ──────────────────────────────────────────────────────────────────────────────\n\nexport type RouteServerConfig<Ctx = unknown> = {\n /**\n * Build a request-scoped context. We wrap this in a middleware that runs\n * *first* (before global/derived/route middlewares), and stash it on\n * `res.locals[CTX_SYMBOL]` so *all* later middlewares can use it.\n */\n buildCtx?: (req: express.Request, res: express.Response) => Ctx | Promise<Ctx>;\n\n /**\n * Global middlewares for every bound route (run *after* buildCtx).\n * You can write them as ctx-aware middlewares for great DX.\n */\n global?: Array<CtxRequestHandler<Ctx>>;\n\n /**\n * Derive middleware from MethodCfg.\n * - `auth` runs when cfg.authenticated === true (or `when` overrides)\n * - `upload` runs when cfg.bodyFiles has entries\n */\n fromCfg?: {\n auth?: RequestHandler | ((leaf: AnyLeaf) => RequestHandler);\n when?: (cfg: MethodCfg, leaf: AnyLeaf) => { auth?: boolean } | void;\n upload?: (files: FileField[] | undefined, leaf: AnyLeaf) => RequestHandler[];\n };\n\n /** Validate handler return values with outputSchema (default: true) */\n validateOutput?: boolean;\n\n /** Custom responder (default: res.json(data)) */\n send?: (res: express.Response, data: unknown) => void;\n\n /** Optional logger hooks */\n logger?: LoggerLike;\n\n /** Optional debug logging for request lifecycle (minimal/complete). */\n debug?: RouteServerDebugOptions;\n};\n\n/** Default JSON responder (typed to avoid implicit-any diagnostics) */\nconst defaultSend: (res: express.Response, data: unknown) => void = (res, data) => {\n res.json(data as any);\n};\n\n/**\n * Normalize `auth` into a RequestHandler (avoids union-narrowing issues).\n * @param auth Static middleware or factory returning one for the current leaf.\n * @param leaf Leaf being registered.\n * @returns Request handler or undefined when no auth is required.\n */\nfunction resolveAuth(\n auth: RequestHandler | ((leaf: AnyLeaf) => RequestHandler) | undefined,\n leaf: AnyLeaf,\n): RequestHandler | undefined {\n if (!auth) return undefined;\n return (auth as (l: AnyLeaf) => RequestHandler).length === 1\n ? (auth as (l: AnyLeaf) => RequestHandler)(leaf)\n : (auth as RequestHandler);\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Core builder\n// ──────────────────────────────────────────────────────────────────────────────\n\nconst REGISTERED_ROUTES_SYMBOL = Symbol.for('routesV3.registeredRoutes');\n\ntype RegisteredRouteStore = Set<string>;\n\n/**\n * Retrieve or initialize the shared store of registered route keys.\n * @param router Express router/application that carries previously registered keys.\n * @returns Set of string keys describing registered routes.\n */\nfunction getRegisteredRouteStore(router: Router | express.Application): RegisteredRouteStore {\n const existing = (router as any)[REGISTERED_ROUTES_SYMBOL] as RegisteredRouteStore | undefined;\n if (existing) return existing;\n const store: RegisteredRouteStore = new Set();\n (router as any)[REGISTERED_ROUTES_SYMBOL] = store;\n return store;\n}\n\n/**\n * Inspect the Express layer stack to discover already-registered routes.\n * @param appOrRouter Express application or router to inspect.\n * @returns All keys in the form `\"METHOD /path\"` found on the stack.\n */\nfunction collectRoutesFromStack(appOrRouter: Router | express.Application): string[] {\n const result: string[] = [];\n const stack: any[] =\n (appOrRouter as any).stack ??\n ((appOrRouter as any)._router ? (appOrRouter as any)._router.stack : undefined) ??\n [];\n\n if (!Array.isArray(stack)) return result;\n\n for (const layer of stack) {\n const route = layer && layer.route;\n if (!route) continue;\n\n const paths = Array.isArray(route.path) ? route.path : [route.path];\n const methodEntries = Object.entries(route.methods ?? {}).filter(([, enabled]) => enabled);\n\n for (const path of paths) {\n for (const [method] of methodEntries) {\n result.push(`${method.toUpperCase()} ${path}`);\n }\n }\n }\n\n return result;\n}\n\n/** Runtime helpers returned by `createRouteServer`. */\nexport type RouteServer<Ctx = unknown> = {\n router: Router | express.Application;\n register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>): void;\n registerControllers<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n controllers: PartialControllerMap<R, Ctx>,\n ): void;\n warnMissingControllers<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n logger: { warn: (...args: any[]) => void },\n ): void;\n getRegisteredKeys(): string[];\n};\n\n/**\n * Create an Express binding helper that keeps routes and controllers in sync.\n * @param router Express router or app to register handlers on.\n * @param config Optional configuration controlling ctx building, auth, uploads, etc.\n * @returns Object with helpers to register controllers and inspect registered keys.\n */\nexport function createRouteServer<Ctx = unknown>(\n router: Router | express.Application,\n config?: RouteServerConfig<Ctx>,\n): RouteServer<Ctx> {\n const validateOutput = config?.validateOutput ?? true;\n const send = config?.send ?? defaultSend;\n const logger = config?.logger;\n const { emit: emitDebug, mode: debugMode } = createServerDebugEmitter(config?.debug);\n const isVerboseDebug = debugMode === 'complete';\n const decorateDebugEvent = <T extends RouteServerDebugEvent>(\n event: T,\n details?: Partial<RouteServerDebugEvent>,\n ): RouteServerDebugEvent => {\n if (!isVerboseDebug || !details) return event;\n return { ...event, ...details } as RouteServerDebugEvent;\n };\n\n const ctxMw: RequestHandler | undefined = config?.buildCtx\n ? async (req, res, next) => {\n try {\n const ctx = await config.buildCtx!(req, res);\n (res.locals as any)[CTX_SYMBOL] = ctx;\n next();\n } catch (err) {\n logger?.error?.('buildCtx error', err);\n next(err as any);\n }\n }\n : undefined;\n\n const globalMws = (config?.global ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n const registered = getRegisteredRouteStore(router);\n\n const buildDerived = (leaf: AnyLeaf): RequestHandler[] => {\n const derived: RequestHandler[] = [];\n const decision = config?.fromCfg?.when?.(leaf.cfg, leaf) ?? {};\n const needsAuth = typeof decision.auth === 'boolean' ? decision.auth : !!leaf.cfg.authenticated;\n\n if (needsAuth && config?.fromCfg?.auth) {\n const authMw = resolveAuth(config.fromCfg.auth, leaf);\n if (authMw) derived.push(authMw);\n }\n\n if (\n config?.fromCfg?.upload &&\n Array.isArray(leaf.cfg.bodyFiles) &&\n leaf.cfg.bodyFiles.length > 0\n ) {\n derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles, leaf));\n }\n\n return derived;\n };\n\n /** Register a single leaf/controller pair on the underlying router. */\n function register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>) {\n const method = leaf.method as HttpMethod;\n const methodUpper = method.toUpperCase() as Uppercase<HttpMethod>;\n const path = leaf.path as string;\n const key = keyOf(leaf);\n emitDebug({ type: 'register', method: methodUpper, path });\n\n const routeSpecific = (def?.use ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n const derived = buildDerived(leaf);\n const before: RequestHandler[] = [\n ...(ctxMw ? [ctxMw] : []),\n ...globalMws,\n ...derived,\n ...routeSpecific,\n ];\n\n const wrapped: RequestHandler = async (req, res, next) => {\n const requestUrl = req.originalUrl ?? path;\n const startedAt = Date.now();\n emitDebug({ type: 'request', stage: 'start', method: methodUpper, path, url: requestUrl });\n let params: ArgParams<L> | undefined;\n let query: ArgQuery<L> | undefined;\n let body: ArgBody<L> | undefined;\n let responsePayload: InferOutput<L> | undefined;\n let hasResponsePayload = false;\n\n try {\n logger?.info?.(`${methodUpper}@${path} (${requestUrl})`);\n\n const ctx = (res.locals as any)[CTX_SYMBOL] as Ctx;\n\n params = (\n leaf.cfg.paramsSchema\n ? (leaf.cfg.paramsSchema as ZodType).parse(req.params)\n : Object.keys(req.params || {}).length\n ? (req.params as any)\n : undefined\n ) as ArgParams<L>;\n\n try {\n query = leaf.cfg.querySchema\n ? (leaf.cfg.querySchema as ZodType).parse(req.query)\n : Object.keys(req.query || {}).length\n ? (req.query as any)\n : undefined;\n } catch (e) {\n logger?.error?.('Query parsing error', {\n path,\n method: methodUpper,\n error: e,\n raw: JSON.stringify(req.query),\n });\n throw e;\n }\n\n body = (\n leaf.cfg.bodySchema\n ? (leaf.cfg.bodySchema as ZodType).parse(req.body)\n : req.body !== undefined\n ? (req.body as any)\n : undefined\n ) as ArgBody<L>;\n\n logger?.verbose?.(`${methodUpper}@${path} (${requestUrl})`, {\n params,\n query,\n body,\n });\n\n let result;\n try {\n result = await def.handler({\n req,\n res,\n next,\n ctx,\n params: params as ArgParams<L>,\n query: query as ArgQuery<L>,\n body: body as ArgBody<L>,\n });\n } catch (e) {\n logger?.error?.('Handler error', e);\n throw e;\n }\n\n if (!res.headersSent && result !== undefined) {\n const out =\n validateOutput && leaf.cfg.outputSchema\n ? (leaf.cfg.outputSchema as ZodType).parse(result)\n : result;\n responsePayload = out as InferOutput<L>;\n hasResponsePayload = true;\n logger?.verbose?.(`${methodUpper}@${path} result`, out);\n send(res, out);\n }\n\n emitDebug(\n decorateDebugEvent(\n {\n type: 'request',\n stage: 'success',\n method: methodUpper,\n path,\n url: requestUrl,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? {\n params,\n query,\n body,\n ...(hasResponsePayload ? { output: responsePayload } : {}),\n }\n : undefined,\n ),\n );\n } catch (err) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'request',\n stage: 'error',\n method: methodUpper,\n path,\n url: requestUrl,\n durationMs: Date.now() - startedAt,\n error: err,\n },\n isVerboseDebug ? { params, query, body } : undefined,\n ),\n );\n logger?.error?.('Route error', err);\n next(err as any);\n }\n };\n\n const after = (def?.after ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n (router as any)[method](path, ...before, wrapped, ...after);\n registered.add(key);\n }\n\n /**\n * Register controller definitions for the provided keys.\n * @param registry Finalized registry of leaves.\n * @param controllers Partial controller map keyed by `\"METHOD /path\"`.\n */\n function registerControllers<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n >(registry: R, controllers: PartialControllerMap<R, Ctx>) {\n (Object.keys(controllers) as Array<KeysOfRegistry<R>>).forEach((key) => {\n const leaf = registry.byKey[key] as unknown as LeafFromKey<R, typeof key> | undefined;\n if (!leaf) {\n logger?.warn?.(`No leaf found for controller key: ${key}. Not registering route.`);\n return;\n }\n const def = controllers[key];\n if (!def) return;\n register(leaf as LeafFromKey<R, typeof key>, def);\n });\n }\n\n /**\n * Warn about leaves that do not have a registered controller.\n * @param registry Finalized registry of leaves.\n * @param warnLogger Logger used for warning output.\n */\n function warnMissing<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n warnLogger: { warn: (...args: any[]) => void },\n ) {\n const registeredFromStore = new Set<string>(Array.from(registered));\n if (registeredFromStore.size === 0) {\n collectRoutesFromStack(router).forEach((key) => registeredFromStore.add(key));\n }\n for (const leaf of registry.all) {\n const key = keyOf(leaf);\n if (!registeredFromStore.has(key)) {\n warnLogger.warn(`No controller registered for route: ${key}`);\n }\n }\n }\n\n return {\n router,\n register,\n registerControllers,\n warnMissingControllers: warnMissing,\n getRegisteredKeys: () => Array.from(registered),\n };\n}\n\n/**\n * Bind only the controllers that are present in the provided map.\n * @param router Express router or app.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param controllers Partial map of controllers keyed by `\"METHOD /path\"`.\n * @param config Optional route server configuration.\n * @returns The same router instance for chaining.\n */\nexport function bindExpressRoutes<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n>(\n router: Router | express.Application,\n registry: R,\n controllers: PartialControllerMap<R, Ctx>,\n config?: RouteServerConfig<Ctx>,\n) {\n const server = createRouteServer<Ctx>(router, config);\n server.registerControllers(registry, controllers);\n return router;\n}\n\n/**\n * Bind controllers for every leaf. Missing entries fail at compile time.\n * @param router Express router or app.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param controllers Complete map of controllers keyed by `\"METHOD /path\"`.\n * @param config Optional route server configuration.\n * @returns The same router instance for chaining.\n */\nexport function bindAll<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n>(\n router: Router | express.Application,\n registry: R,\n controllers: { [K in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, K>, Ctx> },\n config?: RouteServerConfig<Ctx>,\n) {\n const server = createRouteServer<Ctx>(router, config);\n server.registerControllers(registry, controllers);\n return router;\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// DX helpers\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Helper for great IntelliSense when authoring controller maps.\n * @returns Function that enforces key names while preserving partial flexibility.\n */\nexport const defineControllers =\n <R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }, Ctx = unknown>() =>\n <M extends PartialControllerMap<R, Ctx>>(m: M) =>\n m;\n\n/**\n * Wrap a plain RequestHandler as an auth factory compatible with `fromCfg.auth`.\n * @param mw Middleware invoked for any leaf that requires authentication.\n * @param _leaf Leaf metadata (ignored, but provided to match factory signature).\n * @returns Factory that ignores the leaf and returns the same middleware.\n */\nexport const asLeafAuth =\n (mw: RequestHandler) =>\n (_leaf: AnyLeaf): RequestHandler =>\n mw;\n\n/**\n * Warn about leaves that don't have controllers.\n * Call this during startup to surface missing routes.\n * @param router Express router or app to inspect.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param logger Logger where warnings are emitted.\n */\nexport function warnMissingControllers<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n>(router: Router | express.Application, registry: R, logger: { warn: (...args: any[]) => void }) {\n const registeredStore = (router as any)[REGISTERED_ROUTES_SYMBOL] as Set<string> | undefined;\n const initial = registeredStore ? Array.from(registeredStore) : collectRoutesFromStack(router);\n const registeredKeys = new Set<string>(initial);\n\n for (const leaf of registry.all) {\n const k = keyOf(leaf);\n if (!registeredKeys.has(k)) {\n logger.warn(`No controller registered for route: ${k}`);\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/routesV3.server.ts"],"sourcesContent":["/**\n * routesV3.server.ts\n * -----------------------------------------------------------------------------\n * Bind an Express router/app to a `finalize(...)` registry of AnyLeafs.\n * - Fully typed handlers (params/query/body/output)\n * - Zod parsing + optional output validation\n * - buildCtx runs as a middleware *first*, before all other middlewares\n * - Global, per-route, and cfg-derived middlewares (auth, uploads)\n * - Helper to warn about unimplemented routes\n * - DX helpers to use `ctx` in any middleware with proper types\n */\n\nimport type * as express from 'express';\nimport type { RequestHandler, Router } from 'express';\nimport type { ZodType } from 'zod';\nimport {\n FileField,\n HttpMethod,\n MethodCfg,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\n\nexport type { AnyLeaf } from '@emeryld/rrroutes-contract';\n\n/** Shape expected from optional logger implementations. */\nexport type LoggerLike = {\n info?: (...args: any[]) => void;\n warn?: (...args: any[]) => void;\n error?: (...args: any[]) => void;\n debug?: (...args: any[]) => void;\n verbose?: (...args: any[]) => void;\n system?: (...args: any[]) => void;\n log?: (...args: any[]) => void;\n};\n\n// Debug logging --------------------------------------------------------------\nexport type RouteServerDebugMode = 'minimal' | 'complete';\n\nexport type RouteServerDebugEvent =\n | {\n type: 'request';\n stage: 'start' | 'success' | 'error';\n method: Uppercase<HttpMethod>;\n path: string;\n url: string;\n durationMs?: number;\n params?: unknown;\n query?: unknown;\n body?: unknown;\n output?: unknown;\n error?: unknown;\n }\n | {\n type: 'register';\n method: Uppercase<HttpMethod>;\n path: string;\n };\n\nexport type RouteServerDebugLogger = (event: RouteServerDebugEvent) => void;\n\nexport type RouteServerDebugOptions =\n | boolean\n | RouteServerDebugLogger\n | RouteServerDebugMode\n | {\n enabled?: boolean;\n logger?: RouteServerDebugLogger;\n mode?: RouteServerDebugMode;\n };\n\nconst noopServerDebug: RouteServerDebugLogger = () => {};\n\nconst defaultServerDebug: RouteServerDebugLogger = (event: RouteServerDebugEvent) => {\n if (typeof console === 'undefined') return;\n const fn = console.debug ?? console.log;\n fn?.call(console, '[rrroutes-server]', event);\n};\n\ntype ServerDebugEmitter = {\n emit: RouteServerDebugLogger;\n mode: RouteServerDebugMode;\n};\n\nfunction createServerDebugEmitter(option?: RouteServerDebugOptions): ServerDebugEmitter {\n const disabled: ServerDebugEmitter = { emit: noopServerDebug, mode: 'minimal' };\n if (!option) return disabled;\n if (option === true || option === 'minimal') {\n return { emit: defaultServerDebug, mode: 'minimal' };\n }\n if (option === 'complete') {\n return { emit: defaultServerDebug, mode: 'complete' };\n }\n if (typeof option === 'function') {\n return { emit: option, mode: 'minimal' };\n }\n if (option.enabled === false) {\n return { emit: noopServerDebug, mode: option.mode ?? 'minimal' };\n }\n return {\n emit: option.logger ?? defaultServerDebug,\n mode: option.mode ?? 'minimal',\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Keys + leaf helpers (derive keys from byKey to avoid template-literal pitfalls)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Keys like \"GET /v1/foo\" that *actually* exist in the registry */\nexport type KeysOfRegistry<R extends { byKey: Record<string, AnyLeaf> }> = keyof R['byKey'] &\n string;\n\ntype MethodFromKey<K extends string> = K extends `${infer M} ${string}` ? Lowercase<M> : never;\ntype PathFromKey<K extends string> = K extends `${string} ${infer P}` ? P : never;\n\n/** Given a registry and a key, pick the exact leaf for that method+path */\nexport type LeafFromKey<R extends { all: readonly AnyLeaf[] }, K extends string> = Extract<\n R['all'][number],\n { method: MethodFromKey<K> & HttpMethod; path: PathFromKey<K> }\n>;\n\n/** Optional-ify types if your core returns `never` when a schema isn't defined */\ntype Maybe<T> = [T] extends [never] ? undefined : T;\n\n/** Typed params argument exposed to handlers. */\nexport type ArgParams<L extends AnyLeaf> = Maybe<InferParams<L>>;\n/** Typed query argument exposed to handlers. */\nexport type ArgQuery<L extends AnyLeaf> = Maybe<InferQuery<L>>;\n/** Typed body argument exposed to handlers. */\nexport type ArgBody<L extends AnyLeaf> = Maybe<InferBody<L>>;\n\n/**\n * Convenience to compute a `\"METHOD /path\"` key from a leaf.\n * @param leaf Leaf describing the route.\n * @returns Uppercase method + path key.\n */\nexport const keyOf = (leaf: AnyLeaf) => `${leaf.method.toUpperCase()} ${leaf.path}` as const;\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Context typing & DX helpers (so ctx is usable in *any* middleware)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Unique symbol used to stash ctx on res.locals.\n * (Symbols are safer than string keys against collisions.)\n */\nexport const CTX_SYMBOL: unique symbol = Symbol.for('typedLeaves.ctx');\n\nconst AFTER_HANDLER_NEXT_SYMBOL: unique symbol = Symbol.for('typedLeaves.afterHandlerNext');\n\nfunction setAfterHandlerNext(res: express.Response, value: boolean) {\n (res.locals as any)[AFTER_HANDLER_NEXT_SYMBOL] = value;\n}\n\nfunction handlerInvokedNext(res: express.Response): boolean {\n return Boolean((res.locals as any)[AFTER_HANDLER_NEXT_SYMBOL]);\n}\n\n/** Response type that *has* a ctx on locals for DX in middlewares */\nexport type ResponseWithCtx<Ctx> =\n // Replace locals with an intersection that guarantees CTX_SYMBOL exists\n Omit<express.Response, 'locals'> & {\n locals: express.Response['locals'] & { [CTX_SYMBOL]: Ctx };\n };\n\n/** A middleware signature that can *use* ctx via `res.locals[CTX_SYMBOL]` */\nexport type CtxRequestHandler<Ctx> = (args: {\n req: express.Request;\n res: express.Response;\n next: express.NextFunction;\n ctx: Ctx;\n}) => any;\n\n/**\n * Safely read ctx from any Response.\n * @param res Express response whose locals contain the ctx symbol.\n * @returns Strongly typed context object.\n */\nexport function getCtx<Ctx = unknown>(res: express.Response): Ctx {\n return (res.locals as any)[CTX_SYMBOL] as Ctx;\n}\n\n/**\n * Wrap a ctx-typed middleware to a plain RequestHandler (for arrays, etc.).\n * @param mw Middleware that expects a typed response with ctx available.\n * @returns Standard Express request handler.\n */\nfunction adaptCtxMw<Ctx>(mw: CtxRequestHandler<Ctx>): RequestHandler {\n return (req, res, next) => {\n try {\n const result = mw({ req, res, next, ctx: getCtx<Ctx>(res) });\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n return (result as Promise<unknown>).catch((err) => next(err));\n }\n return result as any;\n } catch (err) {\n next(err as any);\n return undefined;\n }\n };\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Controller types — object form only (simpler, clearer typings)\n// ──────────────────────────────────────────────────────────────────────────────\n\n/** Typed route handler for a specific leaf */\nexport type Handler<L extends AnyLeaf, Ctx = unknown> = (args: {\n req: express.Request;\n res: express.Response;\n next: express.NextFunction;\n ctx: Ctx;\n params: ArgParams<L>;\n query: ArgQuery<L>;\n body: ArgBody<L>;\n}) => Promise<InferOutput<L>> | InferOutput<L>;\n\n/** Route definition for one key */\nexport type RouteDef<L extends AnyLeaf, Ctx = unknown> = {\n /** Middlewares before the handler (run after buildCtx/global/derived) */\n use?: Array<CtxRequestHandler<Ctx>>;\n /** Middlewares after the handler *if* it calls next() */\n after?: Array<CtxRequestHandler<Ctx>>;\n /** Your business logic */\n handler: Handler<L, Ctx>;\n};\n\n/** Map of registry keys -> route defs */\nexport type ControllerMap<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n> = {\n [P in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, P>, Ctx>;\n};\n\nexport type PartialControllerMap<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n> = Partial<ControllerMap<R, Ctx>>;\n\n// ──────────────────────────────────────────────────────────────────────────────\n/** Options + derivation helpers */\n// ──────────────────────────────────────────────────────────────────────────────\n\nexport type RouteServerConfig<Ctx = unknown> = {\n /**\n * Build a request-scoped context. We wrap this in a middleware that runs\n * *first* (before global/derived/route middlewares), and stash it on\n * `res.locals[CTX_SYMBOL]` so *all* later middlewares can use it.\n */\n buildCtx: (req: express.Request, res: express.Response) => Ctx | Promise<Ctx>;\n\n /**\n * Global middlewares for every bound route (run *after* buildCtx).\n * You can write them as ctx-aware middlewares for great DX.\n */\n global?: Array<CtxRequestHandler<Ctx>>;\n\n /**\n * Derive middleware from MethodCfg.\n * - `auth` runs when cfg.authenticated === true (or `when` overrides)\n * - `upload` runs when cfg.bodyFiles has entries\n */\n fromCfg?: {\n auth?: RequestHandler | ((leaf: AnyLeaf) => RequestHandler);\n when?: (cfg: MethodCfg, leaf: AnyLeaf) => { auth?: boolean } | void;\n upload?: (files: FileField[] | undefined, leaf: AnyLeaf) => RequestHandler[];\n };\n\n /** Validate handler return values with outputSchema (default: true) */\n validateOutput?: boolean;\n\n /** Custom responder (default: res.json(data)) */\n send?: (res: express.Response, data: unknown) => void;\n\n /** Optional logger hooks */\n logger?: LoggerLike;\n\n /** Optional debug logging for request lifecycle (minimal/complete). */\n debug?: RouteServerDebugOptions;\n};\n\n/** Default JSON responder (typed to avoid implicit-any diagnostics) */\nconst defaultSend: (res: express.Response, data: unknown) => void = (res, data) => {\n res.json(data as any);\n};\n\n/**\n * Normalize `auth` into a RequestHandler (avoids union-narrowing issues).\n * @param auth Static middleware or factory returning one for the current leaf.\n * @param leaf Leaf being registered.\n * @returns Request handler or undefined when no auth is required.\n */\nfunction resolveAuth(\n auth: RequestHandler | ((leaf: AnyLeaf) => RequestHandler) | undefined,\n leaf: AnyLeaf,\n): RequestHandler | undefined {\n if (!auth) return undefined;\n return (auth as (l: AnyLeaf) => RequestHandler).length === 1\n ? (auth as (l: AnyLeaf) => RequestHandler)(leaf)\n : (auth as RequestHandler);\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Core builder\n// ──────────────────────────────────────────────────────────────────────────────\n\nconst REGISTERED_ROUTES_SYMBOL = Symbol.for('routesV3.registeredRoutes');\n\ntype RegisteredRouteStore = Set<string>;\n\n/**\n * Retrieve or initialize the shared store of registered route keys.\n * @param router Express router/application that carries previously registered keys.\n * @returns Set of string keys describing registered routes.\n */\nfunction getRegisteredRouteStore(router: Router): RegisteredRouteStore {\n const existing = (router as any)[REGISTERED_ROUTES_SYMBOL] as RegisteredRouteStore | undefined;\n if (existing) return existing;\n const store: RegisteredRouteStore = new Set();\n (router as any)[REGISTERED_ROUTES_SYMBOL] = store;\n return store;\n}\n\n/**\n * Inspect the Express layer stack to discover already-registered routes.\n * @param appOrRouter Express application or router to inspect.\n * @returns All keys in the form `\"METHOD /path\"` found on the stack.\n */\nfunction collectRoutesFromStack(appOrRouter: Router): string[] {\n const result: string[] = [];\n const stack: any[] =\n (appOrRouter as any).stack ??\n ((appOrRouter as any)._router ? (appOrRouter as any)._router.stack : undefined) ??\n [];\n\n if (!Array.isArray(stack)) return result;\n\n for (const layer of stack) {\n const route = layer && layer.route;\n if (!route) continue;\n\n const paths = Array.isArray(route.path) ? route.path : [route.path];\n const methodEntries = Object.entries(route.methods ?? {}).filter(([, enabled]) => enabled);\n\n for (const path of paths) {\n for (const [method] of methodEntries) {\n result.push(`${method.toUpperCase()} ${path}`);\n }\n }\n }\n\n return result;\n}\n\n/** Runtime helpers returned by `createRouteServer`. */\nexport type RouteServer<Ctx = unknown> = {\n router: Router;\n register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>): void;\n registerControllers<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n controllers: PartialControllerMap<R, Ctx>,\n ): void;\n warnMissingControllers<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n logger: { warn: (...args: any[]) => void },\n ): void;\n getRegisteredKeys(): string[];\n};\n\n/**\n * Create an Express binding helper that keeps routes and controllers in sync.\n * @param router Express router or app to register handlers on.\n * @param config Optional configuration controlling ctx building, auth, uploads, etc.\n * @returns Object with helpers to register controllers and inspect registered keys.\n */\nexport function createRouteServer<Ctx = unknown>(\n router: Router,\n config: RouteServerConfig<Ctx>,\n): RouteServer<Ctx> {\n const validateOutput = config.validateOutput ?? true;\n const send = config.send ?? defaultSend;\n const logger = config.logger;\n const { emit: emitDebug, mode: debugMode } = createServerDebugEmitter(config.debug);\n const isVerboseDebug = debugMode === 'complete';\n const decorateDebugEvent = <T extends RouteServerDebugEvent>(\n event: T,\n details?: Partial<RouteServerDebugEvent>,\n ): RouteServerDebugEvent => {\n if (!isVerboseDebug || !details) return event;\n return { ...event, ...details } as RouteServerDebugEvent;\n };\n\n const ctxMw: RequestHandler | undefined = async (req, res, next) => {\n try {\n const ctx = await config.buildCtx!(req, res);\n (res.locals as any)[CTX_SYMBOL] = ctx;\n next();\n } catch (err) {\n logger?.error?.('buildCtx error', err);\n next(err as any);\n }\n };\n\n const globalMws = (config.global ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n const registered = getRegisteredRouteStore(router);\n\n const buildDerived = (leaf: AnyLeaf): RequestHandler[] => {\n const derived: RequestHandler[] = [];\n const decision = config.fromCfg?.when?.(leaf.cfg, leaf) ?? {};\n const needsAuth = typeof decision.auth === 'boolean' ? decision.auth : !!leaf.cfg.authenticated;\n\n if (needsAuth && config.fromCfg?.auth) {\n const authMw = resolveAuth(config.fromCfg.auth, leaf);\n if (authMw) derived.push(authMw);\n }\n\n if (\n config.fromCfg?.upload &&\n Array.isArray(leaf.cfg.bodyFiles) &&\n leaf.cfg.bodyFiles.length > 0\n ) {\n derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles, leaf));\n }\n\n return derived;\n };\n\n /** Register a single leaf/controller pair on the underlying router. */\n function register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>) {\n const method = leaf.method as HttpMethod;\n const methodUpper = method.toUpperCase() as Uppercase<HttpMethod>;\n const path = leaf.path as string;\n const key = keyOf(leaf);\n emitDebug({ type: 'register', method: methodUpper, path });\n\n const routeSpecific = (def?.use ?? []).map((mw) => adaptCtxMw<Ctx>(mw));\n const derived = buildDerived(leaf);\n const before: RequestHandler[] = [\n ...(ctxMw ? [ctxMw] : []),\n ...globalMws,\n ...derived,\n ...routeSpecific,\n ];\n\n const wrapped: RequestHandler = async (req, res, next) => {\n const requestUrl = req.originalUrl ?? path;\n const startedAt = Date.now();\n emitDebug({ type: 'request', stage: 'start', method: methodUpper, path, url: requestUrl });\n let params: ArgParams<L> | undefined;\n let query: ArgQuery<L> | undefined;\n let body: ArgBody<L> | undefined;\n let responsePayload: InferOutput<L> | undefined;\n let hasResponsePayload = false;\n setAfterHandlerNext(res, false);\n let handlerCalledNext = false;\n const downstreamNext: express.NextFunction = (err?: any) => {\n handlerCalledNext = true;\n setAfterHandlerNext(res, true);\n next(err);\n };\n\n try {\n logger?.info?.(`${methodUpper}@${path} (${requestUrl})`);\n\n const ctx = (res.locals as any)[CTX_SYMBOL] as Ctx;\n\n params = (\n leaf.cfg.paramsSchema\n ? (leaf.cfg.paramsSchema as ZodType).parse(req.params)\n : Object.keys(req.params || {}).length\n ? (req.params as any)\n : undefined\n ) as ArgParams<L>;\n\n try {\n query = leaf.cfg.querySchema\n ? (leaf.cfg.querySchema as ZodType).parse(req.query)\n : Object.keys(req.query || {}).length\n ? (req.query as any)\n : undefined;\n } catch (e) {\n logger?.error?.('Query parsing error', {\n path,\n method: methodUpper,\n error: e,\n raw: JSON.stringify(req.query),\n });\n throw e;\n }\n\n body = (\n leaf.cfg.bodySchema\n ? (leaf.cfg.bodySchema as ZodType).parse(req.body)\n : req.body !== undefined\n ? (req.body as any)\n : undefined\n ) as ArgBody<L>;\n\n logger?.verbose?.(`${methodUpper}@${path} (${requestUrl})`, {\n params,\n query,\n body,\n });\n\n let result;\n try {\n result = await def.handler({\n req,\n res,\n next: downstreamNext,\n ctx,\n params: params as ArgParams<L>,\n query: query as ArgQuery<L>,\n body: body as ArgBody<L>,\n });\n } catch (e) {\n logger?.error?.('Handler error', e);\n throw e;\n }\n\n if (!res.headersSent) {\n if (result !== undefined) {\n const out =\n validateOutput && leaf.cfg.outputSchema\n ? (leaf.cfg.outputSchema as ZodType).parse(result)\n : result;\n responsePayload = out as InferOutput<L>;\n hasResponsePayload = true;\n logger?.verbose?.(`${methodUpper}@${path} result`, out);\n send(res, out);\n } else if (!handlerCalledNext) {\n next();\n }\n }\n\n emitDebug(\n decorateDebugEvent(\n {\n type: 'request',\n stage: 'success',\n method: methodUpper,\n path,\n url: requestUrl,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? {\n params,\n query,\n body,\n ...(hasResponsePayload ? { output: responsePayload } : {}),\n }\n : undefined,\n ),\n );\n } catch (err) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'request',\n stage: 'error',\n method: methodUpper,\n path,\n url: requestUrl,\n durationMs: Date.now() - startedAt,\n error: err,\n },\n isVerboseDebug ? { params, query, body } : undefined,\n ),\n );\n logger?.error?.('Route error', err);\n next(err as any);\n }\n };\n\n const after = (def?.after ?? []).map((mw) => {\n const adapted = adaptCtxMw<Ctx>(mw);\n return (req: express.Request, res: express.Response, next: express.NextFunction) => {\n if (!handlerInvokedNext(res)) {\n next();\n return;\n }\n adapted(req, res, next);\n };\n });\n (router as any)[method](path, ...before, wrapped, ...after);\n registered.add(key);\n }\n\n /**\n * Register controller definitions for the provided keys.\n * @param registry Finalized registry of leaves.\n * @param controllers Partial controller map keyed by `\"METHOD /path\"`.\n */\n function registerControllers<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n >(registry: R, controllers: PartialControllerMap<R, Ctx>) {\n (Object.keys(controllers) as Array<KeysOfRegistry<R>>).forEach((key) => {\n const leaf = registry.byKey[key] as unknown as LeafFromKey<R, typeof key> | undefined;\n if (!leaf) {\n logger?.warn?.(`No leaf found for controller key: ${key}. Not registering route.`);\n return;\n }\n const def = controllers[key];\n if (!def) return;\n register(leaf as LeafFromKey<R, typeof key>, def);\n });\n }\n\n /**\n * Warn about leaves that do not have a registered controller.\n * @param registry Finalized registry of leaves.\n * @param warnLogger Logger used for warning output.\n */\n function warnMissing<R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }>(\n registry: R,\n warnLogger: { warn: (...args: any[]) => void },\n ) {\n const registeredFromStore = new Set<string>(Array.from(registered));\n if (registeredFromStore.size === 0) {\n collectRoutesFromStack(router).forEach((key) => registeredFromStore.add(key));\n }\n for (const leaf of registry.all) {\n const key = keyOf(leaf);\n if (!registeredFromStore.has(key)) {\n warnLogger.warn(`No controller registered for route: ${key}`);\n }\n }\n }\n\n return {\n router,\n register,\n registerControllers,\n warnMissingControllers: warnMissing,\n getRegisteredKeys: () => Array.from(registered),\n };\n}\n\n/**\n * Bind only the controllers that are present in the provided map.\n * @param router Express router or app.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param controllers Partial map of controllers keyed by `\"METHOD /path\"`.\n * @param config Optional route server configuration.\n * @returns The same router instance for chaining.\n */\nexport function bindExpressRoutes<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n>(\n router: Router,\n registry: R,\n controllers: PartialControllerMap<R, Ctx>,\n config: RouteServerConfig<Ctx>,\n) {\n const server = createRouteServer<Ctx>(router, config);\n server.registerControllers(registry, controllers);\n return router;\n}\n\n/**\n * Bind controllers for every leaf. Missing entries fail at compile time.\n * @param router Express router or app.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param controllers Complete map of controllers keyed by `\"METHOD /path\"`.\n * @param config Optional route server configuration.\n * @returns The same router instance for chaining.\n */\nexport function bindAll<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n Ctx = unknown,\n>(\n router: Router,\n registry: R,\n controllers: { [K in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, K>, Ctx> },\n config: RouteServerConfig<Ctx>,\n) {\n const server = createRouteServer<Ctx>(router, config);\n server.registerControllers(registry, controllers);\n return router;\n}\n\n// ──────────────────────────────────────────────────────────────────────────────\n// DX helpers\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Helper for great IntelliSense when authoring controller maps.\n * @returns Function that enforces key names while preserving partial flexibility.\n */\nexport const defineControllers =\n <R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> }, Ctx = unknown>() =>\n <M extends PartialControllerMap<R, Ctx>>(m: M) =>\n m;\n\n/**\n * Wrap a plain RequestHandler as an auth factory compatible with `fromCfg.auth`.\n * @param mw Middleware invoked for any leaf that requires authentication.\n * @param _leaf Leaf metadata (ignored, but provided to match factory signature).\n * @returns Factory that ignores the leaf and returns the same middleware.\n */\nexport const asLeafAuth =\n (mw: RequestHandler) =>\n (_leaf: AnyLeaf): RequestHandler =>\n mw;\n\n/**\n * Warn about leaves that don't have controllers.\n * Call this during startup to surface missing routes.\n * @param router Express router or app to inspect.\n * @param registry Finalized registry produced by `finalize(...)`.\n * @param logger Logger where warnings are emitted.\n */\nexport function warnMissingControllers<\n R extends { all: readonly AnyLeaf[]; byKey: Record<string, AnyLeaf> },\n>(router: Router, registry: R, logger: { warn: (...args: any[]) => void }) {\n const registeredStore = (router as any)[REGISTERED_ROUTES_SYMBOL] as Set<string> | undefined;\n const initial = registeredStore ? Array.from(registeredStore) : collectRoutesFromStack(router);\n const registeredKeys = new Set<string>(initial);\n\n for (const leaf of registry.all) {\n const k = keyOf(leaf);\n if (!registeredKeys.has(k)) {\n logger.warn(`No controller registered for route: ${k}`);\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -136,7 +136,7 @@ export type RouteServerConfig<Ctx = unknown> = {
136
136
  * *first* (before global/derived/route middlewares), and stash it on
137
137
  * `res.locals[CTX_SYMBOL]` so *all* later middlewares can use it.
138
138
  */
139
- buildCtx?: (req: express.Request, res: express.Response) => Ctx | Promise<Ctx>;
139
+ buildCtx: (req: express.Request, res: express.Response) => Ctx | Promise<Ctx>;
140
140
  /**
141
141
  * Global middlewares for every bound route (run *after* buildCtx).
142
142
  * You can write them as ctx-aware middlewares for great DX.
@@ -165,7 +165,7 @@ export type RouteServerConfig<Ctx = unknown> = {
165
165
  };
166
166
  /** Runtime helpers returned by `createRouteServer`. */
167
167
  export type RouteServer<Ctx = unknown> = {
168
- router: Router | express.Application;
168
+ router: Router;
169
169
  register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>): void;
170
170
  registerControllers<R extends {
171
171
  all: readonly AnyLeaf[];
@@ -185,7 +185,7 @@ export type RouteServer<Ctx = unknown> = {
185
185
  * @param config Optional configuration controlling ctx building, auth, uploads, etc.
186
186
  * @returns Object with helpers to register controllers and inspect registered keys.
187
187
  */
188
- export declare function createRouteServer<Ctx = unknown>(router: Router | express.Application, config?: RouteServerConfig<Ctx>): RouteServer<Ctx>;
188
+ export declare function createRouteServer<Ctx = unknown>(router: Router, config: RouteServerConfig<Ctx>): RouteServer<Ctx>;
189
189
  /**
190
190
  * Bind only the controllers that are present in the provided map.
191
191
  * @param router Express router or app.
@@ -197,7 +197,7 @@ export declare function createRouteServer<Ctx = unknown>(router: Router | expres
197
197
  export declare function bindExpressRoutes<R extends {
198
198
  all: readonly AnyLeaf[];
199
199
  byKey: Record<string, AnyLeaf>;
200
- }, Ctx = unknown>(router: Router | express.Application, registry: R, controllers: PartialControllerMap<R, Ctx>, config?: RouteServerConfig<Ctx>): express.Router | express.Application;
200
+ }, Ctx = unknown>(router: Router, registry: R, controllers: PartialControllerMap<R, Ctx>, config: RouteServerConfig<Ctx>): express.Router;
201
201
  /**
202
202
  * Bind controllers for every leaf. Missing entries fail at compile time.
203
203
  * @param router Express router or app.
@@ -209,9 +209,9 @@ export declare function bindExpressRoutes<R extends {
209
209
  export declare function bindAll<R extends {
210
210
  all: readonly AnyLeaf[];
211
211
  byKey: Record<string, AnyLeaf>;
212
- }, Ctx = unknown>(router: Router | express.Application, registry: R, controllers: {
212
+ }, Ctx = unknown>(router: Router, registry: R, controllers: {
213
213
  [K in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, K>, Ctx>;
214
- }, config?: RouteServerConfig<Ctx>): express.Router | express.Application;
214
+ }, config: RouteServerConfig<Ctx>): express.Router;
215
215
  /**
216
216
  * Helper for great IntelliSense when authoring controller maps.
217
217
  * @returns Function that enforces key names while preserving partial flexibility.
@@ -237,6 +237,6 @@ export declare const asLeafAuth: (mw: RequestHandler) => (_leaf: AnyLeaf) => Req
237
237
  export declare function warnMissingControllers<R extends {
238
238
  all: readonly AnyLeaf[];
239
239
  byKey: Record<string, AnyLeaf>;
240
- }>(router: Router | express.Application, registry: R, logger: {
240
+ }>(router: Router, registry: R, logger: {
241
241
  warn: (...args: any[]) => void;
242
242
  }): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/rrroutes-server",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",