@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 +19 -14
- package/dist/index.cjs +108 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +98 -22
- package/dist/index.js.map +1 -1
- package/dist/routesV3.server.d.ts +17 -7
- package/package.json +5 -3
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
|
-
|
|
81
|
-
|
|
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: `
|
|
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
|
-
###
|
|
204
|
+
### Upload parsing
|
|
208
205
|
|
|
209
|
-
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
- `
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
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
|
}
|