@emeryld/rrroutes-server 2.4.2 → 2.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  keyOf,
7
7
  lowProfileParse
8
8
  } from "@emeryld/rrroutes-contract";
9
+ import multer from "multer";
9
10
  var serverDebugEventTypes = [
10
11
  "register",
11
12
  "request",
@@ -64,6 +65,59 @@ var decodeJsonLikeQueryValue = (value) => {
64
65
  return value;
65
66
  };
66
67
  var CTX_SYMBOL = /* @__PURE__ */ Symbol.for("typedLeaves.ctx");
68
+ var REQUEST_PAYLOAD_SYMBOL = /* @__PURE__ */ Symbol.for(
69
+ "typedLeaves.requestPayload"
70
+ );
71
+ function isMulterFile(value) {
72
+ if (!isPlainObject(value)) return false;
73
+ const candidate = value;
74
+ return typeof candidate.fieldname === "string";
75
+ }
76
+ function collectMulterFiles(req) {
77
+ const files = [];
78
+ const pushValue = (value) => {
79
+ if (value === void 0 || value === null) return;
80
+ if (Array.isArray(value)) {
81
+ value.forEach(pushValue);
82
+ return;
83
+ }
84
+ if (isMulterFile(value)) {
85
+ files.push(value);
86
+ return;
87
+ }
88
+ if (isPlainObject(value)) {
89
+ Object.values(value).forEach(pushValue);
90
+ }
91
+ };
92
+ pushValue(req.files);
93
+ pushValue(req.file);
94
+ return files;
95
+ }
96
+ function resolveBodyFilesFromRequest(req, fields) {
97
+ if (!fields?.length) return void 0;
98
+ const allowedNames = new Set(fields.map((field) => field.name));
99
+ const collected = collectMulterFiles(req);
100
+ if (collected.length === 0) return void 0;
101
+ const result = {};
102
+ for (const file of collected) {
103
+ if (!allowedNames.has(file.fieldname)) continue;
104
+ const bucket = result[file.fieldname] ?? [];
105
+ bucket.push(file);
106
+ result[file.fieldname] = bucket;
107
+ }
108
+ return Object.keys(result).length ? result : void 0;
109
+ }
110
+ function getRouteRequestPayload(res) {
111
+ const payload = res.locals[REQUEST_PAYLOAD_SYMBOL];
112
+ if (payload) {
113
+ return payload;
114
+ }
115
+ throw new Error("Request payload was not initialized before middleware");
116
+ }
117
+ function setRouteRequestPayload(res, payload) {
118
+ ;
119
+ res.locals[REQUEST_PAYLOAD_SYMBOL] = payload;
120
+ }
67
121
  function getCtx(res) {
68
122
  return res.locals[CTX_SYMBOL];
69
123
  }
@@ -81,6 +135,26 @@ function adaptCtxMw(mw) {
81
135
  }
82
136
  };
83
137
  }
