sleepy-serv 0.4.0 → 0.6.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 +38 -47
- package/package.json +8 -9
- package/src/errors.js +53 -41
- package/src/index.js +176 -101
- package/src/messages.js +164 -0
- package/src/middleware.js +83 -61
- package/src/socket.js +542 -0
- package/src/utils.js +49 -0
package/src/index.js
CHANGED
|
@@ -2,9 +2,16 @@ import fs from 'fs'
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import querystring from 'querystring'
|
|
4
4
|
import readline from 'node:readline'
|
|
5
|
+
|
|
5
6
|
import { stdin, stdout } from 'node:process'
|
|
7
|
+
import { toSegments, executeMiddlewareChain } from './utils'
|
|
6
8
|
|
|
7
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
buildSocketState,
|
|
11
|
+
buildSocketServer,
|
|
12
|
+
buildSocketHandlers,
|
|
13
|
+
buildSocketCommands,
|
|
14
|
+
} from './socket'
|
|
8
15
|
|
|
9
16
|
import {
|
|
10
17
|
NotFoundError,
|
|
@@ -13,20 +20,27 @@ import {
|
|
|
13
20
|
|
|
14
21
|
export * from './errors'
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
export {
|
|
24
|
+
parseJsonBody,
|
|
25
|
+
setValidationFormats,
|
|
26
|
+
validateSchemas,
|
|
27
|
+
} from './middleware'
|
|
28
|
+
|
|
29
|
+
const ALLOWED_FILES_META = ['meta.js', 'meta.ts']
|
|
17
30
|
|
|
18
31
|
const ALLOWED_FILES_METHODS = [
|
|
19
32
|
'head.js',
|
|
33
|
+
'head.ts',
|
|
20
34
|
'get.js',
|
|
35
|
+
'get.ts',
|
|
21
36
|
'put.js',
|
|
37
|
+
'put.ts',
|
|
22
38
|
'post.js',
|
|
39
|
+
'post.ts',
|
|
23
40
|
'patch.js',
|
|
41
|
+
'patch.ts',
|
|
24
42
|
'delete.js',
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const ALLOWED_FILES_ALL = [
|
|
28
|
-
...ALLOWED_FILES_META,
|
|
29
|
-
...ALLOWED_FILES_METHODS,
|
|
43
|
+
'delete.ts',
|
|
30
44
|
]
|
|
31
45
|
|
|
32
46
|
/* istanbul ignore if */
|
|
@@ -34,8 +48,6 @@ if (process.stdin.isTTY) {
|
|
|
34
48
|
process.stdin.setRawMode(true)
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
export const middleware = _middleware
|
|
38
|
-
|
|
39
51
|
const rl = readline.createInterface({
|
|
40
52
|
input: stdin,
|
|
41
53
|
output: stdout,
|
|
@@ -45,18 +57,32 @@ function methodNotAllowedHandler (_req) {
|
|
|
45
57
|
throw new MethodNotAllowedError()
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
function defaultMethodMap () {
|
|
61
|
+
return {
|
|
62
|
+
HEAD: methodNotAllowedHandler,
|
|
63
|
+
GET: methodNotAllowedHandler,
|
|
64
|
+
PUT: methodNotAllowedHandler,
|
|
65
|
+
POST: methodNotAllowedHandler,
|
|
66
|
+
PATCH: methodNotAllowedHandler,
|
|
67
|
+
DELETE: methodNotAllowedHandler,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildBunRequest (bunReq, server) {
|
|
72
|
+
const url = new URL(bunReq.url)
|
|
73
|
+
const qs = url.search.replace('?', '')
|
|
74
|
+
const json = () => bunReq.json()
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
method: bunReq.method,
|
|
78
|
+
route: url.pathname,
|
|
79
|
+
headers: bunReq.headers,
|
|
80
|
+
params: bunReq.params ?? {},
|
|
81
|
+
query: querystring.parse(qs),
|
|
82
|
+
raw: bunReq,
|
|
83
|
+
server,
|
|
84
|
+
json,
|
|
85
|
+
}
|
|
60
86
|
}
|
|
61
87
|
|
|
62
88
|
function validateLeafDirectory (targetPath, filenames, entries) {
|
|
@@ -64,7 +90,7 @@ function validateLeafDirectory (targetPath, filenames, entries) {
|
|
|
64
90
|
|
|
65
91
|
if (!hasDirectories) {
|
|
66
92
|
const hasMethodEntry = filenames.some(filename =>
|
|
67
|
-
ALLOWED_FILES_METHODS.includes(filename)
|
|
93
|
+
ALLOWED_FILES_METHODS.includes(filename),
|
|
68
94
|
)
|
|
69
95
|
|
|
70
96
|
if (!hasMethodEntry) {
|
|
@@ -81,7 +107,6 @@ function validateDirectory (targetPath, entries) {
|
|
|
81
107
|
.filter(entry => entry.stat.isFile())
|
|
82
108
|
.map(entry => path.basename(entry.path))
|
|
83
109
|
|
|
84
|
-
validateDirectoryIllegalFiles(targetPath, filenames)
|
|
85
110
|
validateLeafDirectory(targetPath, filenames, entries)
|
|
86
111
|
}
|
|
87
112
|
|
|
@@ -100,19 +125,19 @@ function getAllFilePathsRec (targetPath, paths) {
|
|
|
100
125
|
validateDirectory(targetPath, children)
|
|
101
126
|
|
|
102
127
|
return children.reduce((accum, curr) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
const result = curr.stat.isDirectory()
|
|
129
|
+
? getAllFilePathsRec(curr.path, paths)
|
|
130
|
+
: [curr.path]
|
|
106
131
|
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
return [...accum, ...result]
|
|
133
|
+
}, [])
|
|
109
134
|
}
|
|
110
135
|
|
|
111
136
|
function getFilteredFilePaths (targetPath, allowedFiles) {
|
|
112
137
|
const allPaths = getAllFilePathsRec(targetPath, [])
|
|
113
138
|
|
|
114
139
|
return allPaths.filter(item =>
|
|
115
|
-
allowedFiles.includes(path.basename(item))
|
|
140
|
+
allowedFiles.includes(path.basename(item)),
|
|
116
141
|
)
|
|
117
142
|
}
|
|
118
143
|
|
|
@@ -124,8 +149,24 @@ function getMetaFilePaths (targetPath) {
|
|
|
124
149
|
return getFilteredFilePaths(targetPath, ALLOWED_FILES_META)
|
|
125
150
|
}
|
|
126
151
|
|
|
127
|
-
function
|
|
128
|
-
|
|
152
|
+
function selectMetaPaths (metadata, modulePath) {
|
|
153
|
+
return metadata
|
|
154
|
+
.filter(metaPath => modulePath.startsWith(path.dirname(metaPath)))
|
|
155
|
+
.sort((a, b) => a.length - b.length)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function resolveMetaMiddleware (metaPaths) {
|
|
159
|
+
const modules = await Promise.all(metaPaths.map(item => import(item)))
|
|
160
|
+
|
|
161
|
+
return modules
|
|
162
|
+
.map(item => item.middleware)
|
|
163
|
+
.reduce((accum, curr) => [
|
|
164
|
+
...accum,
|
|
165
|
+
...(curr || []),
|
|
166
|
+
], [])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildRoutePaths (rootPath, mountPath, metadata) {
|
|
129
170
|
const paths = getMethodFilePaths(rootPath)
|
|
130
171
|
|
|
131
172
|
return paths.map(modulePath => {
|
|
@@ -138,13 +179,7 @@ function buildRoutesPaths (rootPath, mountPath) {
|
|
|
138
179
|
.filter(item => item)
|
|
139
180
|
.join('') || '/'
|
|
140
181
|
|
|
141
|
-
const metaMiddlewarePath = metadata
|
|
142
|
-
.filter(metaPath => {
|
|
143
|
-
const metaBasePath = path.dirname(metaPath)
|
|
144
|
-
|
|
145
|
-
return modulePath.startsWith(metaBasePath)
|
|
146
|
-
})
|
|
147
|
-
.sort((a, b) => a.length - b.length)
|
|
182
|
+
const metaMiddlewarePath = selectMetaPaths(metadata, modulePath)
|
|
148
183
|
|
|
149
184
|
return {
|
|
150
185
|
method: segments[lastIndex].toUpperCase(),
|
|
@@ -155,19 +190,9 @@ function buildRoutesPaths (rootPath, mountPath) {
|
|
|
155
190
|
})
|
|
156
191
|
}
|
|
157
192
|
|
|
158
|
-
async function
|
|
193
|
+
async function buildChain (route, rootMiddleware) {
|
|
159
194
|
const module = await import(route.modulePath)
|
|
160
|
-
|
|
161
|
-
const middlewareModules = await Promise.all(
|
|
162
|
-
route.metaMiddlewarePath.map(item => import(item))
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
const metaMiddleware = middlewareModules
|
|
166
|
-
.map(item => item.middleware)
|
|
167
|
-
.reduce((accum, curr) => [
|
|
168
|
-
...accum,
|
|
169
|
-
...(curr || []),
|
|
170
|
-
], [])
|
|
195
|
+
const metaMiddleware = await resolveMetaMiddleware(route.metaMiddlewarePath)
|
|
171
196
|
|
|
172
197
|
if (!module.default) {
|
|
173
198
|
throw new ReferenceError(`
|
|
@@ -180,62 +205,87 @@ ${route.modulePath}
|
|
|
180
205
|
? module.default
|
|
181
206
|
: [module.default]
|
|
182
207
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
return {
|
|
209
|
+
method: route.method,
|
|
210
|
+
path: route.path,
|
|
211
|
+
chain: [
|
|
212
|
+
...rootMiddleware,
|
|
213
|
+
...metaMiddleware,
|
|
214
|
+
...baseChain,
|
|
215
|
+
],
|
|
216
|
+
}
|
|
217
|
+
}
|
|
188
218
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
219
|
+
function buildNormalRoutes (routePaths, rootMiddleware) {
|
|
220
|
+
return Promise.all(
|
|
221
|
+
routePaths.map(route => buildChain(route, rootMiddleware)),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
192
224
|
|
|
193
|
-
|
|
225
|
+
async function buildMergedRoutes (routePaths, middleware, state, opts) {
|
|
226
|
+
const { basePath, mountPath, metadata } = opts
|
|
227
|
+
const socketRoutes = buildSocketHandlers(state)
|
|
194
228
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const isLastMiddleware = index === middlewareChain.length - 1
|
|
229
|
+
for (const socketRoute of socketRoutes) {
|
|
230
|
+
const mountedPath = `${mountPath}${socketRoute.path}`
|
|
198
231
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
232
|
+
const targetItem = routePaths.find(item => (
|
|
233
|
+
item.method === socketRoute.method &&
|
|
234
|
+
item.path === mountedPath
|
|
235
|
+
))
|
|
202
236
|
|
|
203
|
-
|
|
237
|
+
if (targetItem) {
|
|
238
|
+
targetItem.chain.push(socketRoute.handler)
|
|
204
239
|
|
|
205
|
-
|
|
206
|
-
return result
|
|
207
|
-
} else {
|
|
208
|
-
throw new TypeError('Handler does not return a Response object')
|
|
209
|
-
}
|
|
240
|
+
continue
|
|
210
241
|
}
|
|
211
242
|
|
|
212
|
-
|
|
243
|
+
const method = socketRoute.method.toLowerCase()
|
|
244
|
+
const modulePath = path.join(basePath, socketRoute.path, `${method}.js`)
|
|
245
|
+
const metaPaths = selectMetaPaths(metadata, modulePath)
|
|
246
|
+
const metaMiddleware = await resolveMetaMiddleware(metaPaths)
|
|
247
|
+
|
|
248
|
+
routePaths.push({
|
|
249
|
+
method: socketRoute.method,
|
|
250
|
+
path: mountedPath,
|
|
251
|
+
chain: [
|
|
252
|
+
...middleware,
|
|
253
|
+
...metaMiddleware,
|
|
254
|
+
socketRoute.handler,
|
|
255
|
+
],
|
|
256
|
+
})
|
|
213
257
|
}
|
|
214
258
|
|
|
215
|
-
return
|
|
216
|
-
method: route.method,
|
|
217
|
-
path: route.path,
|
|
218
|
-
handler,
|
|
219
|
-
}
|
|
259
|
+
return routePaths
|
|
220
260
|
}
|
|
221
261
|
|
|
222
|
-
function
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
262
|
+
function buildSocketRoutes (mergedRoutes) {
|
|
263
|
+
return mergedRoutes.map(route => ({
|
|
264
|
+
...route,
|
|
265
|
+
segments: toSegments(route.path),
|
|
266
|
+
}))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildModuleRoutes (routePaths) {
|
|
270
|
+
return routePaths.map(route => {
|
|
271
|
+
const handler = async (bunReq, server) => {
|
|
272
|
+
const req = buildBunRequest(bunReq, server)
|
|
273
|
+
|
|
274
|
+
return executeMiddlewareChain(req, route.chain)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
method: route.method,
|
|
279
|
+
path: route.path,
|
|
280
|
+
handler,
|
|
281
|
+
}
|
|
282
|
+
})
|
|
226
283
|
}
|
|
227
284
|
|
|
228
285
|
function buildServerRoutes (moduleRoutes) {
|
|
229
286
|
return moduleRoutes.reduce((accum, curr) => {
|
|
230
287
|
if (!accum[curr.path]) {
|
|
231
|
-
accum[curr.path] =
|
|
232
|
-
HEAD: methodNotAllowedHandler,
|
|
233
|
-
GET: methodNotAllowedHandler,
|
|
234
|
-
PUT: methodNotAllowedHandler,
|
|
235
|
-
POST: methodNotAllowedHandler,
|
|
236
|
-
PATCH: methodNotAllowedHandler,
|
|
237
|
-
DELETE: methodNotAllowedHandler,
|
|
238
|
-
}
|
|
288
|
+
accum[curr.path] = defaultMethodMap()
|
|
239
289
|
}
|
|
240
290
|
|
|
241
291
|
accum[curr.path][curr.method] = curr.handler
|
|
@@ -253,43 +303,65 @@ function buildOutputRoutes (moduleRoutes) {
|
|
|
253
303
|
}, {})
|
|
254
304
|
}
|
|
255
305
|
|
|
256
|
-
async function buildRoutes (rootPath, opts) {
|
|
306
|
+
async function buildRoutes (rootPath, state, opts) {
|
|
257
307
|
const basePath = `${rootPath}/api`
|
|
258
308
|
const mountPath = opts.mountPath || ''
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
309
|
+
const middleware = opts.middleware || []
|
|
310
|
+
const metadata = getMetaFilePaths(basePath)
|
|
311
|
+
const routePaths = buildRoutePaths(basePath, mountPath, metadata)
|
|
312
|
+
const normalRoutes = await buildNormalRoutes(routePaths, middleware)
|
|
313
|
+
|
|
314
|
+
const routingOpts = {
|
|
315
|
+
basePath,
|
|
316
|
+
mountPath,
|
|
317
|
+
metadata,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const mergedRoutes = await buildMergedRoutes(
|
|
321
|
+
normalRoutes,
|
|
322
|
+
middleware,
|
|
323
|
+
state,
|
|
324
|
+
routingOpts,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const socketRoutes = buildSocketRoutes(mergedRoutes)
|
|
328
|
+
const moduleRoutes = buildModuleRoutes(socketRoutes)
|
|
262
329
|
const serverRoutes = buildServerRoutes(moduleRoutes)
|
|
263
330
|
const outputRoutes = buildOutputRoutes(moduleRoutes)
|
|
264
331
|
|
|
265
332
|
return {
|
|
266
333
|
server: serverRoutes,
|
|
334
|
+
socket: socketRoutes,
|
|
267
335
|
output: outputRoutes,
|
|
268
336
|
}
|
|
269
337
|
}
|
|
270
338
|
|
|
271
|
-
function buildServer (port, routes, opts) {
|
|
339
|
+
function buildServer (port, routes, state, opts) {
|
|
272
340
|
const hostname = opts.hostname || '0.0.0.0'
|
|
341
|
+
const websocketServer = buildSocketServer(routes.socket, state)
|
|
273
342
|
|
|
274
343
|
return Bun.serve({
|
|
275
344
|
port,
|
|
276
345
|
hostname,
|
|
277
346
|
routes: routes.server,
|
|
278
|
-
|
|
347
|
+
websocket: websocketServer,
|
|
348
|
+
async fetch (_req, _server) {
|
|
279
349
|
throw new NotFoundError()
|
|
280
350
|
},
|
|
281
351
|
error (err) {
|
|
282
352
|
console.error(err)
|
|
283
353
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
354
|
+
const status = err.constructor.status ?? 500
|
|
355
|
+
|
|
356
|
+
return err.output !== undefined
|
|
357
|
+
? Response.json(err.output, { status })
|
|
358
|
+
: new Response(err.message, { status })
|
|
287
359
|
},
|
|
288
360
|
})
|
|
289
361
|
}
|
|
290
362
|
|
|
291
363
|
function processIO (port, server, opts) {
|
|
292
|
-
const onClose = opts.onClose || (() => {})
|
|
364
|
+
const onClose = opts.onClose || (() => { })
|
|
293
365
|
|
|
294
366
|
console.info(`Running on port: ${port}`)
|
|
295
367
|
console.info('')
|
|
@@ -305,13 +377,16 @@ function processIO (port, server, opts) {
|
|
|
305
377
|
}
|
|
306
378
|
|
|
307
379
|
export async function createApp (port, rootPath, opts = {}) {
|
|
308
|
-
const
|
|
309
|
-
const
|
|
380
|
+
const state = buildSocketState(opts)
|
|
381
|
+
const routes = await buildRoutes(rootPath, state, opts)
|
|
382
|
+
const server = buildServer(port, routes, state, opts)
|
|
383
|
+
const commands = buildSocketCommands(state)
|
|
310
384
|
|
|
311
385
|
processIO(port, server, opts)
|
|
312
386
|
|
|
313
387
|
return {
|
|
314
388
|
routes: routes.output,
|
|
315
389
|
server,
|
|
390
|
+
commands,
|
|
316
391
|
}
|
|
317
392
|
}
|
package/src/messages.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import Ajv from 'ajv'
|
|
2
|
+
import addFormats from 'ajv-formats'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { formatError } from './utils'
|
|
5
|
+
import { UnprocessableContentError } from './errors'
|
|
6
|
+
|
|
7
|
+
export const TYPES = {
|
|
8
|
+
REQUEST: 'request',
|
|
9
|
+
RESPONSE: 'response',
|
|
10
|
+
WELCOME: 'welcome',
|
|
11
|
+
HEARTBEAT: 'heartbeat',
|
|
12
|
+
NOTIFICATION: 'notification',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TYPES_RECEIVED = [
|
|
16
|
+
TYPES.HEARTBEAT,
|
|
17
|
+
TYPES.REQUEST,
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const ajv = new Ajv({
|
|
21
|
+
allErrors: true,
|
|
22
|
+
removeAdditional: 'all',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
addFormats(ajv)
|
|
26
|
+
|
|
27
|
+
const SCHEMA_BASE = {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
id: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
format: 'uuid',
|
|
33
|
+
},
|
|
34
|
+
clientId: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
format: 'uuid',
|
|
37
|
+
},
|
|
38
|
+
type: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
enum: TYPES_RECEIVED,
|
|
41
|
+
},
|
|
42
|
+
timestamp: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
format: 'date-time',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: [
|
|
48
|
+
'id',
|
|
49
|
+
'clientId',
|
|
50
|
+
'type',
|
|
51
|
+
'timestamp',
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const validateHeartbeat = ajv.compile({
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
...SCHEMA_BASE.properties,
|
|
59
|
+
type: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
const: TYPES.HEARTBEAT,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: SCHEMA_BASE.required,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const validateRequest = ajv.compile({
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
...SCHEMA_BASE.properties,
|
|
71
|
+
type: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
const: TYPES.REQUEST,
|
|
74
|
+
},
|
|
75
|
+
method: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
enum: [
|
|
78
|
+
'HEAD',
|
|
79
|
+
'GET',
|
|
80
|
+
'PUT',
|
|
81
|
+
'POST',
|
|
82
|
+
'PATCH',
|
|
83
|
+
'DELETE',
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
route: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
format: 'uri-reference',
|
|
89
|
+
},
|
|
90
|
+
headers: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
},
|
|
93
|
+
query: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
},
|
|
96
|
+
body: {
|
|
97
|
+
type: [
|
|
98
|
+
'boolean',
|
|
99
|
+
'number',
|
|
100
|
+
'string',
|
|
101
|
+
'object',
|
|
102
|
+
'array',
|
|
103
|
+
'null',
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
required: [
|
|
108
|
+
...SCHEMA_BASE.required,
|
|
109
|
+
'method',
|
|
110
|
+
'route',
|
|
111
|
+
'headers',
|
|
112
|
+
'query',
|
|
113
|
+
'body',
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const TYPE_VALIDATORS = {
|
|
118
|
+
[TYPES.HEARTBEAT]: validateHeartbeat,
|
|
119
|
+
[TYPES.REQUEST]: validateRequest,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createMessage (clientId, type, opts = {}) {
|
|
123
|
+
const timestamp = new Date().toISOString()
|
|
124
|
+
|
|
125
|
+
const base = {
|
|
126
|
+
id: opts.id ?? crypto.randomUUID(),
|
|
127
|
+
clientId,
|
|
128
|
+
type,
|
|
129
|
+
timestamp,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...opts,
|
|
134
|
+
...base,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function validateMessage (message) {
|
|
139
|
+
const validate = TYPE_VALIDATORS[message.type]
|
|
140
|
+
|
|
141
|
+
if (message.type === undefined) {
|
|
142
|
+
throw new UnprocessableContentError([
|
|
143
|
+
{
|
|
144
|
+
path: '',
|
|
145
|
+
message: `must have required property 'type'`,
|
|
146
|
+
},
|
|
147
|
+
])
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!TYPES_RECEIVED.includes(message.type)) {
|
|
151
|
+
throw new UnprocessableContentError([
|
|
152
|
+
{
|
|
153
|
+
path: 'type',
|
|
154
|
+
message: `must be one of: ${TYPES_RECEIVED}`,
|
|
155
|
+
},
|
|
156
|
+
])
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!validate(message)) {
|
|
160
|
+
const errors = validate.errors.map(item => formatError('', item))
|
|
161
|
+
|
|
162
|
+
throw new UnprocessableContentError(errors)
|
|
163
|
+
}
|
|
164
|
+
}
|