@emeryld/rrroutes-server 1.1.0

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/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @emeryld/rrroutes-server
2
+
3
+ Express bindings for RRRoutes contracts. This package maps finalized leaves to an Express router, validating params/query/body/output with Zod, wiring ctx-aware middleware, and warning about missing controllers.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ pnpm add @emeryld/rrroutes-server express
9
+ # or
10
+ npm install @emeryld/rrroutes-server express
11
+ ```
12
+
13
+ The server package depends on `@emeryld/rrroutes-contract` and `zod` internally.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import express from 'express';
19
+ import { createRouteServer } from '@emeryld/rrroutes-server';
20
+ import { registry } from '../routes';
21
+ import { controllers } from './controllers';
22
+
23
+ const router = express.Router();
24
+
25
+ const server = createRouteServer(router, {
26
+ baseUrl: '/api',
27
+ buildCtx: async (req) => ({ user: await loadUser(req) }),
28
+ fromCfg: {
29
+ auth: (leaf) => (req, res, next) => ensureAuth(leaf, req, res, next),
30
+ },
31
+ });
32
+
33
+ server.registerControllers(registry, controllers);
34
+ server.warnMissingControllers(registry, console);
35
+ ```
36
+
37
+ ## Scripts
38
+
39
+ ```sh
40
+ pnpm install
41
+ pnpm --filter @emeryld/rrroutes-server build
42
+ pnpm --filter @emeryld/rrroutes-server test
43
+ ```
44
+
45
+ ## Publishing
46
+
47
+ ```sh
48
+ cd packages/server
49
+ npm publish --access public
50
+ ```
51
+
52
+ Double-check `packages/server/package.json` for the version bump and ensure the bundled `dist/` output is up to date before publishing.
package/dist/index.cjs ADDED
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CTX_SYMBOL: () => CTX_SYMBOL,
24
+ asLeafAuth: () => asLeafAuth,
25
+ bindAll: () => bindAll,
26
+ bindExpressRoutes: () => bindExpressRoutes,
27
+ createRouteServer: () => createRouteServer,
28
+ defineControllers: () => defineControllers,
29
+ getCtx: () => getCtx,
30
+ keyOf: () => keyOf,
31
+ warnMissingControllers: () => warnMissingControllers
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/routesV3.server.ts
36
+ var keyOf = (leaf) => `${leaf.method.toUpperCase()} ${leaf.path}`;
37
+ var CTX_SYMBOL = Symbol.for("typedLeaves.ctx");
38
+ function getCtx(res) {
39
+ return res.locals[CTX_SYMBOL];
40
+ }
41
+ function adaptCtxMw(mw) {
42
+ return (req, res, next) => mw({ req, res, next, ctx: getCtx(res) });
43
+ }
44
+ var defaultSend = (res, data) => {
45
+ res.json(data);
46
+ };
47
+ function resolveAuth(auth, leaf) {
48
+ if (!auth) return void 0;
49
+ return auth.length === 1 ? auth(leaf) : auth;
50
+ }
51
+ var REGISTERED_ROUTES_SYMBOL = Symbol.for("routesV3.registeredRoutes");
52
+ function getRegisteredRouteStore(router) {
53
+ const existing = router[REGISTERED_ROUTES_SYMBOL];
54
+ if (existing) return existing;
55
+ const store = /* @__PURE__ */ new Set();
56
+ router[REGISTERED_ROUTES_SYMBOL] = store;
57
+ return store;
58
+ }
59
+ function collectRoutesFromStack(appOrRouter) {
60
+ const result = [];
61
+ const stack = appOrRouter.stack ?? (appOrRouter._router ? appOrRouter._router.stack : void 0) ?? [];
62
+ if (!Array.isArray(stack)) return result;
63
+ for (const layer of stack) {
64
+ const route = layer && layer.route;
65
+ if (!route) continue;
66
+ const paths = Array.isArray(route.path) ? route.path : [route.path];
67
+ const methodEntries = Object.entries(route.methods ?? {}).filter(([, enabled]) => enabled);
68
+ for (const path of paths) {
69
+ for (const [method] of methodEntries) {
70
+ result.push(`${method.toUpperCase()} ${path}`);
71
+ }
72
+ }
73
+ }
74
+ return result;
75
+ }
76
+ function createRouteServer(router, config) {
77
+ const validateOutput = config?.validateOutput ?? true;
78
+ const send = config?.send ?? defaultSend;
79
+ const logger = config?.logger;
80
+ const ctxMw = config?.buildCtx ? async (req, res, next) => {
81
+ try {
82
+ const ctx = await config.buildCtx(req, res);
83
+ res.locals[CTX_SYMBOL] = ctx;
84
+ next();
85
+ } catch (err) {
86
+ logger?.error?.("buildCtx error", err);
87
+ next(err);
88
+ }
89
+ } : void 0;
90
+ const globalMws = (config?.global ?? []).map((mw) => adaptCtxMw(mw));
91
+ const registered = getRegisteredRouteStore(router);
92
+ const buildDerived = (leaf) => {
93
+ const derived = [];
94
+ const decision = config?.fromCfg?.when?.(leaf.cfg, leaf) ?? {};
95
+ const needsAuth = typeof decision.auth === "boolean" ? decision.auth : !!leaf.cfg.authenticated;
96
+ if (needsAuth && config?.fromCfg?.auth) {
97
+ const authMw = resolveAuth(config.fromCfg.auth, leaf);
98
+ if (authMw) derived.push(authMw);
99
+ }
100
+ if (config?.fromCfg?.upload && Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0) {
101
+ derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles, leaf));
102
+ }
103
+ return derived;
104
+ };
105
+ function register(leaf, def) {
106
+ const method = leaf.method;
107
+ const path = leaf.path;
108
+ const key = keyOf(leaf);
109
+ const routeSpecific = (def?.use ?? []).map((mw) => adaptCtxMw(mw));
110
+ const derived = buildDerived(leaf);
111
+ const before = [
112
+ ...ctxMw ? [ctxMw] : [],
113
+ ...globalMws,
114
+ ...derived,
115
+ ...routeSpecific
116
+ ];
117
+ const wrapped = async (req, res, next) => {
118
+ try {
119
+ logger?.info?.(`${method.toUpperCase()}@${path} (${req.originalUrl})`);
120
+ const ctx = res.locals[CTX_SYMBOL];
121
+ const params = leaf.cfg.paramsSchema ? leaf.cfg.paramsSchema.parse(req.params) : Object.keys(req.params || {}).length ? req.params : void 0;
122
+ let query;
123
+ try {
124
+ query = leaf.cfg.querySchema ? leaf.cfg.querySchema.parse(req.query) : Object.keys(req.query || {}).length ? req.query : void 0;
125
+ } catch (e) {
126
+ logger?.error?.("Query parsing error", {
127
+ path,
128
+ method,
129
+ error: e,
130
+ raw: JSON.stringify(req.query)
131
+ });
132
+ throw e;
133
+ }
134
+ const body = leaf.cfg.bodySchema ? leaf.cfg.bodySchema.parse(req.body) : req.body !== void 0 ? req.body : void 0;
135
+ logger?.verbose?.(`${method.toUpperCase()}@${path} (${req.originalUrl})`, {
136
+ params,
137
+ query,
138
+ body
139
+ });
140
+ let result;
141
+ try {
142
+ result = await def.handler({ req, res, next, ctx, params, query, body });
143
+ } catch (e) {
144
+ logger?.error?.("Handler error", e);
145
+ throw e;
146
+ }
147
+ if (!res.headersSent && result !== void 0) {
148
+ const out = validateOutput && leaf.cfg.outputSchema ? leaf.cfg.outputSchema.parse(result) : result;
149
+ logger?.verbose?.(`${method.toUpperCase()}@${path} result`, out);
150
+ send(res, out);
151
+ }
152
+ } catch (err) {
153
+ logger?.error?.("Route error", err);
154
+ next(err);
155
+ }
156
+ };
157
+ const after = (def?.after ?? []).map((mw) => adaptCtxMw(mw));
158
+ router[method](path, ...before, wrapped, ...after);
159
+ registered.add(key);
160
+ }
161
+ function registerControllers(registry, controllers) {
162
+ Object.keys(controllers).forEach((key) => {
163
+ const leaf = registry.byKey[key];
164
+ if (!leaf) {
165
+ logger?.warn?.(`No leaf found for controller key: ${key}. Not registering route.`);
166
+ return;
167
+ }
168
+ const def = controllers[key];
169
+ if (!def) return;
170
+ register(leaf, def);
171
+ });
172
+ }
173
+ function warnMissing(registry, warnLogger) {
174
+ const registeredFromStore = new Set(Array.from(registered));
175
+ if (registeredFromStore.size === 0) {
176
+ collectRoutesFromStack(router).forEach((key) => registeredFromStore.add(key));
177
+ }
178
+ for (const leaf of registry.all) {
179
+ const key = keyOf(leaf);
180
+ if (!registeredFromStore.has(key)) {
181
+ warnLogger.warn(`No controller registered for route: ${key}`);
182
+ }
183
+ }
184
+ }
185
+ return {
186
+ router,
187
+ register,
188
+ registerControllers,
189
+ warnMissingControllers: warnMissing,
190
+ getRegisteredKeys: () => Array.from(registered)
191
+ };
192
+ }
193
+ function bindExpressRoutes(router, registry, controllers, config) {
194
+ const server = createRouteServer(router, config);
195
+ server.registerControllers(registry, controllers);
196
+ return router;
197
+ }
198
+ function bindAll(router, registry, controllers, config) {
199
+ const server = createRouteServer(router, config);
200
+ server.registerControllers(registry, controllers);
201
+ return router;
202
+ }
203
+ var defineControllers = () => (m) => m;
204
+ var asLeafAuth = (mw) => (_leaf) => mw;
205
+ function warnMissingControllers(router, registry, logger) {
206
+ const registeredStore = router[REGISTERED_ROUTES_SYMBOL];
207
+ const initial = registeredStore ? Array.from(registeredStore) : collectRoutesFromStack(router);
208
+ const registeredKeys = new Set(initial);
209
+ for (const leaf of registry.all) {
210
+ const k = keyOf(leaf);
211
+ if (!registeredKeys.has(k)) {
212
+ logger.warn(`No controller registered for route: ${k}`);
213
+ }
214
+ }
215
+ }
216
+ // Annotate the CommonJS export names for ESM import in node:
217
+ 0 && (module.exports = {
218
+ CTX_SYMBOL,
219
+ asLeafAuth,
220
+ bindAll,
221
+ bindExpressRoutes,
222
+ createRouteServer,
223
+ defineControllers,
224
+ getCtx,
225
+ keyOf,
226
+ warnMissingControllers
227
+ });
228
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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// ──────────────────────────────────────────────────────────────────────────────\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\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\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 path = leaf.path as string;\n const key = keyOf(leaf);\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 try {\n logger?.info?.(`${method.toUpperCase()}@${path} (${req.originalUrl})`);\n\n const ctx = (res.locals as any)[CTX_SYMBOL] as Ctx;\n\n const 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<typeof leaf>;\n\n let query: ArgQuery<typeof leaf>;\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,\n error: e,\n raw: JSON.stringify(req.query),\n });\n throw e;\n }\n\n const 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<typeof leaf>;\n\n logger?.verbose?.(`${method.toUpperCase()}@${path} (${req.originalUrl})`, {\n params,\n query,\n body,\n });\n\n let result;\n try {\n result = await def.handler({ req, res, next, ctx, params, query, body });\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 logger?.verbose?.(`${method.toUpperCase()}@${path} result`, out);\n send(res, out);\n }\n } catch (err) {\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":[]}
@@ -0,0 +1 @@
1
+ export * from './routesV3.server';
package/dist/index.js ADDED
@@ -0,0 +1,193 @@
1
+ // src/routesV3.server.ts
2
+ var keyOf = (leaf) => `${leaf.method.toUpperCase()} ${leaf.path}`;
3
+ var CTX_SYMBOL = Symbol.for("typedLeaves.ctx");
4
+ function getCtx(res) {
5
+ return res.locals[CTX_SYMBOL];
6
+ }
7
+ function adaptCtxMw(mw) {
8
+ return (req, res, next) => mw({ req, res, next, ctx: getCtx(res) });
9
+ }
10
+ var defaultSend = (res, data) => {
11
+ res.json(data);
12
+ };
13
+ function resolveAuth(auth, leaf) {
14
+ if (!auth) return void 0;
15
+ return auth.length === 1 ? auth(leaf) : auth;
16
+ }
17
+ var REGISTERED_ROUTES_SYMBOL = Symbol.for("routesV3.registeredRoutes");
18
+ function getRegisteredRouteStore(router) {
19
+ const existing = router[REGISTERED_ROUTES_SYMBOL];
20
+ if (existing) return existing;
21
+ const store = /* @__PURE__ */ new Set();
22
+ router[REGISTERED_ROUTES_SYMBOL] = store;
23
+ return store;
24
+ }
25
+ function collectRoutesFromStack(appOrRouter) {
26
+ const result = [];
27
+ const stack = appOrRouter.stack ?? (appOrRouter._router ? appOrRouter._router.stack : void 0) ?? [];
28
+ if (!Array.isArray(stack)) return result;
29
+ for (const layer of stack) {
30
+ const route = layer && layer.route;
31
+ if (!route) continue;
32
+ const paths = Array.isArray(route.path) ? route.path : [route.path];
33
+ const methodEntries = Object.entries(route.methods ?? {}).filter(([, enabled]) => enabled);
34
+ for (const path of paths) {
35
+ for (const [method] of methodEntries) {
36
+ result.push(`${method.toUpperCase()} ${path}`);
37
+ }
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ function createRouteServer(router, config) {
43
+ const validateOutput = config?.validateOutput ?? true;
44
+ const send = config?.send ?? defaultSend;
45
+ const logger = config?.logger;
46
+ const ctxMw = config?.buildCtx ? async (req, res, next) => {
47
+ try {
48
+ const ctx = await config.buildCtx(req, res);
49
+ res.locals[CTX_SYMBOL] = ctx;
50
+ next();
51
+ } catch (err) {
52
+ logger?.error?.("buildCtx error", err);
53
+ next(err);
54
+ }
55
+ } : void 0;
56
+ const globalMws = (config?.global ?? []).map((mw) => adaptCtxMw(mw));
57
+ const registered = getRegisteredRouteStore(router);
58
+ const buildDerived = (leaf) => {
59
+ const derived = [];
60
+ const decision = config?.fromCfg?.when?.(leaf.cfg, leaf) ?? {};
61
+ const needsAuth = typeof decision.auth === "boolean" ? decision.auth : !!leaf.cfg.authenticated;
62
+ if (needsAuth && config?.fromCfg?.auth) {
63
+ const authMw = resolveAuth(config.fromCfg.auth, leaf);
64
+ if (authMw) derived.push(authMw);
65
+ }
66
+ if (config?.fromCfg?.upload && Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0) {
67
+ derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles, leaf));
68
+ }
69
+ return derived;
70
+ };
71
+ function register(leaf, def) {
72
+ const method = leaf.method;
73
+ const path = leaf.path;
74
+ const key = keyOf(leaf);
75
+ const routeSpecific = (def?.use ?? []).map((mw) => adaptCtxMw(mw));
76
+ const derived = buildDerived(leaf);
77
+ const before = [
78
+ ...ctxMw ? [ctxMw] : [],
79
+ ...globalMws,
80
+ ...derived,
81
+ ...routeSpecific
82
+ ];
83
+ const wrapped = async (req, res, next) => {
84
+ try {
85
+ logger?.info?.(`${method.toUpperCase()}@${path} (${req.originalUrl})`);
86
+ const ctx = res.locals[CTX_SYMBOL];
87
+ const params = leaf.cfg.paramsSchema ? leaf.cfg.paramsSchema.parse(req.params) : Object.keys(req.params || {}).length ? req.params : void 0;
88
+ let query;
89
+ try {
90
+ query = leaf.cfg.querySchema ? leaf.cfg.querySchema.parse(req.query) : Object.keys(req.query || {}).length ? req.query : void 0;
91
+ } catch (e) {
92
+ logger?.error?.("Query parsing error", {
93
+ path,
94
+ method,
95
+ error: e,
96
+ raw: JSON.stringify(req.query)
97
+ });
98
+ throw e;
99
+ }
100
+ const body = leaf.cfg.bodySchema ? leaf.cfg.bodySchema.parse(req.body) : req.body !== void 0 ? req.body : void 0;
101
+ logger?.verbose?.(`${method.toUpperCase()}@${path} (${req.originalUrl})`, {
102
+ params,
103
+ query,
104
+ body
105
+ });
106
+ let result;
107
+ try {
108
+ result = await def.handler({ req, res, next, ctx, params, query, body });
109
+ } catch (e) {
110
+ logger?.error?.("Handler error", e);
111
+ throw e;
112
+ }
113
+ if (!res.headersSent && result !== void 0) {
114
+ const out = validateOutput && leaf.cfg.outputSchema ? leaf.cfg.outputSchema.parse(result) : result;
115
+ logger?.verbose?.(`${method.toUpperCase()}@${path} result`, out);
116
+ send(res, out);
117
+ }
118
+ } catch (err) {
119
+ logger?.error?.("Route error", err);
120
+ next(err);
121
+ }
122
+ };
123
+ const after = (def?.after ?? []).map((mw) => adaptCtxMw(mw));
124
+ router[method](path, ...before, wrapped, ...after);
125
+ registered.add(key);
126
+ }
127
+ function registerControllers(registry, controllers) {
128
+ Object.keys(controllers).forEach((key) => {
129
+ const leaf = registry.byKey[key];
130
+ if (!leaf) {
131
+ logger?.warn?.(`No leaf found for controller key: ${key}. Not registering route.`);
132
+ return;
133
+ }
134
+ const def = controllers[key];
135
+ if (!def) return;
136
+ register(leaf, def);
137
+ });
138
+ }
139
+ function warnMissing(registry, warnLogger) {
140
+ const registeredFromStore = new Set(Array.from(registered));
141
+ if (registeredFromStore.size === 0) {
142
+ collectRoutesFromStack(router).forEach((key) => registeredFromStore.add(key));
143
+ }
144
+ for (const leaf of registry.all) {
145
+ const key = keyOf(leaf);
146
+ if (!registeredFromStore.has(key)) {
147
+ warnLogger.warn(`No controller registered for route: ${key}`);
148
+ }
149
+ }
150
+ }
151
+ return {
152
+ router,
153
+ register,
154
+ registerControllers,
155
+ warnMissingControllers: warnMissing,
156
+ getRegisteredKeys: () => Array.from(registered)
157
+ };
158
+ }
159
+ function bindExpressRoutes(router, registry, controllers, config) {
160
+ const server = createRouteServer(router, config);
161
+ server.registerControllers(registry, controllers);
162
+ return router;
163
+ }
164
+ function bindAll(router, registry, controllers, config) {
165
+ const server = createRouteServer(router, config);
166
+ server.registerControllers(registry, controllers);
167
+ return router;
168
+ }
169
+ var defineControllers = () => (m) => m;
170
+ var asLeafAuth = (mw) => (_leaf) => mw;
171
+ function warnMissingControllers(router, registry, logger) {
172
+ const registeredStore = router[REGISTERED_ROUTES_SYMBOL];
173
+ const initial = registeredStore ? Array.from(registeredStore) : collectRoutesFromStack(router);
174
+ const registeredKeys = new Set(initial);
175
+ for (const leaf of registry.all) {
176
+ const k = keyOf(leaf);
177
+ if (!registeredKeys.has(k)) {
178
+ logger.warn(`No controller registered for route: ${k}`);
179
+ }
180
+ }
181
+ }
182
+ export {
183
+ CTX_SYMBOL,
184
+ asLeafAuth,
185
+ bindAll,
186
+ bindExpressRoutes,
187
+ createRouteServer,
188
+ defineControllers,
189
+ getCtx,
190
+ keyOf,
191
+ warnMissingControllers
192
+ };
193
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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// ──────────────────────────────────────────────────────────────────────────────\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\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\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 path = leaf.path as string;\n const key = keyOf(leaf);\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 try {\n logger?.info?.(`${method.toUpperCase()}@${path} (${req.originalUrl})`);\n\n const ctx = (res.locals as any)[CTX_SYMBOL] as Ctx;\n\n const 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<typeof leaf>;\n\n let query: ArgQuery<typeof leaf>;\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,\n error: e,\n raw: JSON.stringify(req.query),\n });\n throw e;\n }\n\n const 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<typeof leaf>;\n\n logger?.verbose?.(`${method.toUpperCase()}@${path} (${req.originalUrl})`, {\n params,\n query,\n body,\n });\n\n let result;\n try {\n result = await def.handler({ req, res, next, ctx, params, query, body });\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 logger?.verbose?.(`${method.toUpperCase()}@${path} result`, out);\n send(res, out);\n }\n } catch (err) {\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":";AAyEO,IAAM,QAAQ,CAAC,SAAkB,GAAG,KAAK,OAAO,YAAY,CAAC,IAAI,KAAK,IAAI;AAU1E,IAAM,aAA4B,OAAO,IAAI,iBAAiB;AAsB9D,SAAS,OAAsB,KAA4B;AAChE,SAAQ,IAAI,OAAe,UAAU;AACvC;AAOA,SAAS,WAAgB,IAA4C;AACnE,SAAO,CAAC,KAAK,KAAK,SAAS,GAAG,EAAE,KAAK,KAAK,MAAM,KAAK,OAAY,GAAG,EAAE,CAAC;AACzE;AAgFA,IAAM,cAA8D,CAAC,KAAK,SAAS;AACjF,MAAI,KAAK,IAAW;AACtB;AAQA,SAAS,YACP,MACA,MAC4B;AAC5B,MAAI,CAAC,KAAM,QAAO;AAClB,SAAQ,KAAwC,WAAW,IACtD,KAAwC,IAAI,IAC5C;AACP;AAMA,IAAM,2BAA2B,OAAO,IAAI,2BAA2B;AASvE,SAAS,wBAAwB,QAA4D;AAC3F,QAAM,WAAY,OAAe,wBAAwB;AACzD,MAAI,SAAU,QAAO;AACrB,QAAM,QAA8B,oBAAI,IAAI;AAC5C,EAAC,OAAe,wBAAwB,IAAI;AAC5C,SAAO;AACT;AAOA,SAAS,uBAAuB,aAAqD;AACnF,QAAM,SAAmB,CAAC;AAC1B,QAAM,QACH,YAAoB,UACnB,YAAoB,UAAW,YAAoB,QAAQ,QAAQ,WACrE,CAAC;AAEH,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAElC,aAAW,SAAS,OAAO;AACzB,UAAM,QAAQ,SAAS,MAAM;AAC7B,QAAI,CAAC,MAAO;AAEZ,UAAM,QAAQ,MAAM,QAAQ,MAAM,IAAI,IAAI,MAAM,OAAO,CAAC,MAAM,IAAI;AAClE,UAAM,gBAAgB,OAAO,QAAQ,MAAM,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,OAAO,MAAM,OAAO;AAEzF,eAAW,QAAQ,OAAO;AACxB,iBAAW,CAAC,MAAM,KAAK,eAAe;AACpC,eAAO,KAAK,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI,EAAE;AAAA,MAC/C;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAuBO,SAAS,kBACd,QACA,QACkB;AAClB,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,SAAS,QAAQ;AAEvB,QAAM,QAAoC,QAAQ,WAC9C,OAAO,KAAK,KAAK,SAAS;AACxB,QAAI;AACF,YAAM,MAAM,MAAM,OAAO,SAAU,KAAK,GAAG;AAC3C,MAAC,IAAI,OAAe,UAAU,IAAI;AAClC,WAAK;AAAA,IACP,SAAS,KAAK;AACZ,cAAQ,QAAQ,kBAAkB,GAAG;AACrC,WAAK,GAAU;AAAA,IACjB;AAAA,EACF,IACA;AAEJ,QAAM,aAAa,QAAQ,UAAU,CAAC,GAAG,IAAI,CAAC,OAAO,WAAgB,EAAE,CAAC;AACxE,QAAM,aAAa,wBAAwB,MAAM;AAEjD,QAAM,eAAe,CAAC,SAAoC;AACxD,UAAM,UAA4B,CAAC;AACnC,UAAM,WAAW,QAAQ,SAAS,OAAO,KAAK,KAAK,IAAI,KAAK,CAAC;AAC7D,UAAM,YAAY,OAAO,SAAS,SAAS,YAAY,SAAS,OAAO,CAAC,CAAC,KAAK,IAAI;AAElF,QAAI,aAAa,QAAQ,SAAS,MAAM;AACtC,YAAM,SAAS,YAAY,OAAO,QAAQ,MAAM,IAAI;AACpD,UAAI,OAAQ,SAAQ,KAAK,MAAM;AAAA,IACjC;AAEA,QACE,QAAQ,SAAS,UACjB,MAAM,QAAQ,KAAK,IAAI,SAAS,KAChC,KAAK,IAAI,UAAU,SAAS,GAC5B;AACA,cAAQ,KAAK,GAAG,OAAO,QAAQ,OAAO,KAAK,IAAI,WAAW,IAAI,CAAC;AAAA,IACjE;AAEA,WAAO;AAAA,EACT;AAGA,WAAS,SAA4B,MAAS,KAAuB;AACnE,UAAM,SAAS,KAAK;AACpB,UAAM,OAAO,KAAK;AAClB,UAAM,MAAM,MAAM,IAAI;AAEtB,UAAM,iBAAiB,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,WAAgB,EAAE,CAAC;AACtE,UAAM,UAAU,aAAa,IAAI;AACjC,UAAM,SAA2B;AAAA,MAC/B,GAAI,QAAQ,CAAC,KAAK,IAAI,CAAC;AAAA,MACvB,GAAG;AAAA,MACH,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAEA,UAAM,UAA0B,OAAO,KAAK,KAAK,SAAS;AACxD,UAAI;AACF,gBAAQ,OAAO,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI,KAAK,IAAI,WAAW,GAAG;AAErE,cAAM,MAAO,IAAI,OAAe,UAAU;AAE1C,cAAM,SACJ,KAAK,IAAI,eACJ,KAAK,IAAI,aAAyB,MAAM,IAAI,MAAM,IACnD,OAAO,KAAK,IAAI,UAAU,CAAC,CAAC,EAAE,SAC3B,IAAI,SACL;AAGR,YAAI;AACJ,YAAI;AACF,kBAAQ,KAAK,IAAI,cACZ,KAAK,IAAI,YAAwB,MAAM,IAAI,KAAK,IACjD,OAAO,KAAK,IAAI,SAAS,CAAC,CAAC,EAAE,SAC1B,IAAI,QACL;AAAA,QACR,SAAS,GAAG;AACV,kBAAQ,QAAQ,uBAAuB;AAAA,YACrC;AAAA,YACA;AAAA,YACA,OAAO;AAAA,YACP,KAAK,KAAK,UAAU,IAAI,KAAK;AAAA,UAC/B,CAAC;AACD,gBAAM;AAAA,QACR;AAEA,cAAM,OACJ,KAAK,IAAI,aACJ,KAAK,IAAI,WAAuB,MAAM,IAAI,IAAI,IAC/C,IAAI,SAAS,SACV,IAAI,OACL;AAGR,gBAAQ,UAAU,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI,KAAK,IAAI,WAAW,KAAK;AAAA,UACxE;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAED,YAAI;AACJ,YAAI;AACF,mBAAS,MAAM,IAAI,QAAQ,EAAE,KAAK,KAAK,MAAM,KAAK,QAAQ,OAAO,KAAK,CAAC;AAAA,QACzE,SAAS,GAAG;AACV,kBAAQ,QAAQ,iBAAiB,CAAC;AAClC,gBAAM;AAAA,QACR;AAEA,YAAI,CAAC,IAAI,eAAe,WAAW,QAAW;AAC5C,gBAAM,MACJ,kBAAkB,KAAK,IAAI,eACtB,KAAK,IAAI,aAAyB,MAAM,MAAM,IAC/C;AACN,kBAAQ,UAAU,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI,WAAW,GAAG;AAC/D,eAAK,KAAK,GAAG;AAAA,QACf;AAAA,MACF,SAAS,KAAK;AACZ,gBAAQ,QAAQ,eAAe,GAAG;AAClC,aAAK,GAAU;AAAA,MACjB;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,SAAS,CAAC,GAAG,IAAI,CAAC,OAAO,WAAgB,EAAE,CAAC;AAChE,IAAC,OAAe,MAAM,EAAE,MAAM,GAAG,QAAQ,SAAS,GAAG,KAAK;AAC1D,eAAW,IAAI,GAAG;AAAA,EACpB;AAOA,WAAS,oBAEP,UAAa,aAA2C;AACxD,IAAC,OAAO,KAAK,WAAW,EAA+B,QAAQ,CAAC,QAAQ;AACtE,YAAM,OAAO,SAAS,MAAM,GAAG;AAC/B,UAAI,CAAC,MAAM;AACT,gBAAQ,OAAO,qCAAqC,GAAG,0BAA0B;AACjF;AAAA,MACF;AACA,YAAM,MAAM,YAAY,GAAG;AAC3B,UAAI,CAAC,IAAK;AACV,eAAS,MAAoC,GAAG;AAAA,IAClD,CAAC;AAAA,EACH;AAOA,WAAS,YACP,UACA,YACA;AACA,UAAM,sBAAsB,IAAI,IAAY,MAAM,KAAK,UAAU,CAAC;AAClE,QAAI,oBAAoB,SAAS,GAAG;AAClC,6BAAuB,MAAM,EAAE,QAAQ,CAAC,QAAQ,oBAAoB,IAAI,GAAG,CAAC;AAAA,IAC9E;AACA,eAAW,QAAQ,SAAS,KAAK;AAC/B,YAAM,MAAM,MAAM,IAAI;AACtB,UAAI,CAAC,oBAAoB,IAAI,GAAG,GAAG;AACjC,mBAAW,KAAK,uCAAuC,GAAG,EAAE;AAAA,MAC9D;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,wBAAwB;AAAA,IACxB,mBAAmB,MAAM,MAAM,KAAK,UAAU;AAAA,EAChD;AACF;AAUO,SAAS,kBAId,QACA,UACA,aACA,QACA;AACA,QAAM,SAAS,kBAAuB,QAAQ,MAAM;AACpD,SAAO,oBAAoB,UAAU,WAAW;AAChD,SAAO;AACT;AAUO,SAAS,QAId,QACA,UACA,aACA,QACA;AACA,QAAM,SAAS,kBAAuB,QAAQ,MAAM;AACpD,SAAO,oBAAoB,UAAU,WAAW;AAChD,SAAO;AACT;AAUO,IAAM,oBACX,MACA,CAAyC,MACvC;AAQG,IAAM,aACX,CAAC,OACD,CAAC,UACC;AASG,SAAS,uBAEd,QAAsC,UAAa,QAA4C;AAC/F,QAAM,kBAAmB,OAAe,wBAAwB;AAChE,QAAM,UAAU,kBAAkB,MAAM,KAAK,eAAe,IAAI,uBAAuB,MAAM;AAC7F,QAAM,iBAAiB,IAAI,IAAY,OAAO;AAE9C,aAAW,QAAQ,SAAS,KAAK;AAC/B,UAAM,IAAI,MAAM,IAAI;AACpB,QAAI,CAAC,eAAe,IAAI,CAAC,GAAG;AAC1B,aAAO,KAAK,uCAAuC,CAAC,EAAE;AAAA,IACxD;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,216 @@
1
+ /**
2
+ * routesV3.server.ts
3
+ * -----------------------------------------------------------------------------
4
+ * Bind an Express router/app to a `finalize(...)` registry of AnyLeafs.
5
+ * - Fully typed handlers (params/query/body/output)
6
+ * - Zod parsing + optional output validation
7
+ * - buildCtx runs as a middleware *first*, before all other middlewares
8
+ * - Global, per-route, and cfg-derived middlewares (auth, uploads)
9
+ * - Helper to warn about unimplemented routes
10
+ * - DX helpers to use `ctx` in any middleware with proper types
11
+ */
12
+ import type * as express from 'express';
13
+ import type { RequestHandler, Router } from 'express';
14
+ import { FileField, HttpMethod, MethodCfg } from '@emeryld/rrroutes-contract';
15
+ import type { AnyLeaf, InferBody, InferOutput, InferParams, InferQuery } from '@emeryld/rrroutes-contract';
16
+ export type { AnyLeaf } from '@emeryld/rrroutes-contract';
17
+ /** Shape expected from optional logger implementations. */
18
+ export type LoggerLike = {
19
+ info?: (...args: any[]) => void;
20
+ warn?: (...args: any[]) => void;
21
+ error?: (...args: any[]) => void;
22
+ debug?: (...args: any[]) => void;
23
+ verbose?: (...args: any[]) => void;
24
+ system?: (...args: any[]) => void;
25
+ log?: (...args: any[]) => void;
26
+ };
27
+ /** Keys like "GET /v1/foo" that *actually* exist in the registry */
28
+ export type KeysOfRegistry<R extends {
29
+ byKey: Record<string, AnyLeaf>;
30
+ }> = keyof R['byKey'] & string;
31
+ type MethodFromKey<K extends string> = K extends `${infer M} ${string}` ? Lowercase<M> : never;
32
+ type PathFromKey<K extends string> = K extends `${string} ${infer P}` ? P : never;
33
+ /** Given a registry and a key, pick the exact leaf for that method+path */
34
+ export type LeafFromKey<R extends {
35
+ all: readonly AnyLeaf[];
36
+ }, K extends string> = Extract<R['all'][number], {
37
+ method: MethodFromKey<K> & HttpMethod;
38
+ path: PathFromKey<K>;
39
+ }>;
40
+ /** Optional-ify types if your core returns `never` when a schema isn't defined */
41
+ type Maybe<T> = [T] extends [never] ? undefined : T;
42
+ /** Typed params argument exposed to handlers. */
43
+ export type ArgParams<L extends AnyLeaf> = Maybe<InferParams<L>>;
44
+ /** Typed query argument exposed to handlers. */
45
+ export type ArgQuery<L extends AnyLeaf> = Maybe<InferQuery<L>>;
46
+ /** Typed body argument exposed to handlers. */
47
+ export type ArgBody<L extends AnyLeaf> = Maybe<InferBody<L>>;
48
+ /**
49
+ * Convenience to compute a `"METHOD /path"` key from a leaf.
50
+ * @param leaf Leaf describing the route.
51
+ * @returns Uppercase method + path key.
52
+ */
53
+ export declare const keyOf: (leaf: AnyLeaf) => `${string} ${string}`;
54
+ /**
55
+ * Unique symbol used to stash ctx on res.locals.
56
+ * (Symbols are safer than string keys against collisions.)
57
+ */
58
+ export declare const CTX_SYMBOL: unique symbol;
59
+ /** Response type that *has* a ctx on locals for DX in middlewares */
60
+ export type ResponseWithCtx<Ctx> = Omit<express.Response, 'locals'> & {
61
+ locals: express.Response['locals'] & {
62
+ [CTX_SYMBOL]: Ctx;
63
+ };
64
+ };
65
+ /** A middleware signature that can *use* ctx via `res.locals[CTX_SYMBOL]` */
66
+ export type CtxRequestHandler<Ctx> = (args: {
67
+ req: express.Request;
68
+ res: express.Response;
69
+ next: express.NextFunction;
70
+ ctx: Ctx;
71
+ }) => any;
72
+ /**
73
+ * Safely read ctx from any Response.
74
+ * @param res Express response whose locals contain the ctx symbol.
75
+ * @returns Strongly typed context object.
76
+ */
77
+ export declare function getCtx<Ctx = unknown>(res: express.Response): Ctx;
78
+ /** Typed route handler for a specific leaf */
79
+ export type Handler<L extends AnyLeaf, Ctx = unknown> = (args: {
80
+ req: express.Request;
81
+ res: express.Response;
82
+ next: express.NextFunction;
83
+ ctx: Ctx;
84
+ params: ArgParams<L>;
85
+ query: ArgQuery<L>;
86
+ body: ArgBody<L>;
87
+ }) => Promise<InferOutput<L>> | InferOutput<L>;
88
+ /** Route definition for one key */
89
+ export type RouteDef<L extends AnyLeaf, Ctx = unknown> = {
90
+ /** Middlewares before the handler (run after buildCtx/global/derived) */
91
+ use?: Array<CtxRequestHandler<Ctx>>;
92
+ /** Middlewares after the handler *if* it calls next() */
93
+ after?: Array<CtxRequestHandler<Ctx>>;
94
+ /** Your business logic */
95
+ handler: Handler<L, Ctx>;
96
+ };
97
+ /** Map of registry keys -> route defs */
98
+ export type ControllerMap<R extends {
99
+ all: readonly AnyLeaf[];
100
+ byKey: Record<string, AnyLeaf>;
101
+ }, Ctx = unknown> = {
102
+ [P in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, P>, Ctx>;
103
+ };
104
+ export type PartialControllerMap<R extends {
105
+ all: readonly AnyLeaf[];
106
+ byKey: Record<string, AnyLeaf>;
107
+ }, Ctx = unknown> = Partial<ControllerMap<R, Ctx>>;
108
+ /** Options + derivation helpers */
109
+ export type RouteServerConfig<Ctx = unknown> = {
110
+ /**
111
+ * Build a request-scoped context. We wrap this in a middleware that runs
112
+ * *first* (before global/derived/route middlewares), and stash it on
113
+ * `res.locals[CTX_SYMBOL]` so *all* later middlewares can use it.
114
+ */
115
+ buildCtx?: (req: express.Request, res: express.Response) => Ctx | Promise<Ctx>;
116
+ /**
117
+ * Global middlewares for every bound route (run *after* buildCtx).
118
+ * You can write them as ctx-aware middlewares for great DX.
119
+ */
120
+ global?: Array<CtxRequestHandler<Ctx>>;
121
+ /**
122
+ * Derive middleware from MethodCfg.
123
+ * - `auth` runs when cfg.authenticated === true (or `when` overrides)
124
+ * - `upload` runs when cfg.bodyFiles has entries
125
+ */
126
+ fromCfg?: {
127
+ auth?: RequestHandler | ((leaf: AnyLeaf) => RequestHandler);
128
+ when?: (cfg: MethodCfg, leaf: AnyLeaf) => {
129
+ auth?: boolean;
130
+ } | void;
131
+ upload?: (files: FileField[] | undefined, leaf: AnyLeaf) => RequestHandler[];
132
+ };
133
+ /** Validate handler return values with outputSchema (default: true) */
134
+ validateOutput?: boolean;
135
+ /** Custom responder (default: res.json(data)) */
136
+ send?: (res: express.Response, data: unknown) => void;
137
+ /** Optional logger hooks */
138
+ logger?: LoggerLike;
139
+ };
140
+ /** Runtime helpers returned by `createRouteServer`. */
141
+ export type RouteServer<Ctx = unknown> = {
142
+ router: Router | express.Application;
143
+ register<L extends AnyLeaf>(leaf: L, def: RouteDef<L, Ctx>): void;
144
+ registerControllers<R extends {
145
+ all: readonly AnyLeaf[];
146
+ byKey: Record<string, AnyLeaf>;
147
+ }>(registry: R, controllers: PartialControllerMap<R, Ctx>): void;
148
+ warnMissingControllers<R extends {
149
+ all: readonly AnyLeaf[];
150
+ byKey: Record<string, AnyLeaf>;
151
+ }>(registry: R, logger: {
152
+ warn: (...args: any[]) => void;
153
+ }): void;
154
+ getRegisteredKeys(): string[];
155
+ };
156
+ /**
157
+ * Create an Express binding helper that keeps routes and controllers in sync.
158
+ * @param router Express router or app to register handlers on.
159
+ * @param config Optional configuration controlling ctx building, auth, uploads, etc.
160
+ * @returns Object with helpers to register controllers and inspect registered keys.
161
+ */
162
+ export declare function createRouteServer<Ctx = unknown>(router: Router | express.Application, config?: RouteServerConfig<Ctx>): RouteServer<Ctx>;
163
+ /**
164
+ * Bind only the controllers that are present in the provided map.
165
+ * @param router Express router or app.
166
+ * @param registry Finalized registry produced by `finalize(...)`.
167
+ * @param controllers Partial map of controllers keyed by `"METHOD /path"`.
168
+ * @param config Optional route server configuration.
169
+ * @returns The same router instance for chaining.
170
+ */
171
+ export declare function bindExpressRoutes<R extends {
172
+ all: readonly AnyLeaf[];
173
+ byKey: Record<string, AnyLeaf>;
174
+ }, Ctx = unknown>(router: Router | express.Application, registry: R, controllers: PartialControllerMap<R, Ctx>, config?: RouteServerConfig<Ctx>): express.Router | express.Application;
175
+ /**
176
+ * Bind controllers for every leaf. Missing entries fail at compile time.
177
+ * @param router Express router or app.
178
+ * @param registry Finalized registry produced by `finalize(...)`.
179
+ * @param controllers Complete map of controllers keyed by `"METHOD /path"`.
180
+ * @param config Optional route server configuration.
181
+ * @returns The same router instance for chaining.
182
+ */
183
+ export declare function bindAll<R extends {
184
+ all: readonly AnyLeaf[];
185
+ byKey: Record<string, AnyLeaf>;
186
+ }, Ctx = unknown>(router: Router | express.Application, registry: R, controllers: {
187
+ [K in KeysOfRegistry<R>]: RouteDef<LeafFromKey<R, K>, Ctx>;
188
+ }, config?: RouteServerConfig<Ctx>): express.Router | express.Application;
189
+ /**
190
+ * Helper for great IntelliSense when authoring controller maps.
191
+ * @returns Function that enforces key names while preserving partial flexibility.
192
+ */
193
+ export declare const defineControllers: <R extends {
194
+ all: readonly AnyLeaf[];
195
+ byKey: Record<string, AnyLeaf>;
196
+ }, Ctx = unknown>() => <M extends PartialControllerMap<R, Ctx>>(m: M) => M;
197
+ /**
198
+ * Wrap a plain RequestHandler as an auth factory compatible with `fromCfg.auth`.
199
+ * @param mw Middleware invoked for any leaf that requires authentication.
200
+ * @param _leaf Leaf metadata (ignored, but provided to match factory signature).
201
+ * @returns Factory that ignores the leaf and returns the same middleware.
202
+ */
203
+ export declare const asLeafAuth: (mw: RequestHandler) => (_leaf: AnyLeaf) => RequestHandler;
204
+ /**
205
+ * Warn about leaves that don't have controllers.
206
+ * Call this during startup to surface missing routes.
207
+ * @param router Express router or app to inspect.
208
+ * @param registry Finalized registry produced by `finalize(...)`.
209
+ * @param logger Logger where warnings are emitted.
210
+ */
211
+ export declare function warnMissingControllers<R extends {
212
+ all: readonly AnyLeaf[];
213
+ byKey: Record<string, AnyLeaf>;
214
+ }>(router: Router | express.Application, registry: R, logger: {
215
+ warn: (...args: any[]) => void;
216
+ }): void;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@emeryld/rrroutes-server",
3
+ "version": "1.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "dependencies": {
20
+ "@emeryld/rrroutes-contract": "^1.0.1",
21
+ "zod": "^4.1.8"
22
+ },
23
+ "peerDependencies": {
24
+ "express": "^5.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@jest/globals": "^30.2.0"
28
+ },
29
+ "scripts": {
30
+ "clean": "rimraf dist",
31
+ "build": "pnpm run clean && pnpm run build:js && pnpm run build:types",
32
+ "build:js": "tsup src/index.ts --sourcemap --format esm,cjs --out-dir dist",
33
+ "build:types": "tsc -p tsconfig.build.json",
34
+ "typecheck": "tsc -p tsconfig.json --noEmit"
35
+ }
36
+ }