138
+ function adaptRouteBeforeMw(mw) {
139
+ return (req, res, next) => {
140
+ try {
141
+ const result = mw({
142
+ req,
143
+ res,
144
+ next,
145
+ ctx: getCtx(res),
146
+ ...getRouteRequestPayload(res)
147
+ });
148
+ if (result && typeof result.then === "function") {
149
+ return result.catch((err) => next(err));
150
+ }
151
+ return result;
152
+ } catch (err) {
153
+ next(err);
154
+ return void 0;
155
+ }
156
+ };
157
+ }
84
158
  function logHandlerDebugWithRoutesLogger(logger, event) {
85
159
  if (!logger || event.type !== "handler") return;
86
160
  const payload = [
@@ -95,6 +169,9 @@ function logHandlerDebugWithRoutesLogger(logger, event) {
95
169
  ;
96
170
  (logger.debug ?? logger.verbose ?? logger.info ?? logger.log ?? logger.system)?.call(logger, ...payload);
97
171
  }
172
+ var defaultMulterOptions = {
173
+ storage: multer.memoryStorage()
174
+ };
98
175
  var defaultSend = (res, data) => {
99
176
  res.json(data);
100
177
  };
@@ -138,12 +215,22 @@ function createRRRoute(router, config) {
138
215
  (mw) => adaptCtxMw(mw)
139
216
  );
140
217
  const registered = getRegisteredRouteStore(router);
141
- const buildDerived = (leaf) => {
142
- const derived = [];
143
- if (config.fromCfg?.upload && Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0) {
144
- derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles));
145
- }
146
- return derived;
218
+ const getMulterOptions = (fields) => {
219
+ if (!fields || fields.length === 0) return void 0;
220
+ const resolved = typeof config.multerOptions === "function" ? config.multerOptions(fields) : config.multerOptions;
221
+ if (resolved === false) return void 0;
222
+ return resolved ?? defaultMulterOptions;
223
+ };
224
+ const runMulterHandler = (handler, req, res) => {
225
+ return new Promise((resolve, reject) => {
226
+ handler(req, res, (err) => {
227
+ if (err) {
228
+ reject(err);
229
+ return;
230
+ }
231
+ resolve();
232
+ });
233
+ });
147
234
  };
148
235
  function register(leaf, def) {
149
236
  const method = leaf.method;
@@ -171,8 +258,84 @@ function createRRRoute(router, config) {
171
258
  const emit = (event) => activeEmit(event, debugName);
172
259
  const isVerboseDebug = activeDebugMode === "complete";
173
260
  emit({ type: "register", method: methodUpper, path });
174
- const routeSpecific = (def?.before ?? []).map((mw) => adaptCtxMw(mw));
175
- const derived = buildDerived(leaf);
261
+ const routeSpecific = (def?.before ?? []).map(
262
+ (mw) => adaptRouteBeforeMw(mw)
263
+ );
264
+ const resolvePayloadMw = async (req, res, next) => {
265
+ const requestUrl = req.originalUrl ?? path;
266
+ const startedAt = Date.now();
267
+ let params;
268
+ let query;
269
+ let body;
270
+ let bodyFiles;
271
+ try {
272
+ if (leaf.cfg.bodyFiles && leaf.cfg.bodyFiles.length > 0) {
273
+ const uploadOptions = getMulterOptions(leaf.cfg.bodyFiles);
274
+ if (uploadOptions) {
275
+ const fieldDefs = leaf.cfg.bodyFiles.map(({ name, maxCount }) => ({
276
+ name,
277
+ maxCount
278
+ }));
279
+ const uploader = multer(uploadOptions).fields(fieldDefs);
280
+ await runMulterHandler(uploader, req, res);
281
+ }
282
+ }
283
+ bodyFiles = resolveBodyFilesFromRequest(req, leaf.cfg.bodyFiles);
284
+ params = leaf.cfg.paramsSchema ? lowProfileParse(leaf.cfg.paramsSchema, req.params) : Object.keys(req.params || {}).length ? req.params : void 0;
285
+ const hasQueryKeys = req.query && Object.keys(req.query || {}).length > 0;
286
+ const parsedQueryInput = leaf.cfg.querySchema && hasQueryKeys ? decodeJsonLikeQueryValue(req.query) : req.query;
287
+ if (leaf.cfg.querySchema) {
288
+ try {
289
+ query = lowProfileParse(
290
+ leaf.cfg.querySchema,
291
+ parsedQueryInput
292
+ );
293
+ } catch (err) {
294
+ const parseError = new Error(
295
+ `Query parsing error: ${err.message ?? String(err)}`
296
+ );
297
+ parseError.raw = JSON.stringify(req.query);
298
+ parseError.cause = err;
299
+ throw parseError;
300
+ }
301
+ } else {
302
+ query = hasQueryKeys ? req.query : void 0;
303
+ }
304
+ body = leaf.cfg.bodySchema ? lowProfileParse(leaf.cfg.bodySchema, req.body) : req.body !== void 0 ? req.body : void 0;
305
+ } catch (err) {
306
+ const payloadError = {
307
+ params,
308
+ query,
309
+ body,
310
+ bodyFiles
311
+ };
312
+ emit(
313
+ decorateDebugEvent(
314
+ isVerboseDebug,
315
+ {
316
+ type: "request",
317
+ stage: "error",
318
+ method: methodUpper,
319
+ path,
320
+ url: requestUrl,
321
+ durationMs: Date.now() - startedAt,
322
+ error: err
323
+ },
324
+ isVerboseDebug ? payloadError : void 0
325
+ )
326
+ );
327
+ next(err);
328
+ return;
329
+ }
330
+ const requestPayload = {
331
+ params,
332
+ query,
333
+ body,
334
+ bodyFiles
335
+ };
336
+ setRouteRequestPayload(res, requestPayload);
337
+ next();
338
+ };
176
339
  const ctxMw = async (req, res, next) => {
177
340
  const requestUrl = req.originalUrl ?? path;
178
341
  const startedAt = Date.now();
@@ -184,7 +347,10 @@ function createRRRoute(router, config) {
184
347
  url: requestUrl
185
348
  });
186
349
  try {
187
- const ctx = await config.buildCtx(req, res);
350
+ const ctx = await config.buildCtx({
351
+ req,
352
+ res
353
+ });
188
354
  res.locals[CTX_SYMBOL] = ctx;
189
355
  emit({
190
356
  type: "buildCtx",
@@ -209,9 +375,9 @@ function createRRRoute(router, config) {
209
375
  }
210
376
  };
211
377
  const before = [
378
+ resolvePayloadMw,
212
379
  ctxMw,
213
380
  ...globalBeforeMws,
214
- ...derived,
215
381
  ...routeSpecific
216
382
  ];
217
383
  const wrapped = async (req, res, next) => {
@@ -224,9 +390,11 @@ function createRRRoute(router, config) {
224
390
  path,
225
391
  url: requestUrl
226
392
  });
227
- let params;
228
- let query;
229
- let body;
393
+ const requestPayload = getRouteRequestPayload(res);
394
+ const params = requestPayload.params;
395
+ const query = requestPayload.query;
396
+ const body = requestPayload.body;
397
+ const bodyFiles = requestPayload.bodyFiles;
230
398
  let responsePayload;
231
399
  let hasResponsePayload = false;
232
400
  const downstreamNext = next;
@@ -244,26 +412,6 @@ function createRRRoute(router, config) {
244
412
  }
245
413
  };
246
414
  try {
247
- params = leaf.cfg.paramsSchema ? lowProfileParse(leaf.cfg.paramsSchema, req.params) : Object.keys(req.params || {}).length ? req.params : void 0;
248
- try {
249
- const parsedQueryInput = leaf.cfg.querySchema && req.query ? decodeJsonLikeQueryValue(req.query) : req.query;
250
- query = leaf.cfg.querySchema ? lowProfileParse(leaf.cfg.querySchema, parsedQueryInput) : Object.keys(req.query || {}).length ? req.query : void 0;
251
- } catch (e) {
252
- emitWithCtx({
253
- type: "request",
254
- stage: "error",
255
- method: methodUpper,
256
- path,
257
- url: requestUrl,
258
- error: {
259
- ...e,
260
- raw: JSON.stringify(req.query),
261
- message: `Query parsing error: ${e.message}`
262
- }
263
- });
264
- throw e;
265
- }
266
- body = leaf.cfg.bodySchema ? lowProfileParse(leaf.cfg.bodySchema, req.body) : req.body !== void 0 ? req.body : void 0;
267
415
  const handlerStartedAt = Date.now();
268
416
  emitWithCtx(
269
417
  {
@@ -272,7 +420,7 @@ function createRRRoute(router, config) {
272
420
  method: methodUpper,
273
421
  path
274
422
  },
275
- isVerboseDebug ? { params, query, body } : void 0
423
+ isVerboseDebug ? { params, query, body, bodyFiles } : void 0
276
424
  );
277
425
  let result;
278
426
  try {
@@ -283,7 +431,8 @@ function createRRRoute(router, config) {
283
431
  ctx,
284
432
  params,
285
433
  query,
286
- body
434
+ body,
435
+ bodyFiles
287
436
  });
288
437
  emitWithCtx(
289
438
  {
@@ -297,6 +446,7 @@ function createRRRoute(router, config) {
297
446
  params,
298
447
  query,
299
448
  body,
449
+ bodyFiles,
300
450
  ...result !== void 0 ? { output: result } : {}
301
451
  } : void 0
302
452
  );
@@ -310,7 +460,7 @@ function createRRRoute(router, config) {
310
460
  durationMs: Date.now() - handlerStartedAt,
311
461
  error: e
312
462
  },
313
- isVerboseDebug ? { params, query, body } : void 0
463
+ isVerboseDebug ? { params, query, body, bodyFiles } : void 0
314
464
  );
315
465
  throw e;
316
466
  }
@@ -331,6 +481,7 @@ function createRRRoute(router, config) {
331
481
  params,
332
482
  query,
333
483
  body,
484
+ bodyFiles,
334
485
  ...hasResponsePayload ? { output: responsePayload } : {}
335
486
  } : void 0
336
487
  );
@@ -345,7 +496,7 @@ function createRRRoute(router, config) {
345
496
  durationMs: Date.now() - startedAt,
346
497
  error: err
347
498
  },
348
- isVerboseDebug ? { params, query, body } : void 0
499
+ isVerboseDebug ? { params, query, body, bodyFiles } : void 0
349
500
  );
350
501
  next(err);
351
502
  }
@@ -472,8 +623,12 @@ function createBuiltInConnectionHandlers(opts) {
472
623
  const pingEvent = "sys:ping";
473
624
  const pongEvent = "sys:pong";
474
625
  const heartbeatEnabled = heartbeat?.enabled !== false;
475
- const joinPayloadSchema = buildRoomPayloadSchema(config.joinMetaMessage);
476
- const leavePayloadSchema = buildRoomPayloadSchema(config.leaveMetaMessage);
626
+ const joinPayloadSchema = buildRoomPayloadSchema(
627
+ config.joinMetaMessage
628
+ );
629
+ const leavePayloadSchema = buildRoomPayloadSchema(
630
+ config.leaveMetaMessage
631
+ );
477
632
  const pingPayloadSchema = config.pingPayload;
478
633
  const pongPayloadSchema = config.pongPayload;
479
634
  const sysEvents = sys;