@emeryld/rrroutes-server 2.4.3 → 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/README.md CHANGED
@@ -62,7 +62,6 @@ const leaves = resource('/api')
62
62
  const registry = finalize(leaves)
63
63
 
64
64
  // 2) Wire Express with ctx + derived upload middleware
65
- const upload = multer({ storage: multer.memoryStorage() })
66
65
  const app = express()
67
66
  const server = createRRRoute(app, {
68
67
  buildCtx: async (req) => ({
@@ -77,10 +76,8 @@ const server = createRRRoute(app, {
77
76
  },
78
77
  ],
79
78
  },
80
- fromCfg: {
81
- upload: (files) =>
82
- files && files.length > 0 ? [upload.fields(files)] : [],
83
- },
79
+ multerOptions: (files) =>
80
+ files && files.length > 0 ? { storage: multer.memoryStorage() } : undefined,
84
81
  validateOutput: true, // parse handler returns with outputSchema (default true)
85
82
  debug: {
86
83
  request: true,
@@ -172,7 +169,7 @@ const server = createRRRoute(app, {
172
169
 
173
170
  ### Middleware order and ctx usage
174
171
 
175
- Order: `buildCtx` → `global.before` → `fromCfg` (derived) → `route.before` → handler.
172
+ Order: `resolve` → `ctx` → `global.before` → `route.before` → handler.
176
173
 
177
174
  ```ts
178
175
  import { getCtx, CtxRequestHandler } from '@emeryld/rrroutes-server'
@@ -204,25 +201,33 @@ app.use((req, res, next) => {
204
201
  - `route.before` handlers now receive the same parsed `params`, `query`, and `body` payload as the handler, alongside `req`, `res`, and `ctx`.
205
202
  - 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.
206
203
 
207
- ### Derived middleware from route cfg (uploads)
204
+ ### Upload parsing
208
205
 
209
- Use `fromCfg.upload` to attach middleware when a leaf declares `bodyFiles`.
206
+ Routes that declare `bodyFiles` automatically run Multer before `ctx` using shared memory storage. Override or disable that behavior with `multerOptions`.
210
207
 
211
208
  ```ts
212
209
  import multer from 'multer'
213
210
  import { FileField } from '@emeryld/rrroutes-contract'
214
211
 
215
- const upload = multer({ storage: multer.memoryStorage() })
212
+ const diskStorage = multer.diskStorage({
213
+ destination: 'tmp/uploads',
214
+ filename: (_req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`),
215
+ })
216
216
 
217
217
  const server = createRRRoute(app, {
218
218
  buildCtx,
219
- fromCfg: {
220
- upload: (files: FileField[] | undefined) =>
221
- files?.length ? [upload.fields(files)] : [],
222
- },
219
+ multerOptions: (files: FileField[] | undefined) =>
220
+ files?.length
221
+ ? {
222
+ storage: diskStorage,
223
+ limits: { fileSize: 5 * 1024 * 1024 },
224
+ }
225
+ : false,
223
226
  })
224
227
  ```
225
228
 
229
+ Return `false` from `multerOptions` when you want to skip Multer for a specific route even if `bodyFiles` are declared.
230
+
226
231
  ### Output validation and custom responders
227
232
 
228
233
  - `validateOutput: true` parses handler return values with the leaf `outputSchema`. Set to `false` to skip.
@@ -349,7 +354,7 @@ process.on('SIGTERM', () => sockets.destroy())
349
354
  - Post-response work should hook into `res.on('finish', handler)` from a middleware in the normal pipeline if you need to observe completed responses.
350
355
  - `compilePath`/param parsing exceptions bubble to Express error handlers; wrap `buildCtx`/middleware in try/catch if you need custom error shapes.
351
356
  - When `validateOutput` is true and no `outputSchema` exists, raw handler output is passed through.
352
- - `fromCfg.upload` runs only when `leaf.cfg.bodyFiles` is a non-empty array.
357
+ - `multerOptions` runs only when `leaf.cfg.bodyFiles` is a non-empty array; return `false` to disable the upload middleware for that route.
353
358
  - Socket `emit` will throw on invalid payloads; handle errors around broadcast loops.
354
359
 
355
360
  ## Scripts
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -36,6 +46,7 @@ var import_rrroutes_contract2 = require("@emeryld/rrroutes-contract");
36
46
 
37
47
  // src/routesV3.server.ts
38
48
  var import_rrroutes_contract = require("@emeryld/rrroutes-contract");
49
+ var import_multer = __toESM(require("multer"), 1);
39
50
  var serverDebugEventTypes = [
40
51
  "register",
41
52
  "request",
@@ -97,6 +108,45 @@ var CTX_SYMBOL = /* @__PURE__ */ Symbol.for("typedLeaves.ctx");
97
108
  var REQUEST_PAYLOAD_SYMBOL = /* @__PURE__ */ Symbol.for(
98
109
  "typedLeaves.requestPayload"
99
110
  );
111
+ function isMulterFile(value) {
112
+ if (!isPlainObject(value)) return false;
113
+ const candidate = value;
114
+ return typeof candidate.fieldname === "string";
115
+ }
116
+ function collectMulterFiles(req) {
117
+ const files = [];
118
+ const pushValue = (value) => {
119
+ if (value === void 0 || value === null) return;
120
+ if (Array.isArray(value)) {
121
+ value.forEach(pushValue);
122
+ return;
123
+ }
124
+ if (isMulterFile(value)) {
125
+ files.push(value);
126
+ return;
127
+ }
128
+ if (isPlainObject(value)) {
129
+ Object.values(value).forEach(pushValue);
130
+ }
131
+ };
132
+ pushValue(req.files);
133
+ pushValue(req.file);
134
+ return files;
135
+ }
136
+ function resolveBodyFilesFromRequest(req, fields) {
137
+ if (!fields?.length) return void 0;
138
+ const allowedNames = new Set(fields.map((field) => field.name));
139
+ const collected = collectMulterFiles(req);
140
+ if (collected.length === 0) return void 0;
141
+ const result = {};
142
+ for (const file of collected) {
143
+ if (!allowedNames.has(file.fieldname)) continue;
144
+ const bucket = result[file.fieldname] ?? [];
145
+ bucket.push(file);
146
+ result[file.fieldname] = bucket;
147
+ }
148
+ return Object.keys(result).length ? result : void 0;
149
+ }
100
150
  function getRouteRequestPayload(res) {
101
151
  const payload = res.locals[REQUEST_PAYLOAD_SYMBOL];
102
152
  if (payload) {
@@ -159,6 +209,9 @@ function logHandlerDebugWithRoutesLogger(logger, event) {
159
209
  ;
160
210
  (logger.debug ?? logger.verbose ?? logger.info ?? logger.log ?? logger.system)?.call(logger, ...payload);
161
211
  }
212
+ var defaultMulterOptions = {
213
+ storage: import_multer.default.memoryStorage()
214
+ };
162
215
  var defaultSend = (res, data) => {
163
216
  res.json(data);
164
217
  };
@@ -202,12 +255,22 @@ function createRRRoute(router, config) {
202
255
  (mw) => adaptCtxMw(mw)
203
256
  );
204
257
  const registered = getRegisteredRouteStore(router);
205
- const buildDerived = (leaf) => {
206
- const derived = [];
207
- if (config.fromCfg?.upload && Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0) {
208
- derived.push(...config.fromCfg.upload(leaf.cfg.bodyFiles));
209
- }
210
- return derived;
258
+ const getMulterOptions = (fields) => {
259
+ if (!fields || fields.length === 0) return void 0;
260
+ const resolved = typeof config.multerOptions === "function" ? config.multerOptions(fields) : config.multerOptions;
261
+ if (resolved === false) return void 0;
262
+ return resolved ?? defaultMulterOptions;
263
+ };
264
+ const runMulterHandler = (handler, req, res) => {
265
+ return new Promise((resolve, reject) => {
266
+ handler(req, res, (err) => {
267
+ if (err) {
268
+ reject(err);
269
+ return;
270
+ }
271
+ resolve();
272
+ });
273
+ });
211
274
  };
212
275
  function register(leaf, def) {
213
276
  const method = leaf.method;
@@ -238,21 +301,26 @@ function createRRRoute(router, config) {
238
301
  const routeSpecific = (def?.before ?? []).map(
239
302
  (mw) => adaptRouteBeforeMw(mw)
240
303
  );
241
- const derived = buildDerived(leaf);
242
- const ctxMw = async (req, res, next) => {
304
+ const resolvePayloadMw = async (req, res, next) => {
243
305
  const requestUrl = req.originalUrl ?? path;
244
306
  const startedAt = Date.now();
245
- emit({
246
- type: "buildCtx",
247
- stage: "start",
248
- method: methodUpper,
249
- path,
250
- url: requestUrl
251
- });
252
307
  let params;
253
308
  let query;
254
309
  let body;
310
+ let bodyFiles;
255
311
  try {
312
+ if (leaf.cfg.bodyFiles && leaf.cfg.bodyFiles.length > 0) {
313
+ const uploadOptions = getMulterOptions(leaf.cfg.bodyFiles);
314
+ if (uploadOptions) {
315
+ const fieldDefs = leaf.cfg.bodyFiles.map(({ name, maxCount }) => ({
316
+ name,
317
+ maxCount
318
+ }));
319
+ const uploader = (0, import_multer.default)(uploadOptions).fields(fieldDefs);
320
+ await runMulterHandler(uploader, req, res);
321
+ }
322
+ }
323
+ bodyFiles = resolveBodyFilesFromRequest(req, leaf.cfg.bodyFiles);
256
324
  params = leaf.cfg.paramsSchema ? (0, import_rrroutes_contract.lowProfileParse)(leaf.cfg.paramsSchema, req.params) : Object.keys(req.params || {}).length ? req.params : void 0;
257
325
  const hasQueryKeys = req.query && Object.keys(req.query || {}).length > 0;
258
326
  const parsedQueryInput = leaf.cfg.querySchema && hasQueryKeys ? decodeJsonLikeQueryValue(req.query) : req.query;
@@ -278,7 +346,8 @@ function createRRRoute(router, config) {
278
346
  const payloadError = {
279
347
  params,
280
348
  query,
281
- body
349
+ body,
350
+ bodyFiles
282
351
  };
283
352
  emit(
284
353
  decorateDebugEvent(
@@ -301,9 +370,22 @@ function createRRRoute(router, config) {
301
370
  const requestPayload = {
302
371
  params,
303
372
  query,
304
- body
373
+ body,
374
+ bodyFiles
305
375
  };
306
376
  setRouteRequestPayload(res, requestPayload);
377
+ next();
378
+ };
379
+ const ctxMw = async (req, res, next) => {
380
+ const requestUrl = req.originalUrl ?? path;
381
+ const startedAt = Date.now();
382
+ emit({
383
+ type: "buildCtx",
384
+ stage: "start",
385
+ method: methodUpper,
386
+ path,
387
+ url: requestUrl
388
+ });
307
389
  try {
308
390
  const ctx = await config.buildCtx({
309
391
  req,
@@ -333,9 +415,9 @@ function createRRRoute(router, config) {
333
415
  }
334
416
  };
335
417
  const before = [
418
+ resolvePayloadMw,
336
419
  ctxMw,
337
420
  ...globalBeforeMws,
338
- ...derived,
339
421
  ...routeSpecific
340
422
  ];
341
423
  const wrapped = async (req, res, next) => {
@@ -352,6 +434,7 @@ function createRRRoute(router, config) {
352
434
  const params = requestPayload.params;
353
435
  const query = requestPayload.query;
354
436
  const body = requestPayload.body;
437
+ const bodyFiles = requestPayload.bodyFiles;
355
438
  let responsePayload;
356
439
  let hasResponsePayload = false;
357
440
  const downstreamNext = next;
@@ -377,7 +460,7 @@ function createRRRoute(router, config) {
377
460
  method: methodUpper,
378
461
  path
379
462
  },
380
- isVerboseDebug ? { params, query, body } : void 0
463
+ isVerboseDebug ? { params, query, body, bodyFiles } : void 0
381
464
  );
382
465
  let result;
383
466
  try {
@@ -388,7 +471,8 @@ function createRRRoute(router, config) {
388
471
  ctx,
389
472
  params,
390
473
  query,
391
- body
474
+ body,
475
+ bodyFiles
392
476
  });
393
477
  emitWithCtx(
394
478
  {
@@ -402,6 +486,7 @@ function createRRRoute(router, config) {
402
486
  params,
403
487
  query,
404
488
  body,
489
+ bodyFiles,
405
490
  ...result !== void 0 ? { output: result } : {}
406
491
  } : void 0
407
492
  );
@@ -415,7 +500,7 @@ function createRRRoute(router, config) {
415
500
  durationMs: Date.now() - handlerStartedAt,
416
501
  error: e
417
502
  },
418
- isVerboseDebug ? { params, query, body } : void 0
503
+ isVerboseDebug ? { params, query, body, bodyFiles } : void 0
419
504
  );
420
505
  throw e;
421
506
  }
@@ -436,6 +521,7 @@ function createRRRoute(router, config) {
436
521
  params,
437
522
  query,
438
523
  body,
524
+ bodyFiles,
439
525
  ...hasResponsePayload ? { output: responsePayload } : {}
440
526
  } : void 0
441
527
  );
@@ -450,7 +536,7 @@ function createRRRoute(router, config) {
450
536
  durationMs: Date.now() - startedAt,
451
537
  error: err
452
538
  },
453
- isVerboseDebug ? { params, query, body } : void 0
539
+ isVerboseDebug ? { params, query, body, bodyFiles } : void 0
454
540
  );
455
541
  next(err);
456
542
  }