@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 +19 -0
- package/dist/index.cjs +115 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +115 -43
- package/dist/index.js.map +1 -1
- package/dist/routesV3.server.d.ts +19 -2
- package/dist/sockets/socket.server.sys.d.ts +1 -1
- package/package.json +6 -5
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(
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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(
|
|
513
|
-
|
|
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;
|