@emeryld/rrroutes-server 2.6.0 → 2.6.2
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 +44 -1
- package/dist/index.cjs +152 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -1
- package/dist/routesV3.server.d.ts +32 -0
- package/dist/sockets/socket.server.index.d.ts +23 -1
- package/dist/sockets/socket.server.sys.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
Summary:
|
|
3
3
|
- Added full quick start for binding finalized contracts to Express with typed controllers, ctx builders, derived upload middleware, and output validation.
|
|
4
|
-
- Documented debug options, per-route overrides, partial/complete registration helpers, missing-controller warnings, and socket server helpers (typed events, heartbeat, room hooks).
|
|
4
|
+
- Documented debug options, per-route overrides, partial/complete registration helpers, batch endpoint registration (`batchLeaf`), missing-controller warnings, and socket server helpers (typed events, heartbeat, room hooks).
|
|
5
5
|
- Missing: cluster/multi-process Socket.IO deployment notes and an end-to-end auth example (ctx + room allow/deny) to add later.
|
|
6
6
|
-->
|
|
7
7
|
|
|
@@ -167,6 +167,49 @@ const server = createRRRoute(app, {
|
|
|
167
167
|
- `registerControllers` accepts partial maps (missing routes are skipped); `bindAll` enforces completeness at compile time.
|
|
168
168
|
- `warnMissingControllers(router, registry, logger)` inspects the Express stack and warns for any leaf without a handler.
|
|
169
169
|
|
|
170
|
+
### Batch endpoint helper (`batchLeaf`)
|
|
171
|
+
|
|
172
|
+
Use `batchLeaf` to register one endpoint that dispatches multiple already-registered controllers by encoded route keys.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { batchLeaf } from '@emeryld/rrroutes-server'
|
|
176
|
+
|
|
177
|
+
const server = createRRRoute(app, { buildCtx })
|
|
178
|
+
server.registerControllers(registry, controllers)
|
|
179
|
+
|
|
180
|
+
// Defaults to POST. You can override with { method: 'put' } etc.
|
|
181
|
+
batchLeaf(server, '/v1/batch', registry)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Client request body shape:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
{
|
|
188
|
+
[encodeURIComponent('GET /v1/users/:userId')]: {
|
|
189
|
+
params: { userId: 'u_1' },
|
|
190
|
+
},
|
|
191
|
+
[encodeURIComponent('PATCH /v1/users/:userId')]: {
|
|
192
|
+
params: { userId: 'u_1' },
|
|
193
|
+
body: { name: 'Emery' },
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Response shape (same encoded keys):
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
{
|
|
202
|
+
[encodeURIComponent('GET /v1/users/:userId')]: { out: { ... }, meta: ... },
|
|
203
|
+
[encodeURIComponent('PATCH /v1/users/:userId')]: { out: { ... }, meta: ... },
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Notes:
|
|
208
|
+
|
|
209
|
+
- Register controllers before calling `batchLeaf(...)`; unknown keys fail at runtime.
|
|
210
|
+
- Dispatch uses `server.invoke(...)` for each entry, so each sub-call runs per-leaf parsing, `buildCtx`, `route.before`, handler execution, and output validation.
|
|
211
|
+
- Batch dispatch does not replay the full global middleware chain of the original route registration (`sanitizer`, `preCtx`, `postCtx`, Multer).
|
|
212
|
+
|
|
170
213
|
### Middleware order and ctx usage
|
|
171
214
|
|
|
172
215
|
Order: `sanitizer` → `preCtx` → `resolve` → `ctx` → `postCtx` → `route.before` → handler.
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
CTX_SYMBOL: () => CTX_SYMBOL,
|
|
34
|
+
batchLeaf: () => batchLeaf,
|
|
34
35
|
bindAll: () => bindAll,
|
|
35
36
|
bindExpressRoutes: () => bindExpressRoutes,
|
|
36
37
|
buildLowProfileLeaf: () => import_rrroutes_contract.buildLowProfileLeaf,
|
|
@@ -383,6 +384,44 @@ function adaptRouteBeforeMw(mw) {
|
|
|
383
384
|
}
|
|
384
385
|
};
|
|
385
386
|
}
|
|
387
|
+
function runRouteBeforeHandler(mw, args) {
|
|
388
|
+
return new Promise((resolve, reject) => {
|
|
389
|
+
let settled = false;
|
|
390
|
+
const next = (err) => {
|
|
391
|
+
if (settled) return;
|
|
392
|
+
settled = true;
|
|
393
|
+
if (err) {
|
|
394
|
+
reject(err);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
resolve();
|
|
398
|
+
};
|
|
399
|
+
try {
|
|
400
|
+
const result = mw({ ...args, next });
|
|
401
|
+
if (result && typeof result.then === "function") {
|
|
402
|
+
;
|
|
403
|
+
result.then(() => {
|
|
404
|
+
if (settled) return;
|
|
405
|
+
settled = true;
|
|
406
|
+
resolve();
|
|
407
|
+
}).catch((err) => {
|
|
408
|
+
if (settled) return;
|
|
409
|
+
settled = true;
|
|
410
|
+
reject(err);
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (!settled) {
|
|
415
|
+
settled = true;
|
|
416
|
+
resolve();
|
|
417
|
+
}
|
|
418
|
+
} catch (err) {
|
|
419
|
+
if (settled) return;
|
|
420
|
+
settled = true;
|
|
421
|
+
reject(err);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
386
425
|
function logHandlerDebugWithRoutesLogger(logger, event) {
|
|
387
426
|
if (!logger || event.type !== "handler") return;
|
|
388
427
|
const payload = [
|
|
@@ -435,6 +474,7 @@ function createRRRoute(router, config) {
|
|
|
435
474
|
const send = config.send ?? defaultSend;
|
|
436
475
|
const { emit: defaultEmitDebug, mode: defaultDebugMode } = createServerDebugEmitter(config.debug);
|
|
437
476
|
const knownLeaves = /* @__PURE__ */ new Map();
|
|
477
|
+
const registeredDefs = /* @__PURE__ */ new Map();
|
|
438
478
|
const decorateDebugEvent = (isVerbose, event, details) => {
|
|
439
479
|
if (!isVerbose || !details) return event;
|
|
440
480
|
return { ...event, ...details };
|
|
@@ -736,6 +776,71 @@ function createRRRoute(router, config) {
|
|
|
736
776
|
};
|
|
737
777
|
router[method](path, ...before, wrapped);
|
|
738
778
|
registered.add(key);
|
|
779
|
+
registeredDefs.set(key, {
|
|
780
|
+
leaf,
|
|
781
|
+
def
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async function invoke(key, args) {
|
|
785
|
+
const registration = registeredDefs.get(key);
|
|
786
|
+
if (!registration) {
|
|
787
|
+
throw new Error(`No controller registered for route: ${key}`);
|
|
788
|
+
}
|
|
789
|
+
const { leaf, def } = registration;
|
|
790
|
+
const req = args.req;
|
|
791
|
+
const res = args.res;
|
|
792
|
+
const next = args.next ?? (() => void 0);
|
|
793
|
+
const parsedParams = leaf.cfg.paramsSchema ? (0, import_rrroutes_contract.lowProfileParse)(leaf.cfg.paramsSchema, args.params) : args.params;
|
|
794
|
+
const parsedQueryInput = leaf.cfg.querySchema ? decodeJsonLikeQueryValue(args.query) : args.query;
|
|
795
|
+
let parsedQuery = parsedQueryInput;
|
|
796
|
+
if (leaf.cfg.querySchema) {
|
|
797
|
+
try {
|
|
798
|
+
parsedQuery = (0, import_rrroutes_contract.lowProfileParse)(
|
|
799
|
+
leaf.cfg.querySchema,
|
|
800
|
+
parsedQueryInput
|
|
801
|
+
);
|
|
802
|
+
} catch (err) {
|
|
803
|
+
const parseError = new Error(
|
|
804
|
+
`Query parsing error: ${err.message ?? String(err)}`
|
|
805
|
+
);
|
|
806
|
+
parseError.raw = JSON.stringify(args.query);
|
|
807
|
+
parseError.cause = err;
|
|
808
|
+
throw parseError;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const parsedBody = leaf.cfg.bodySchema ? (0, import_rrroutes_contract.lowProfileParse)(leaf.cfg.bodySchema, args.body) : args.body;
|
|
812
|
+
const payload = {
|
|
813
|
+
params: parsedParams,
|
|
814
|
+
query: parsedQuery,
|
|
815
|
+
body: parsedBody,
|
|
816
|
+
bodyFiles: args.bodyFiles
|
|
817
|
+
};
|
|
818
|
+
setRouteRequestPayload(res, payload);
|
|
819
|
+
const ctx = args.ctx ?? await config.buildCtx({
|
|
820
|
+
req,
|
|
821
|
+
res
|
|
822
|
+
});
|
|
823
|
+
res.locals[CTX_SYMBOL] = ctx;
|
|
824
|
+
for (const before of def.before ?? []) {
|
|
825
|
+
await runRouteBeforeHandler(before, {
|
|
826
|
+
req,
|
|
827
|
+
res,
|
|
828
|
+
ctx,
|
|
829
|
+
...payload
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
const result = await def.handler({
|
|
833
|
+
req,
|
|
834
|
+
res,
|
|
835
|
+
next,
|
|
836
|
+
ctx,
|
|
837
|
+
params: payload.params,
|
|
838
|
+
query: payload.query,
|
|
839
|
+
body: payload.body,
|
|
840
|
+
bodyFiles: payload.bodyFiles
|
|
841
|
+
});
|
|
842
|
+
const output = validateOutput && leaf.cfg.outputSchema ? (0, import_rrroutes_contract.lowProfileParse)(leaf.cfg.outputSchema, result) : result;
|
|
843
|
+
return output;
|
|
739
844
|
}
|
|
740
845
|
function registerControllers(registry, controllers, all) {
|
|
741
846
|
for (const leaf of registry.all) {
|
|
@@ -780,6 +885,7 @@ function createRRRoute(router, config) {
|
|
|
780
885
|
register,
|
|
781
886
|
registerControllers,
|
|
782
887
|
warnMissingControllers: warnMissing,
|
|
888
|
+
invoke,
|
|
783
889
|
getRegisteredKeys: () => Array.from(registered)
|
|
784
890
|
};
|
|
785
891
|
}
|
|
@@ -793,6 +899,51 @@ function bindAll(router, registry, controllers, config) {
|
|
|
793
899
|
server.registerControllers(registry, controllers);
|
|
794
900
|
return router;
|
|
795
901
|
}
|
|
902
|
+
function batchLeaf(server, path, registry, options) {
|
|
903
|
+
const method = String(options?.method ?? "post").toLowerCase();
|
|
904
|
+
const allowedMethods = ["get", "post", "put", "patch", "delete"];
|
|
905
|
+
if (!allowedMethods.includes(method)) {
|
|
906
|
+
throw new Error(
|
|
907
|
+
`Invalid batch method "${String(options?.method)}". Expected one of: ${allowedMethods.join(", ")}.`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
;
|
|
911
|
+
server.router[method](
|
|
912
|
+
path,
|
|
913
|
+
async (req, res, next) => {
|
|
914
|
+
try {
|
|
915
|
+
const body = req.body;
|
|
916
|
+
if (!isPlainObject2(body)) {
|
|
917
|
+
throw new Error(
|
|
918
|
+
"Batch request body must be a plain object keyed by encoded route identifiers."
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
const output = {};
|
|
922
|
+
for (const [encodedKey, value] of Object.entries(body)) {
|
|
923
|
+
const decodedKey = decodeURIComponent(encodedKey);
|
|
924
|
+
const leaf = registry.byKey[decodedKey];
|
|
925
|
+
if (!leaf) {
|
|
926
|
+
throw new Error(`Unknown batch route key: ${decodedKey}`);
|
|
927
|
+
}
|
|
928
|
+
const payload = isPlainObject2(value) ? value : {};
|
|
929
|
+
output[encodedKey] = await server.invoke(decodedKey, {
|
|
930
|
+
req,
|
|
931
|
+
res,
|
|
932
|
+
next,
|
|
933
|
+
params: payload.params,
|
|
934
|
+
query: payload.query,
|
|
935
|
+
body: payload.body,
|
|
936
|
+
bodyFiles: payload.bodyFiles
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
res.json(output);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
next(err);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
return server.router;
|
|
946
|
+
}
|
|
796
947
|
var defineControllers = () => (m) => m;
|
|
797
948
|
function warnMissingControllers(router, registry, logger) {
|
|
798
949
|
const registeredStore = router[REGISTERED_ROUTES_SYMBOL];
|
|
@@ -1621,6 +1772,7 @@ var createConnectionLoggingMiddleware = (options = {}) => {
|
|
|
1621
1772
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1622
1773
|
0 && (module.exports = {
|
|
1623
1774
|
CTX_SYMBOL,
|
|
1775
|
+
batchLeaf,
|
|
1624
1776
|
bindAll,
|
|
1625
1777
|
bindExpressRoutes,
|
|
1626
1778
|
buildLowProfileLeaf,
|