@emeryld/rrroutes-server 2.2.17 → 2.3.0

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
@@ -22,18 +22,20 @@ This package peers with `@emeryld/rrroutes-contract` and bundles `zod`.
22
22
  ## Quick start: HTTP routes
23
23
 
24
24
  ```ts
25
- import express from 'express';
26
- import { finalize, resource } from '@emeryld/rrroutes-contract';
27
- import { createRRRoute, defineControllers } from '@emeryld/rrroutes-server';
28
- import multer from 'multer';
29
- import { z } from 'zod';
25
+ import express from 'express'
26
+ import { finalize, resource } from '@emeryld/rrroutes-contract'
27
+ import { createRRRoute, defineControllers } from '@emeryld/rrroutes-server'
28
+ import multer from 'multer'
29
+ import { z } from 'zod'
30
30
 
31
31
  // 1) Build & finalize contracts (usually elsewhere in your app)
32
32
  const leaves = resource('/api')
33
33
  .sub('profiles', (r) =>
34
34
  r
35
35
  .get({
36
- outputSchema: z.array(z.object({ id: z.string().uuid(), name: z.string() })),
36
+ outputSchema: z.array(
37
+ z.object({ id: z.string().uuid(), name: z.string() }),
38
+ ),
37
39
  description: 'List profiles',
38
40
  })
39
41
  .routeParameter('profileId', z.string().uuid(), (p) =>
@@ -55,56 +57,73 @@ const leaves = resource('/api')
55
57
  )
56
58
  .done(),
57
59
  )
58
- .done();
60
+ .done()
59
61
 
60
- const registry = finalize(leaves);
62
+ const registry = finalize(leaves)
61
63
 
62
64
  // 2) Wire Express with ctx + derived upload middleware
63
- const upload = multer({ storage: multer.memoryStorage() });
64
- const app = express();
65
+ const upload = multer({ storage: multer.memoryStorage() })
66
+ const app = express()
65
67
  const server = createRRRoute(app, {
66
- buildCtx: async (req) => ({ user: await loadUser(req), routesLogger: console }), // ctx lives on res.locals[CTX_SYMBOL]
68
+ buildCtx: async (req) => ({
69
+ user: await loadUser(req),
70
+ routesLogger: console,
71
+ }), // ctx lives on res.locals[CTX_SYMBOL]
67
72
  globalMiddleware: {
68
73
  before: [
69
74
  ({ ctx, next }) => {
70
- if (!ctx.user) throw new Error('unauthorized');
71
- next();
75
+ if (!ctx.user) throw new Error('unauthorized')
76
+ next()
72
77
  },
73
78
  ],
74
79
  },
75
80
  fromCfg: {
76
- upload: (files) => (files && files.length > 0 ? [upload.fields(files)] : []),
81
+ upload: (files) =>
82
+ files && files.length > 0 ? [upload.fields(files)] : [],
77
83
  },
78
84
  validateOutput: true, // parse handler returns with outputSchema (default true)
79
- debug: { request: true, handler: true, verbose: true, logger: (e) => console.debug(e) },
80
- });
85
+ debug: {
86
+ request: true,
87
+ handler: true,
88
+ verbose: true,
89
+ logger: (e) => console.debug(e),
90
+ },
91
+ })
81
92
 
82
93
  // 3) Author controllers with enforced keys/types
83
- const controllers = defineControllers<typeof registry, { user: { id: string } }>()({
94
+ const controllers = defineControllers<
95
+ typeof registry,
96
+ { user: { id: string } }
97
+ >()({
84
98
  'GET /api/profiles': {
85
99
  handler: async ({ ctx }) => {
86
- return fetchProfilesFor(ctx.user.id);
100
+ return fetchProfilesFor(ctx.user.id)
87
101
  },
88
102
  },
89
103
  'PATCH /api/profiles/:profileId': {
90
- before: [({ ctx, params, next }) => (params.profileId === ctx.user.id ? next() : next(new Error('Forbidden')))],
104
+ before: [
105
+ ({ ctx, params, next }) =>
106
+ params.profileId === ctx.user.id
107
+ ? next()
108
+ : next(new Error('Forbidden')),
109
+ ],
91
110
  handler: async ({ params, body }) => {
92
- return updateProfile(params.profileId, body);
111
+ return updateProfile(params.profileId, body)
93
112
  },
94
113
  },
95
114
  'PUT /api/profiles/:profileId/avatar': {
96
115
  handler: async ({ req, params }) => {
97
- const avatar = (req.files as any)?.avatar?.[0];
98
- await storeAvatar(params.profileId, avatar?.buffer);
99
- return { ok: true };
116
+ const avatar = (req.files as any)?.avatar?.[0]
117
+ await storeAvatar(params.profileId, avatar?.buffer)
118
+ return { ok: true }
100
119
  },
101
120
  },
102
- });
121
+ })
103
122
 
104
- server.registerControllers(registry, controllers);
105
- server.warnMissingControllers(registry, console); // warns in dev about unhandled leaves
123
+ server.registerControllers(registry, controllers)
124
+ server.warnMissingControllers(registry, console) // warns in dev about unhandled leaves
106
125
 
107
- app.listen(3000);
126
+ app.listen(3000)
108
127
  ```
