@emeryld/rrroutes-server 2.4.1 → 2.4.3

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 CHANGED
@@ -153,6 +153,19 @@ bindExpressRoutes(
153
153
  )
154
154
  ```
155
155
 
156
+ If you need access to the parsed params/query/body inside `buildCtx`, destructure them from the single argument:
157
+
158
+ ```ts
159
+ const server = createRRRoute(app, {
160
+ buildCtx: ({ params, query, body }) => ({
161
+ user: lookupUser(params.id),
162
+ verbose: query?.verbose === 'yes',
163
+ }),
164
+ })
165
+ ```
166
+
167
+ > `buildCtx` now receives the `{ req, res, params, query, body }` object; the legacy `(req, res)` signature is no longer supported.
168
+
156
169
  - `defineControllers<Registry, Ctx>()(map)` keeps literal `"METHOD /path"` keys accurate and infers params/query/body/output types per leaf.
157
170
  - `registerControllers` accepts partial maps (missing routes are skipped); `bindAll` enforces completeness at compile time.
158
171
  - `warnMissingControllers(router, registry, logger)` inspects the Express stack and warns for any leaf without a handler.
@@ -174,6 +187,11 @@ const server = createRRRoute(app, {
174
187
  globalMiddleware: { before: [audit] },
175
188
  })
176
189
 
190
+ const routeBefore = ({ params, query, body, ctx, next }) => {
191
+ ctx.routesLogger?.debug?.('route.before payload', { params, query, body })
192
+ next()
193
+ }
194
+
177
195
  // Inside any Express middleware (even outside route.before), use getCtx to retrieve typed ctx:
178
196
  app.use((req, res, next) => {
179
197
  const ctx = getCtx<Ctx>(res)
@@ -183,6 +201,7 @@ app.use((req, res, next) => {
183
201
  ```
184
202
 
185
203
  - `CtxRequestHandler` receives `{ req, res, next, ctx }` with your typed ctx.
204
+ - `route.before` handlers now receive the same parsed `params`, `query`, and `body` payload as the handler, alongside `req`, `res`, and `ctx`.
186
205
  - Need post-response hooks? Register a middleware that wires `res.on('finish', handler)` inside `route.before`/`global.before` instead of relying on a dedicated "after" stage.
187
206
 
188
207
  ### Derived middleware from route cfg (uploads)
package/dist/index.cjs CHANGED
@@ -94,6 +94,20 @@ var decodeJsonLikeQueryValue = (value) => {
94
94
  return value;
95
95
  };
96
96
  var CTX_SYMBOL = /* @__PURE__ */ Symbol.for("typedLeaves.ctx");
97
+ var REQUEST_PAYLOAD_SYMBOL = /* @__PURE__ */ Symbol.for(
98
+ "typedLeaves.requestPayload"
99
+ );
100
+ function getRouteRequestPayload(res) {
101
+ const payload = res.locals[REQUEST_PAYLOAD_SYMBOL];
102
+ if (payload) {
103
+ return payload;
104
+ }
105
+ throw new Error("Request payload was not initialized before middleware");
106
+ }
107
+ function setRouteRequestPayload(res, payload) {
108
+ ;
109
+ res.locals[REQUEST_PAYLOAD_SYMBOL] = payload;
110
+ }
97
111
  function getCtx(res) {
98
112
  return res.locals[CTX_SYMBOL];
99
113
  }
@@ -111,6 +125,26 @@ function adaptCtxMw(mw) {
111
125
  }
112
126
  };
113
127
  }
