sleepy-serv 0.6.2 → 0.7.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 +1 -1
- package/dist/errors.d.ts +173 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/messages.d.ts +63 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/middleware.d.ts +19 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/socket.d.ts +50 -0
- package/dist/socket.d.ts.map +1 -0
- package/dist/utils.d.ts +125 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +17 -3
- package/src/{errors.js → errors.ts} +171 -80
- package/src/{index.js → index.ts} +180 -42
- package/src/messages.ts +237 -0
- package/src/middleware.ts +209 -0
- package/src/{socket.js → socket.ts} +247 -86
- package/src/utils.ts +204 -0
- package/src/messages.js +0 -164
- package/src/meta.js +0 -65
- package/src/middleware.js +0 -138
- package/src/utils.js +0 -49
|
@@ -4,7 +4,7 @@ import querystring from 'querystring'
|
|
|
4
4
|
import readline from 'node:readline'
|
|
5
5
|
|
|
6
6
|
import { stdin, stdout } from 'node:process'
|
|
7
|
-
import { toSegments, executeMiddlewareChain } from './utils'
|
|
7
|
+
import { StatusCode, toSegments, executeMiddlewareChain } from './utils'
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
buildSocketState,
|
|
@@ -14,10 +14,27 @@ import {
|
|
|
14
14
|
} from './socket'
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
+
RequestError,
|
|
17
18
|
NotFoundError,
|
|
18
19
|
MethodNotAllowedError,
|
|
19
20
|
} from './errors'
|
|
20
21
|
|
|
22
|
+
import type { BunRequest } from 'bun'
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
HttpMethod,
|
|
26
|
+
Middleware,
|
|
27
|
+
EndpointRequest,
|
|
28
|
+
AppOptions,
|
|
29
|
+
Server,
|
|
30
|
+
} from './utils'
|
|
31
|
+
|
|
32
|
+
import type {
|
|
33
|
+
SocketRoute,
|
|
34
|
+
SocketState,
|
|
35
|
+
SocketCommands,
|
|
36
|
+
} from './socket'
|
|
37
|
+
|
|
21
38
|
export * from './errors'
|
|
22
39
|
|
|
23
40
|
export {
|
|
@@ -26,6 +43,78 @@ export {
|
|
|
26
43
|
validateSchemas,
|
|
27
44
|
} from './middleware'
|
|
28
45
|
|
|
46
|
+
export { HttpMethod, StatusCode } from './utils'
|
|
47
|
+
|
|
48
|
+
export type {
|
|
49
|
+
AppOptions,
|
|
50
|
+
EndpointRequest,
|
|
51
|
+
FormattedError,
|
|
52
|
+
Middleware,
|
|
53
|
+
NextFn,
|
|
54
|
+
Request,
|
|
55
|
+
Server,
|
|
56
|
+
SocketOptions,
|
|
57
|
+
WebSocketRequest,
|
|
58
|
+
} from './utils'
|
|
59
|
+
|
|
60
|
+
export type {
|
|
61
|
+
FormatterField,
|
|
62
|
+
FormatterSchema,
|
|
63
|
+
ValidationSchemas,
|
|
64
|
+
} from './middleware'
|
|
65
|
+
|
|
66
|
+
export type { SocketCommands } from './socket'
|
|
67
|
+
|
|
68
|
+
type OutputRoutes = Record<string, string[]>
|
|
69
|
+
type ServerRoutes = Record<string, Record<string, EndpointHandler>>
|
|
70
|
+
|
|
71
|
+
type EndpointHandler = (
|
|
72
|
+
bunReq: BunRequest,
|
|
73
|
+
server: Server,
|
|
74
|
+
) => Promise<Response>
|
|
75
|
+
|
|
76
|
+
type DirEntry = {
|
|
77
|
+
path: string
|
|
78
|
+
stat: fs.Stats
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type RoutePath = {
|
|
82
|
+
method: HttpMethod
|
|
83
|
+
path: string
|
|
84
|
+
metaMiddlewarePath: string[]
|
|
85
|
+
modulePath: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type ChainRoute = {
|
|
89
|
+
method: HttpMethod
|
|
90
|
+
path: string
|
|
91
|
+
chain: Middleware[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type ModuleRoute = {
|
|
95
|
+
method: HttpMethod
|
|
96
|
+
path: string
|
|
97
|
+
handler: EndpointHandler
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type RoutingOptions = {
|
|
101
|
+
basePath: string
|
|
102
|
+
mountPath: string
|
|
103
|
+
metadata: string[]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type AppRoutes = {
|
|
107
|
+
server: ServerRoutes
|
|
108
|
+
output: OutputRoutes
|
|
109
|
+
socket: SocketRoute[]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type App = {
|
|
113
|
+
server: Server
|
|
114
|
+
commands: SocketCommands
|
|
115
|
+
routes: OutputRoutes
|
|
116
|
+
}
|
|
117
|
+
|
|
29
118
|
const ALLOWED_FILES_META = ['meta.js', 'meta.ts']
|
|
30
119
|
|
|
31
120
|
const ALLOWED_FILES_METHODS = [
|
|
@@ -53,11 +142,11 @@ const rl = readline.createInterface({
|
|
|
53
142
|
output: stdout,
|
|
54
143
|
})
|
|
55
144
|
|
|
56
|
-
function methodNotAllowedHandler (_req) {
|
|
145
|
+
function methodNotAllowedHandler (_req: unknown): never {
|
|
57
146
|
throw new MethodNotAllowedError()
|
|
58
147
|
}
|
|
59
148
|
|
|
60
|
-
function defaultMethodMap () {
|
|
149
|
+
function defaultMethodMap (): Record<string, EndpointHandler> {
|
|
61
150
|
return {
|
|
62
151
|
HEAD: methodNotAllowedHandler,
|
|
63
152
|
GET: methodNotAllowedHandler,
|
|
@@ -68,13 +157,16 @@ function defaultMethodMap () {
|
|
|
68
157
|
}
|
|
69
158
|
}
|
|
70
159
|
|
|
71
|
-
function
|
|
160
|
+
function buildEndpointRequest (
|
|
161
|
+
bunReq: BunRequest,
|
|
162
|
+
server: Server,
|
|
163
|
+
): EndpointRequest {
|
|
72
164
|
const url = new URL(bunReq.url)
|
|
73
165
|
const qs = url.search.replace('?', '')
|
|
74
166
|
const json = () => bunReq.json()
|
|
75
167
|
|
|
76
168
|
return {
|
|
77
|
-
method: bunReq.method,
|
|
169
|
+
method: bunReq.method as HttpMethod,
|
|
78
170
|
route: url.pathname,
|
|
79
171
|
headers: bunReq.headers,
|
|
80
172
|
params: bunReq.params ?? {},
|
|
@@ -85,7 +177,11 @@ function buildBunRequest (bunReq, server) {
|
|
|
85
177
|
}
|
|
86
178
|
}
|
|
87
179
|
|
|
88
|
-
function validateLeafDirectory (
|
|
180
|
+
function validateLeafDirectory (
|
|
181
|
+
targetPath: string,
|
|
182
|
+
filenames: string[],
|
|
183
|
+
entries: DirEntry[],
|
|
184
|
+
): void {
|
|
89
185
|
const hasDirectories = entries.some(entry => entry.stat.isDirectory())
|
|
90
186
|
|
|
91
187
|
if (!hasDirectories) {
|
|
@@ -102,7 +198,7 @@ ${targetPath}
|
|
|
102
198
|
}
|
|
103
199
|
}
|
|
104
200
|
|
|
105
|
-
function validateDirectory (targetPath, entries) {
|
|
201
|
+
function validateDirectory (targetPath: string, entries: DirEntry[]): void {
|
|
106
202
|
const filenames = entries
|
|
107
203
|
.filter(entry => entry.stat.isFile())
|
|
108
204
|
.map(entry => path.basename(entry.path))
|
|
@@ -110,7 +206,7 @@ function validateDirectory (targetPath, entries) {
|
|
|
110
206
|
validateLeafDirectory(targetPath, filenames, entries)
|
|
111
207
|
}
|
|
112
208
|
|
|
113
|
-
function getAllFilePathsRec (targetPath, paths) {
|
|
209
|
+
function getAllFilePathsRec (targetPath: string, paths: string[]): string[] {
|
|
114
210
|
const entries = fs.readdirSync(targetPath)
|
|
115
211
|
|
|
116
212
|
const children = entries.map(item => {
|
|
@@ -124,7 +220,7 @@ function getAllFilePathsRec (targetPath, paths) {
|
|
|
124
220
|
|
|
125
221
|
validateDirectory(targetPath, children)
|
|
126
222
|
|
|
127
|
-
return children.reduce((accum, curr) => {
|
|
223
|
+
return children.reduce<string[]>((accum, curr) => {
|
|
128
224
|
const result = curr.stat.isDirectory()
|
|
129
225
|
? getAllFilePathsRec(curr.path, paths)
|
|
130
226
|
: [curr.path]
|
|
@@ -133,7 +229,10 @@ function getAllFilePathsRec (targetPath, paths) {
|
|
|
133
229
|
}, [])
|
|
134
230
|
}
|
|
135
231
|
|
|
136
|
-
function getFilteredFilePaths (
|
|
232
|
+
function getFilteredFilePaths (
|
|
233
|
+
targetPath: string,
|
|
234
|
+
allowedFiles: string[],
|
|
235
|
+
): string[] {
|
|
137
236
|
const allPaths = getAllFilePathsRec(targetPath, [])
|
|
138
237
|
|
|
139
238
|
return allPaths.filter(item =>
|
|
@@ -141,21 +240,23 @@ function getFilteredFilePaths (targetPath, allowedFiles) {
|
|
|
141
240
|
)
|
|
142
241
|
}
|
|
143
242
|
|
|
144
|
-
function getMethodFilePaths (targetPath) {
|
|
243
|
+
function getMethodFilePaths (targetPath: string): string[] {
|
|
145
244
|
return getFilteredFilePaths(targetPath, ALLOWED_FILES_METHODS)
|
|
146
245
|
}
|
|
147
246
|
|
|
148
|
-
function getMetaFilePaths (targetPath) {
|
|
247
|
+
function getMetaFilePaths (targetPath: string): string[] {
|
|
149
248
|
return getFilteredFilePaths(targetPath, ALLOWED_FILES_META)
|
|
150
249
|
}
|
|
151
250
|
|
|
152
|
-
function selectMetaPaths (metadata, modulePath) {
|
|
251
|
+
function selectMetaPaths (metadata: string[], modulePath: string): string[] {
|
|
153
252
|
return metadata
|
|
154
253
|
.filter(metaPath => modulePath.startsWith(path.dirname(metaPath)))
|
|
155
254
|
.sort((a, b) => a.length - b.length)
|
|
156
255
|
}
|
|
157
256
|
|
|
158
|
-
async function resolveMetaMiddleware (
|
|
257
|
+
async function resolveMetaMiddleware (
|
|
258
|
+
metaPaths: string[],
|
|
259
|
+
): Promise<Middleware[]> {
|
|
159
260
|
const modules = await Promise.all(metaPaths.map(item => import(item)))
|
|
160
261
|
|
|
161
262
|
return modules
|
|
@@ -166,7 +267,11 @@ async function resolveMetaMiddleware (metaPaths) {
|
|
|
166
267
|
], [])
|
|
167
268
|
}
|
|
168
269
|
|
|
169
|
-
function buildRoutePaths (
|
|
270
|
+
function buildRoutePaths (
|
|
271
|
+
rootPath: string,
|
|
272
|
+
mountPath: string,
|
|
273
|
+
metadata: string[],
|
|
274
|
+
): RoutePath[] {
|
|
170
275
|
const paths = getMethodFilePaths(rootPath)
|
|
171
276
|
|
|
172
277
|
return paths.map(modulePath => {
|
|
@@ -182,7 +287,7 @@ function buildRoutePaths (rootPath, mountPath, metadata) {
|
|
|
182
287
|
const metaMiddlewarePath = selectMetaPaths(metadata, modulePath)
|
|
183
288
|
|
|
184
289
|
return {
|
|
185
|
-
method: segments[lastIndex].toUpperCase(),
|
|
290
|
+
method: segments[lastIndex].toUpperCase() as HttpMethod,
|
|
186
291
|
path: joinedPath,
|
|
187
292
|
metaMiddlewarePath,
|
|
188
293
|
modulePath,
|
|
@@ -190,7 +295,10 @@ function buildRoutePaths (rootPath, mountPath, metadata) {
|
|
|
190
295
|
})
|
|
191
296
|
}
|
|
192
297
|
|
|
193
|
-
async function buildChain (
|
|
298
|
+
async function buildChain (
|
|
299
|
+
route: RoutePath,
|
|
300
|
+
rootMiddleware: Middleware[],
|
|
301
|
+
): Promise<ChainRoute> {
|
|
194
302
|
const module = await import(route.modulePath)
|
|
195
303
|
const metaMiddleware = await resolveMetaMiddleware(route.metaMiddlewarePath)
|
|
196
304
|
|
|
@@ -216,13 +324,21 @@ ${route.modulePath}
|
|
|
216
324
|
}
|
|
217
325
|
}
|
|
218
326
|
|
|
219
|
-
function buildNormalRoutes (
|
|
327
|
+
function buildNormalRoutes (
|
|
328
|
+
routePaths: RoutePath[],
|
|
329
|
+
rootMiddleware: Middleware[],
|
|
330
|
+
): Promise<ChainRoute[]> {
|
|
220
331
|
return Promise.all(
|
|
221
332
|
routePaths.map(route => buildChain(route, rootMiddleware)),
|
|
222
333
|
)
|
|
223
334
|
}
|
|
224
335
|
|
|
225
|
-
async function buildMergedRoutes (
|
|
336
|
+
async function buildMergedRoutes (
|
|
337
|
+
routePaths: ChainRoute[],
|
|
338
|
+
middleware: Middleware[],
|
|
339
|
+
state: SocketState,
|
|
340
|
+
opts: RoutingOptions,
|
|
341
|
+
): Promise<ChainRoute[]> {
|
|
226
342
|
const { basePath, mountPath, metadata } = opts
|
|
227
343
|
const socketRoutes = buildSocketHandlers(state)
|
|
228
344
|
|
|
@@ -259,17 +375,17 @@ async function buildMergedRoutes (routePaths, middleware, state, opts) {
|
|
|
259
375
|
return routePaths
|
|
260
376
|
}
|
|
261
377
|
|
|
262
|
-
function
|
|
378
|
+
function buildSocketRoutes (mergedRoutes: ChainRoute[]): SocketRoute[] {
|
|
263
379
|
return mergedRoutes.map(route => ({
|
|
264
380
|
...route,
|
|
265
381
|
segments: toSegments(route.path),
|
|
266
382
|
}))
|
|
267
383
|
}
|
|
268
384
|
|
|
269
|
-
function buildModuleRoutes (routePaths) {
|
|
385
|
+
function buildModuleRoutes (routePaths: SocketRoute[]): ModuleRoute[] {
|
|
270
386
|
return routePaths.map(route => {
|
|
271
|
-
const handler = async (bunReq, server) => {
|
|
272
|
-
const req =
|
|
387
|
+
const handler: EndpointHandler = async (bunReq, server) => {
|
|
388
|
+
const req = buildEndpointRequest(bunReq, server)
|
|
273
389
|
|
|
274
390
|
return executeMiddlewareChain(req, route.chain)
|
|
275
391
|
}
|
|
@@ -282,20 +398,21 @@ function buildModuleRoutes (routePaths) {
|
|
|
282
398
|
})
|
|
283
399
|
}
|
|
284
400
|
|
|
285
|
-
function buildServerRoutes (moduleRoutes) {
|
|
286
|
-
return moduleRoutes.reduce(
|
|
287
|
-
|
|
288
|
-
accum[curr.path]
|
|
289
|
-
|
|
401
|
+
function buildServerRoutes (moduleRoutes: ModuleRoute[]): ServerRoutes {
|
|
402
|
+
return moduleRoutes.reduce<ServerRoutes>(
|
|
403
|
+
(accum, curr) => {
|
|
404
|
+
if (!accum[curr.path]) {
|
|
405
|
+
accum[curr.path] = defaultMethodMap()
|
|
406
|
+
}
|
|
290
407
|
|
|
291
|
-
|
|
408
|
+
accum[curr.path][curr.method] = curr.handler
|
|
292
409
|
|
|
293
|
-
|
|
294
|
-
|
|
410
|
+
return accum
|
|
411
|
+
}, {})
|
|
295
412
|
}
|
|
296
413
|
|
|
297
|
-
function buildOutputRoutes (moduleRoutes) {
|
|
298
|
-
return moduleRoutes.reduce((accum, curr) => {
|
|
414
|
+
function buildOutputRoutes (moduleRoutes: ModuleRoute[]): OutputRoutes {
|
|
415
|
+
return moduleRoutes.reduce<OutputRoutes>((accum, curr) => {
|
|
299
416
|
accum[curr.path] = accum[curr.path] || []
|
|
300
417
|
accum[curr.path].push(curr.method)
|
|
301
418
|
|
|
@@ -303,7 +420,11 @@ function buildOutputRoutes (moduleRoutes) {
|
|
|
303
420
|
}, {})
|
|
304
421
|
}
|
|
305
422
|
|
|
306
|
-
async function buildRoutes (
|
|
423
|
+
async function buildRoutes (
|
|
424
|
+
rootPath: string,
|
|
425
|
+
state: SocketState,
|
|
426
|
+
opts: AppOptions,
|
|
427
|
+
): Promise<AppRoutes> {
|
|
307
428
|
const basePath = `${rootPath}/api`
|
|
308
429
|
const mountPath = opts.mountPath || ''
|
|
309
430
|
const middleware = opts.middleware || []
|
|
@@ -336,7 +457,12 @@ async function buildRoutes (rootPath, state, opts) {
|
|
|
336
457
|
}
|
|
337
458
|
}
|
|
338
459
|
|
|
339
|
-
function buildServer (
|
|
460
|
+
function buildServer (
|
|
461
|
+
port: number,
|
|
462
|
+
routes: AppRoutes,
|
|
463
|
+
state: SocketState,
|
|
464
|
+
opts: AppOptions,
|
|
465
|
+
): Server {
|
|
340
466
|
const hostname = opts.hostname || '0.0.0.0'
|
|
341
467
|
const websocketServer = buildSocketServer(routes.socket, state)
|
|
342
468
|
|
|
@@ -351,17 +477,25 @@ function buildServer (port, routes, state, opts) {
|
|
|
351
477
|
error (err) {
|
|
352
478
|
console.error(err)
|
|
353
479
|
|
|
354
|
-
|
|
480
|
+
if (err instanceof RequestError) {
|
|
481
|
+
const ctor = err.constructor as typeof RequestError
|
|
482
|
+
|
|
483
|
+
return Response.json(err.output, { status: ctor.status })
|
|
484
|
+
}
|
|
355
485
|
|
|
356
|
-
return err.
|
|
357
|
-
|
|
358
|
-
|
|
486
|
+
return new Response(err.message, {
|
|
487
|
+
status: StatusCode.InternalServerError,
|
|
488
|
+
})
|
|
359
489
|
},
|
|
360
490
|
})
|
|
361
491
|
}
|
|
362
492
|
|
|
363
|
-
function processIO (
|
|
364
|
-
|
|
493
|
+
function processIO (
|
|
494
|
+
port: number,
|
|
495
|
+
server: Server,
|
|
496
|
+
opts: AppOptions,
|
|
497
|
+
): void {
|
|
498
|
+
const onClose = opts.onClose || (() => {})
|
|
365
499
|
|
|
366
500
|
console.info(`Running on port: ${port}`)
|
|
367
501
|
console.info('')
|
|
@@ -376,7 +510,11 @@ function processIO (port, server, opts) {
|
|
|
376
510
|
})
|
|
377
511
|
}
|
|
378
512
|
|
|
379
|
-
export async function createApp (
|
|
513
|
+
export async function createApp (
|
|
514
|
+
port: number,
|
|
515
|
+
rootPath: string,
|
|
516
|
+
opts: AppOptions = {},
|
|
517
|
+
): Promise<App> {
|
|
380
518
|
const state = buildSocketState(opts)
|
|
381
519
|
const routes = await buildRoutes(rootPath, state, opts)
|
|
382
520
|
const server = buildServer(port, routes, state, opts)
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
import type { ValidateFunction } from 'ajv'
|
|
8
|
+
import type { HttpMethod } from './utils'
|
|
9
|
+
|
|
10
|
+
export const MessageType = {
|
|
11
|
+
Request: 'request',
|
|
12
|
+
Response: 'response',
|
|
13
|
+
Welcome: 'welcome',
|
|
14
|
+
Heartbeat: 'heartbeat',
|
|
15
|
+
Notification: 'notification',
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
export type MessageType = typeof MessageType[keyof typeof MessageType]
|
|
19
|
+
|
|
20
|
+
export const RECEIVED_MESSAGE_TYPES: string[] = [
|
|
21
|
+
MessageType.Heartbeat,
|
|
22
|
+
MessageType.Request,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export type BaseMessage = {
|
|
26
|
+
id: string
|
|
27
|
+
clientId: string
|
|
28
|
+
type: MessageType
|
|
29
|
+
timestamp: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type HeartbeatMessage = BaseMessage & {
|
|
33
|
+
type: typeof MessageType.Heartbeat
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type RequestMessage = BaseMessage & {
|
|
37
|
+
type: typeof MessageType.Request
|
|
38
|
+
method: HttpMethod
|
|
39
|
+
route: string
|
|
40
|
+
headers: Bun.HeadersInit
|
|
41
|
+
query: Record<string, unknown>
|
|
42
|
+
body: unknown
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ResponseMessage = BaseMessage & {
|
|
46
|
+
type: typeof MessageType.Response
|
|
47
|
+
status: number
|
|
48
|
+
headers: Headers
|
|
49
|
+
body: unknown
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type WelcomeMessage = BaseMessage & {
|
|
53
|
+
type: typeof MessageType.Welcome
|
|
54
|
+
headers: Headers
|
|
55
|
+
body: {
|
|
56
|
+
heartbeatInterval: number
|
|
57
|
+
token: string
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type NotificationMessage = BaseMessage & {
|
|
62
|
+
type: typeof MessageType.Notification
|
|
63
|
+
event: string
|
|
64
|
+
headers: Headers
|
|
65
|
+
body: unknown
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type Message =
|
|
69
|
+
| HeartbeatMessage
|
|
70
|
+
| RequestMessage
|
|
71
|
+
| ResponseMessage
|
|
72
|
+
| WelcomeMessage
|
|
73
|
+
| NotificationMessage
|
|
74
|
+
|
|
75
|
+
export type RawMessage = {
|
|
76
|
+
type?: string
|
|
77
|
+
[key: string]: unknown
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type IncomingMessage = HeartbeatMessage | RequestMessage
|
|
81
|
+
|
|
82
|
+
export type MessageContent<T extends MessageType = MessageType> =
|
|
83
|
+
Partial<Pick<Extract<Message, { type: T }>, 'id'>>
|
|
84
|
+
& Omit<Extract<Message, { type: T }>, keyof BaseMessage>
|
|
85
|
+
|
|
86
|
+
const ajv = new Ajv({
|
|
87
|
+
allErrors: true,
|
|
88
|
+
removeAdditional: 'all',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
addFormats(ajv)
|
|
92
|
+
|
|
93
|
+
const SCHEMA_BASE = {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
id: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
format: 'uuid',
|
|
99
|
+
},
|
|
100
|
+
clientId: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
format: 'uuid',
|
|
103
|
+
},
|
|
104
|
+
type: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
enum: RECEIVED_MESSAGE_TYPES,
|
|
107
|
+
},
|
|
108
|
+
timestamp: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
format: 'date-time',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
required: [
|
|
114
|
+
'id',
|
|
115
|
+
'clientId',
|
|
116
|
+
'type',
|
|
117
|
+
'timestamp',
|
|
118
|
+
],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const validateHeartbeat = ajv.compile({
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
...SCHEMA_BASE.properties,
|
|
125
|
+
type: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
const: MessageType.Heartbeat,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: SCHEMA_BASE.required,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const validateRequest = ajv.compile({
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
...SCHEMA_BASE.properties,
|
|
137
|
+
type: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
const: MessageType.Request,
|
|
140
|
+
},
|
|
141
|
+
method: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
enum: [
|
|
144
|
+
'HEAD',
|
|
145
|
+
'GET',
|
|
146
|
+
'PUT',
|
|
147
|
+
'POST',
|
|
148
|
+
'PATCH',
|
|
149
|
+
'DELETE',
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
route: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
format: 'uri-reference',
|
|
155
|
+
},
|
|
156
|
+
headers: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
},
|
|
159
|
+
query: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
},
|
|
162
|
+
body: {
|
|
163
|
+
type: [
|
|
164
|
+
'boolean',
|
|
165
|
+
'number',
|
|
166
|
+
'string',
|
|
167
|
+
'object',
|
|
168
|
+
'array',
|
|
169
|
+
'null',
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
required: [
|
|
174
|
+
...SCHEMA_BASE.required,
|
|
175
|
+
'method',
|
|
176
|
+
'route',
|
|
177
|
+
'headers',
|
|
178
|
+
'query',
|
|
179
|
+
'body',
|
|
180
|
+
],
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const TYPE_VALIDATORS: Record<string, ValidateFunction> = {
|
|
184
|
+
[MessageType.Heartbeat]: validateHeartbeat,
|
|
185
|
+
[MessageType.Request]: validateRequest,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createMessage<T extends MessageType> (
|
|
189
|
+
clientId: string,
|
|
190
|
+
type: T,
|
|
191
|
+
content: MessageContent<T>,
|
|
192
|
+
): Extract<Message, { type: T }> {
|
|
193
|
+
const id = content.id ?? crypto.randomUUID()
|
|
194
|
+
const timestamp = new Date().toISOString()
|
|
195
|
+
|
|
196
|
+
const base = {
|
|
197
|
+
id,
|
|
198
|
+
clientId,
|
|
199
|
+
type,
|
|
200
|
+
timestamp,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
...content,
|
|
205
|
+
...base,
|
|
206
|
+
} as Extract<Message, { type: T }>
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function validateMessage (message: RawMessage): IncomingMessage {
|
|
210
|
+
if (message.type === undefined) {
|
|
211
|
+
throw new UnprocessableContentError([
|
|
212
|
+
{
|
|
213
|
+
path: '',
|
|
214
|
+
message: `must have required property 'type'`,
|
|
215
|
+
},
|
|
216
|
+
])
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!RECEIVED_MESSAGE_TYPES.includes(message.type)) {
|
|
220
|
+
throw new UnprocessableContentError([
|
|
221
|
+
{
|
|
222
|
+
path: 'type',
|
|
223
|
+
message: `must be one of: ${RECEIVED_MESSAGE_TYPES}`,
|
|
224
|
+
},
|
|
225
|
+
])
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const validate = TYPE_VALIDATORS[message.type]
|
|
229
|
+
|
|
230
|
+
if (!validate(message)) {
|
|
231
|
+
const errors = validate.errors!.map(item => formatError('', item))
|
|
232
|
+
|
|
233
|
+
throw new UnprocessableContentError(errors)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return message as IncomingMessage
|
|
237
|
+
}
|