109
128
 
110
129
  ## Detailed usage (HTTP)
@@ -112,19 +131,26 @@ app.listen(3000);
112
131
  ### Controller maps and typing
113
132
 
114
133
  ```ts
115
- import { defineControllers, bindExpressRoutes } from '@emeryld/rrroutes-server';
134
+ import { defineControllers, bindExpressRoutes } from '@emeryld/rrroutes-server'
116
135
 
117
136
  const controllers = defineControllers<typeof registry, Ctx>()({
118
- 'POST /v1/articles': { handler: async ({ body, ctx }) => createArticle(ctx.user.id, body) },
119
- });
137
+ 'POST /v1/articles': {
138
+ handler: async ({ body, ctx }) => createArticle(ctx.user.id, body),
139
+ },
140
+ })
120
141
 
121
142
  // register only the controllers provided (missing keys are ignored)
122
143
  bindExpressRoutes(app, registry, controllers, {
123
144
  buildCtx: () => ({ user: { id: '123' } }),
124
- });
145
+ })
125
146
 
126
147
  // or enforce every key is present at compile time
127
- bindExpressRoutes(app, registry, controllers as { [K in keyof typeof registry.byKey]: any }, { buildCtx });
148
+ bindExpressRoutes(
149
+ app,
150
+ registry,
151
+ controllers as { [K in keyof typeof registry.byKey]: any },
152
+ { buildCtx },
153
+ )
128
154
  ```
129
155
 
130
156
  - `defineControllers<Registry, Ctx>()(map)` keeps literal `"METHOD /path"` keys accurate and infers params/query/body/output types per leaf.