128
+ function adaptRouteBeforeMw(mw) {
129
+ return (req, res, next) => {
130
+ try {
131
+ const result = mw({
132
+ req,
133
+ res,
134
+ next,
135
+ ctx: getCtx(res),
136
+ ...getRouteRequestPayload(res)
137
+ });
138
+ if (result && typeof result.then === "function") {
139
+ return result.catch((err) => next(err));
140
+ }
141
+ return result;
142
+ } catch (err) {
143
+ next(err);
144
+ return void 0;
145
+ }
146
+ };
147
+ }
114
148
  function logHandlerDebugWithRoutesLogger(logger, event) {
115
149
  if (!logger || event.type !== "handler") return;
116
150
  const payload = [
@@ -201,7 +235,9 @@ function createRRRoute(router, config) {
201
235
  const emit = (event) => activeEmit(event, debugName);
202
236
  const isVerboseDebug = activeDebugMode === "complete";
203
237
  emit({ type: "register", method: methodUpper, path });
204
- const routeSpecific = (def?.before ?? []).map((mw) => adaptCtxMw(mw));
238
+ const routeSpecific = (def?.before ?? []).map(
239
+ (mw) => adaptRouteBeforeMw(mw)
240
+ );
205
241
  const derived = buildDerived(leaf);
206
242
  const ctxMw = async (req, res, next) => {
207
243
  const requestUrl = req.originalUrl ?? path;
@@ -213,8 +249,66 @@ function createRRRoute(router, config) {
213
249
  path,
214
250
  url: requestUrl
215
251
  });
252
+ let params;
253
+ let query;
254
+ let body;
216
255
  try {
217
- const ctx = await config.buildCtx(req, res);
256
+ params = leaf.cfg.paramsSchema ? (0, import_rrroutes_contract.lowProfileParse)(leaf.cfg.paramsSchema, req.params) : Object.keys(req.params || {}).length ? req.params : void 0;
257
+ const hasQueryKeys = req.query && Object.keys(req.query || {}).length > 0;
258
+ const parsedQueryInput = leaf.cfg.querySchema && hasQueryKeys ? decodeJsonLikeQueryValue(req.query) : req.query;
259
+ if (leaf.cfg.querySchema) {
260
+ try {
261
+ query = (0, import_rrroutes_contract.lowProfileParse)(
262
+ leaf.cfg.querySchema,
263
+ parsedQueryInput
264
+ );
265
+ } catch (err) {
266
+ const parseError = new Error(
267
+ `Query parsing error: ${err.message ?? String(err)}`
268
+ );
269
+ parseError.raw = JSON.stringify(req.query);
270
+ parseError.cause = err;
271
+ throw parseError;
272
+ }
273
+ } else {
274
+ query = hasQueryKeys ? req.query : void 0;
275
+ }
276
+ body = leaf.cfg.bodySchema ? (0, import_rrroutes_contract.lowProfileParse)(leaf.cfg.bodySchema, req.body) : req.body !== void 0 ? req.body : void 0;
277
+ } catch (err) {
278
+ const payloadError = {
279
+ params,
280
+ query,
281
+ body
282
+ };
283
+ emit(
284
+ decorateDebugEvent(
285
+ isVerboseDebug,
286
+ {
287
+ type: "request",
288
+ stage: "error",
289
+ method: methodUpper,
290
+ path,
291
+ url: requestUrl,
292
+ durationMs: Date.now() - startedAt,
293
+ error: err
294
+ },
295
+ isVerboseDebug ? payloadError : void 0
296
+ )
297
+ );
298
+ next(err);
299
+ return;
300
+ }
301
+ const requestPayload = {
302
+ params,
303
+ query,
304
+ body
305
+ };
306
+ setRouteRequestPayload(res, requestPayload);
307
+ try {
308
+ const ctx = await config.buildCtx({
309
+ req,
310
+ res
311
+ });
218
312
  res.locals[CTX_SYMBOL] = ctx;
219
313
  emit({
220
314
  type: "buildCtx",
@@ -254,21 +348,16 @@ function createRRRoute(router, config) {
254
348
  path,
255
349
  url: requestUrl
256
350
  });
257
- let params;
258
- let query;
259
- let body;
351
+ const requestPayload = getRouteRequestPayload(res);
352
+ const params = requestPayload.params;
353
+ const query = requestPayload.query;
354
+ const body = requestPayload.body;
260
355
  let responsePayload;
261
356
  let hasResponsePayload = false;
262
357
  const downstreamNext = next;
263
358
  const ctx = res.locals[CTX_SYMBOL];
264
359
  const ctxRoutesLogger = ctx?.routesLogger;
265
360
  const emitWithCtx = (event, details) => {
266
- if (event.type == "request" && event.stage == "error") {
267
- console.log("Request error event emitted:", {
268
- event,
269
- ctxRoutesLogger: !!ctxRoutesLogger
270
- });
271
- }
272
361
  const decorated = decorateDebugEvent(isVerboseDebug, event, details);
273
362
  if (!config.debug || !config.debug[decorated.type]) {
274
363
  return;
@@ -280,26 +369,6 @@ function createRRRoute(router, config) {
280
369
  }
281
370
  };
282
371
  try {
283
- params = leaf.cfg.paramsSchema ? (0, import_rrroutes_contract.lowProfileParse)(leaf.cfg.paramsSchema, req.params) : Object.keys(req.params || {}).length ? req.params : void 0;
284
- try {
285
- const parsedQueryInput = leaf.cfg.querySchema && req.query ? decodeJsonLikeQueryValue(req.query) : req.query;
286
- query = leaf.cfg.querySchema ? (0, import_rrroutes_contract.lowProfileParse)(leaf.cfg.querySchema, parsedQueryInput) : Object.keys(req.query || {}).length ? req.query : void 0;
287
- } catch (e) {
288
- emitWithCtx({
289
- type: "request",
290
- stage: "error",
291
- method: methodUpper,
292
- path,
293
- url: requestUrl,
294
- error: {
295
- ...e,
296
- raw: JSON.stringify(req.query),
297
- message: `Query parsing error: ${e.message}`
298
- }
299
- });
300
- throw e;
301
- }
302
- body = leaf.cfg.bodySchema ? (0, import_rrroutes_contract.lowProfileParse)(leaf.cfg.bodySchema, req.body) : req.body !== void 0 ? req.body : void 0;
303
372
  const handlerStartedAt = Date.now();
304
373
  emitWithCtx(
305
374
  {
@@ -390,29 +459,28 @@ function createRRRoute(router, config) {
390
459
  registered.add(key);
391
460
  }
392
461
  function registerControllers(registry, controllers, all) {
393
- if (all !== void 0 && all !== false) {
394
- const label = typeof all === "string" ? all : all === true ? "true" : String(all);
395
- throw new Error(
396
- `registerControllers: "${label}" is not allowed at runtime. Use bindAll(...) for compile-time coverage or warnMissingControllers(...) to surface missing routes.`
397
- );
398
- }
399
462
  for (const leaf of registry.all) {
400
463
  const key = (0, import_rrroutes_contract.keyOf)(leaf.method, leaf.path, false);
401
464
  knownLeaves.set(key, leaf);
402
465
  }
403
- ;
466
+ const missingLeaves = [];
404
467
  Object.keys(controllers).forEach((key) => {
405
468
  const leaf = registry.byKey[key];
406
469
  if (!leaf) {
407
- console.warn(
408
- `No leaf found for controller key: ${key}. Not registering route.`
409
- );
470
+ missingLeaves.push(key);
410
471
  return;
411
472
  }
412
473
  const def = controllers[key];
413
474
  if (!def) return;
414
475
  register(leaf, def);
415
476
  });
477
+ if (all && missingLeaves.length > 0) {
478
+ throw new Error(
479
+ `Cannot register controllers: missing definitions for routes: ${missingLeaves.join(
480
+ ", "
481
+ )}`
482
+ );
483
+ }
416
484
  }
417
485
  function warnMissing(registry, warnLogger) {
418
486
  const registeredFromStore = new Set(Array.from(registered));
@@ -509,8 +577,12 @@ function createBuiltInConnectionHandlers(opts) {
509
577
  const pingEvent = "sys:ping";
510
578
  const pongEvent = "sys:pong";
511
579
  const heartbeatEnabled = heartbeat?.enabled !== false;
512
- const joinPayloadSchema = buildRoomPayloadSchema(config.joinMetaMessage);
513
- const leavePayloadSchema = buildRoomPayloadSchema(config.leaveMetaMessage);
580
+ const joinPayloadSchema = buildRoomPayloadSchema(
581
+ config.joinMetaMessage
582
+ );
583
+ const leavePayloadSchema = buildRoomPayloadSchema(
584
+ config.leaveMetaMessage
585
+ );
514
586
  const pingPayloadSchema = config.pingPayload;
515
587
  const pongPayloadSchema = config.pongPayload;
516
588
  const sysEvents = sys;