@@ -136,24 +162,24 @@ bindExpressRoutes(app, registry, controllers as { [K in keyof typeof registry.by
136
162
  Order: `buildCtx` → `global.before` → `fromCfg` (derived) → `route.before` → handler → `route.after` → `global.after`.
137
163
 
138
164
  ```ts
139
- import { getCtx, CtxRequestHandler } from '@emeryld/rrroutes-server';
165
+ import { getCtx, CtxRequestHandler } from '@emeryld/rrroutes-server'
140
166
 
141
167
  const audit: CtxRequestHandler<Ctx> = ({ ctx, req, next }) => {
142
- ctx.routesLogger?.info?.('audit', { user: ctx.user?.id, path: req.path });
143
- next();
144
- };
168
+ ctx.routesLogger?.info?.('audit', { user: ctx.user?.id, path: req.path })
169
+ next()
170
+ }
145
171
 
146
172
  const server = createRRRoute(app, {
147
173
  buildCtx: (req, res) => ({ user: res.locals.user, routesLogger: console }),
148
174
  globalMiddleware: { before: [audit] },
149
- });
175
+ })
150
176
 
151
177
  // Inside any Express middleware (even outside route.before/after), use getCtx to retrieve typed ctx:
152
178
  app.use((req, res, next) => {
153
- const ctx = getCtx<Ctx>(res);
154
- ctx?.routesLogger?.debug?.('in arbitrary middleware');
155
- next();
156
- });
179
+ const ctx = getCtx<Ctx>(res)
180
+ ctx?.routesLogger?.debug?.('in arbitrary middleware')
181
+ next()
182
+ })
157
183
  ```
158
184
 
159
185
  - `CtxRequestHandler` receives `{ req, res, next, ctx }` with your typed ctx.
@@ -164,17 +190,18 @@ app.use((req, res, next) => {
164
190
  Use `fromCfg.upload` to attach middleware when a leaf declares `bodyFiles`.
165
191
 
166
192
  ```ts
167
- import multer from 'multer';
168
- import { FileField } from '@emeryld/rrroutes-contract';
193
+ import multer from 'multer'
194
+ import { FileField } from '@emeryld/rrroutes-contract'
169
195
 
170
- const upload = multer({ storage: multer.memoryStorage() });
196
+ const upload = multer({ storage: multer.memoryStorage() })
171
197
 
172
198
  const server = createRRRoute(app, {
173
199
  buildCtx,
174
200
  fromCfg: {
175
- upload: (files: FileField[] | undefined) => (files?.length ? [upload.fields(files)] : []),
201
+ upload: (files: FileField[] | undefined) =>
202
+ files?.length ? [upload.fields(files)] : [],
176
203
  },
177
- });
204
+ })
178
205
  ```
179
206
 
180
207
  ### Output validation and custom responders
@@ -186,7 +213,7 @@ const server = createRRRoute(app, {
186
213
  const server = createRRRoute(app, {
187
214
  buildCtx,
188
215
  send: (res, data) => res.status(201).json({ data }),
189
- });
216
+ })
190
217
  ```
191
218
 
192
219
  ### Debug logging
@@ -203,7 +230,7 @@ const server = createRRRoute(app, {
203
230
  only: ['users:list'], // filter by RouteDef.debug?.debugName
204
231
  logger: (event) => console.log('[route-debug]', event),
205
232
  },
206
- });
233
+ })
207
234
  ```
208
235
 
209
236
  Per-route overrides:
@@ -212,7 +239,7 @@ Per-route overrides:
212
239
  server.register(registry.byKey['GET /api/profiles'], {
213
240
  debug: { handler: true, debugName: 'profiles:list' },
214
241
  handler: async () => [],
215
- });
242
+ })
216
243
  ```
217
244
 
218
245
  Context logger passthrough: if `buildCtx` provides `routesLogger`, handler debug events also flow to that logger (useful for request-scoped loggers).
@@ -228,33 +255,45 @@ Context logger passthrough: if `buildCtx` provides `routesLogger`, handler debug
228
255
  `@emeryld/rrroutes-server` also ships a typed Socket.IO wrapper that pairs with `defineSocketEvents` from the contract package.
229
256
 
230
257
  ```ts
231
- import { Server } from 'socket.io';
232
- import { defineSocketEvents } from '@emeryld/rrroutes-contract';
233
- import { createSocketConnections, createConnectionLoggingMiddleware } from '@emeryld/rrroutes-server';
234
- import { z } from 'zod';
258
+ import { Server } from 'socket.io'
259
+ import { defineSocketEvents } from '@emeryld/rrroutes-contract'
260
+ import {
261
+ createSocketConnections,
262
+ createConnectionLoggingMiddleware,
263
+ } from '@emeryld/rrroutes-server'
264
+ import { z } from 'zod'
235
265
 
236
266
  const { config, events } = defineSocketEvents(
237
267
  {
238
268
  joinMetaMessage: z.object({ room: z.string() }),
239
269
  leaveMetaMessage: z.object({ room: z.string() }),
240
270
  pingPayload: z.object({ sentAt: z.string() }),
241
- pongPayload: z.object({ sentAt: z.string(), sinceMs: z.number().optional() }),
271
+ pongPayload: z.object({
272
+ sentAt: z.string(),
273
+ sinceMs: z.number().optional(),
274
+ }),
242
275
  },
243
276
  {
244
- 'chat:message': { message: z.object({ roomId: z.string(), text: z.string(), userId: z.string() }) },
277
+ 'chat:message': {
278
+ message: z.object({
279
+ roomId: z.string(),
280
+ text: z.string(),
281
+ userId: z.string(),
282
+ }),
283
+ },
245
284
  },
246
- );
285
+ )
247
286
 
248
- const io = new Server(3000, { cors: { origin: '*', credentials: true } });
249
- io.use(createConnectionLoggingMiddleware({ includeHeaders: false }));
287
+ const io = new Server(3000, { cors: { origin: '*', credentials: true } })
288
+ io.use(createConnectionLoggingMiddleware({ includeHeaders: false }))
250
289
 
251
290
  const sockets = createSocketConnections(io, events, {
252
291
  config,
253
292
  heartbeat: { enabled: true }, // enables sys:ping/sys:pong using config schemas
254
293
  sys: {
255
294
  'sys:connect': async ({ socket, complete }) => {
256
- socket.data.user = await loadUserFromHandshake(socket.handshake);
257
- await complete(); // attach built-ins (ping/pong, join/leave)
295
+ socket.data.user = await loadUserFromHandshake(socket.handshake)
296
+ await complete() // attach built-ins (ping/pong, join/leave)
258
297
  },
259
298
  'sys:ping': async ({ socket, ping }) => ({
260
299
  sentAt: ping.sentAt,
@@ -268,17 +307,17 @@ const sockets = createSocketConnections(io, events, {
268
307
  verbose: true,
269
308
  logger: (e) => console.debug('[socket-debug]', e),
270
309
  },
271
- });
310
+ })
272
311
 
273
312
  // Validate inbound payloads + emit envelopes
274
313
  sockets.on('chat:message', async (payload, ctx) => {
275
- await saveMessage(payload, ctx.user);
314
+ await saveMessage(payload, ctx.user)
276
315
  // broadcast to room participants
277
- sockets.emit('chat:message', payload, payload.roomId);
278
- });
316
+ sockets.emit('chat:message', payload, payload.roomId)
317
+ })
279
318
 
280
319
  // Graceful shutdown
281
- process.on('SIGTERM', () => sockets.destroy());
320
+ process.on('SIGTERM', () => sockets.destroy())
282
321
  ```
283
322
 
284
323
  - Payloads are validated on both emit and receive; invalid payloads trigger `<event>:error` with Zod issues.
package/dist/index.cjs CHANGED
@@ -578,7 +578,12 @@ function createBuiltInConnectionHandlers(opts) {
578
578
  const list = toArray(parsed.data.rooms);
579
579
  const join = async (room) => {
580
580
  await socket.join(room);
581
- dbg(null, { type: "rooms", action: "join", rooms: room, socketId: socket.id });
581
+ dbg(null, {
582
+ type: "rooms",
583
+ action: "join",
584
+ rooms: room,
585
+ socketId: socket.id
586
+ });
582
587
  };
583
588
  const run = async () => {
584
589
  await getSysEvent("sys:room_join")({
@@ -592,7 +597,9 @@ function createBuiltInConnectionHandlers(opts) {
592
597
  try {
593
598
  await run();
594
599
  } catch (error) {
595
- socket.emit(`${roomJoinEvent}:error`, { error: normalizeError(error) });
600
+ socket.emit(`${roomJoinEvent}:error`, {
601
+ error: normalizeError(error)
602
+ });
596
603
  dbg(roomJoinEvent, {
597
604
  type: "rooms",
598
605
  action: "join",
@@ -630,7 +637,12 @@ function createBuiltInConnectionHandlers(opts) {
630
637
  const list = toArray(parsed.data.rooms);
631
638
  const leave = async (room) => {
632
639
  await socket.leave(room);
633
- dbg(null, { type: "rooms", action: "leave", rooms: room, socketId: socket.id });
640
+ dbg(null, {
641
+ type: "rooms",
642
+ action: "leave",
643
+ rooms: room,
644
+ socketId: socket.id
645
+ });
634
646
  };
635
647
  const run = async () => {
636
648
  await getSysEvent("sys:room_leave")({
@@ -644,7 +656,9 @@ function createBuiltInConnectionHandlers(opts) {
644
656
  try {
645
657
  await run();
646
658
  } catch (error) {
647
- socket.emit(`${roomLeaveEvent}:error`, { error: normalizeError(error) });
659
+ socket.emit(`${roomLeaveEvent}:error`, {
660
+ error: normalizeError(error)
661
+ });
648
662
  dbg(roomJoinEvent, {
649
663
  type: "rooms",
650
664
  phase: "handler_error",
@@ -693,7 +707,12 @@ function createBuiltInConnectionHandlers(opts) {
693
707
  });
694
708
  let pongPayload;
695
709
  try {
696
- pongPayload = await getSysEvent("sys:ping")({ ping: parsedPing.data, ctx, socket, helper });
710
+ pongPayload = await getSysEvent("sys:ping")({
711
+ ping: parsedPing.data,
712
+ ctx,
713
+ socket,
714
+ helper
715
+ });
697
716
  } catch (error) {
698
717
  socket.emit(`${pingEvent}:error`, { error: normalizeError(error) });
699
718
  dbg(null, {
@@ -858,7 +877,9 @@ function createSocketConnections(io, events, opts) {
858
877
  },
859
878
  metadata
860
879
  });
861
- throw new Error(`Invalid payload for "${String(eventName)}": ${check.error.message}`);
880
+ throw new Error(
881
+ `Invalid payload for "${String(eventName)}": ${check.error.message}`
882
+ );
862
883
  }
863
884
  const envelope = {
864
885
  eventName,
@@ -919,7 +940,12 @@ function createSocketConnections(io, events, opts) {
919
940
  }
920
941
  }
921
942
  registrations.delete(eventName);
922
- dbg(eventName, { type: "register", action: "unregister", event: eventName, msg: "unregistered" });
943
+ dbg(eventName, {
944
+ type: "register",
945
+ action: "unregister",
946
+ event: eventName,
947
+ msg: "unregistered"
948
+ });
923
949
  };
924
950
  const {
925
951
  builtInConnectionListener,
@@ -1005,7 +1031,10 @@ function createSocketConnections(io, events, opts) {
1005
1031
  });
1006
1032
  };
1007
1033
  io.on("connection", connectionListener);
1008
- addRegistration(String(eventName), { connectionListener, socketListeners });
1034
+ addRegistration(String(eventName), {
1035
+ connectionListener,
1036
+ socketListeners
1037
+ });
1009
1038
  dbg(String(eventName), {
1010
1039
  type: "register",
1011
1040
  action: "register",
@@ -1151,7 +1180,9 @@ var createConnectionLoggingMiddleware = (options = {}) => {
1151
1180
  const logger = options.logger ?? defaultLogger;
1152
1181
  const includeHeaders = options.includeHeaders ?? false;
1153
1182
  const redactKeys = new Set(
1154
- (options.redactAuthKeys ?? ["authorization", "token"]).map((key) => key.toLowerCase())
1183
+ (options.redactAuthKeys ?? ["authorization", "token"]).map(
1184
+ (key) => key.toLowerCase()
1185
+ )
1155
1186
  );
1156
1187
  return (socket, next) => {
1157
1188
  const context = buildContext(socket, includeHeaders, redactKeys);