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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import Ajv from 'ajv'
|
|
2
|
+
import addFormats from 'ajv-formats'
|
|
3
|
+
|
|
4
|
+
import { formatError } from './utils'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
BadRequestError,
|
|
8
|
+
UnsupportedMediaTypeError,
|
|
9
|
+
UnprocessableContentError,
|
|
10
|
+
} from './errors'
|
|
11
|
+
|
|
12
|
+
import type { Format, Schema, ValidateFunction } from 'ajv'
|
|
13
|
+
import type { FormattedError, Middleware, NextFn, Request } from './utils'
|
|
14
|
+
|
|
15
|
+
export type FormatterField = {
|
|
16
|
+
type: string
|
|
17
|
+
value: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type FormatterSchema = Record<string, FormatterField>
|
|
21
|
+
|
|
22
|
+
export type ValidationSchemas = {
|
|
23
|
+
headers?: FormatterSchema
|
|
24
|
+
params?: FormatterSchema
|
|
25
|
+
query?: FormatterSchema
|
|
26
|
+
body?: Schema
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SchemaKey = keyof ValidationSchemas
|
|
30
|
+
|
|
31
|
+
function requireNext (next: NextFn | null): NextFn {
|
|
32
|
+
if (!next) {
|
|
33
|
+
throw new TypeError('Middleware cannot be the last entry in a chain')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return next
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let _schemasCompiled = false
|
|
40
|
+
let _customFormats: Record<string, Format> | null = null
|
|
41
|
+
|
|
42
|
+
async function parseBody (req: Request): Promise<unknown> {
|
|
43
|
+
try {
|
|
44
|
+
const result = await req.json()
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
} catch {
|
|
48
|
+
throw new BadRequestError('Invalid JSON')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildFormatterSchema (schema: FormatterSchema): Schema {
|
|
53
|
+
const properties = Object
|
|
54
|
+
.entries(schema)
|
|
55
|
+
.map(([key, config]): [string, Record<string, unknown>] => [
|
|
56
|
+
key,
|
|
57
|
+
{
|
|
58
|
+
type: 'string',
|
|
59
|
+
[config.type]: config.value,
|
|
60
|
+
},
|
|
61
|
+
])
|
|
62
|
+
.reduce((accum: Record<string, unknown>, [key, value]) => ({
|
|
63
|
+
...accum,
|
|
64
|
+
[key]: value,
|
|
65
|
+
}), {})
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeHeaderKeys (schema: FormatterSchema): FormatterSchema {
|
|
74
|
+
return Object.fromEntries(
|
|
75
|
+
Object
|
|
76
|
+
.entries(schema)
|
|
77
|
+
.map(([field, config]) => [field.toLowerCase(), config]),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function compileSchemas (
|
|
82
|
+
schemas: ValidationSchemas,
|
|
83
|
+
): [SchemaKey, ValidateFunction][] {
|
|
84
|
+
return (Object.entries(schemas) as [SchemaKey, FormatterSchema | Schema][])
|
|
85
|
+
.reduce((
|
|
86
|
+
accum: [SchemaKey, Schema][],
|
|
87
|
+
[key, schema],
|
|
88
|
+
): [SchemaKey, Schema][] => {
|
|
89
|
+
const formatterSchema = key === 'headers'
|
|
90
|
+
? normalizeHeaderKeys(schema as FormatterSchema)
|
|
91
|
+
: schema as FormatterSchema
|
|
92
|
+
|
|
93
|
+
const formattedSchema = key !== 'body'
|
|
94
|
+
? buildFormatterSchema(formatterSchema)
|
|
95
|
+
: schema as Schema
|
|
96
|
+
|
|
97
|
+
return [
|
|
98
|
+
...accum,
|
|
99
|
+
[key, formattedSchema],
|
|
100
|
+
]
|
|
101
|
+
}, [])
|
|
102
|
+
.map(([key, schema]): [SchemaKey, ValidateFunction] => {
|
|
103
|
+
const ajv = new Ajv({
|
|
104
|
+
allErrors: true,
|
|
105
|
+
removeAdditional: 'all',
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
addFormats(ajv)
|
|
109
|
+
|
|
110
|
+
Object
|
|
111
|
+
.entries(_customFormats ?? {})
|
|
112
|
+
.forEach(([k, v]) => ajv.addFormat(k, v))
|
|
113
|
+
|
|
114
|
+
const validator = ajv.compile(schema)
|
|
115
|
+
|
|
116
|
+
return [key, validator]
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function parseJsonBody (): Middleware {
|
|
121
|
+
return async (
|
|
122
|
+
req: Request,
|
|
123
|
+
res: unknown,
|
|
124
|
+
next: NextFn | null,
|
|
125
|
+
): Promise<unknown> => {
|
|
126
|
+
const contentType = req.headers.get('content-type')
|
|
127
|
+
|
|
128
|
+
if (!contentType) {
|
|
129
|
+
return requireNext(next)(res)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!contentType.startsWith('application/json')) {
|
|
133
|
+
throw new UnsupportedMediaTypeError('content-type')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const body = await parseBody(req)
|
|
137
|
+
|
|
138
|
+
return requireNext(next)(body)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function setValidationFormats (
|
|
143
|
+
formats: Record<string, Format>,
|
|
144
|
+
): void {
|
|
145
|
+
if (_customFormats) {
|
|
146
|
+
console.warn('setValidationFormats() - already initialized')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (_schemasCompiled) {
|
|
150
|
+
console.warn('setValidationFormats() - called after compilation')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_customFormats = formats
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* only for testing purposes */
|
|
157
|
+
|
|
158
|
+
export function resetValidationFormatsState (): void {
|
|
159
|
+
_customFormats = null
|
|
160
|
+
_schemasCompiled = false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildValidationSource (
|
|
164
|
+
req: Request,
|
|
165
|
+
res: unknown,
|
|
166
|
+
): Record<SchemaKey, unknown> {
|
|
167
|
+
return {
|
|
168
|
+
body: res,
|
|
169
|
+
headers: Object.fromEntries(req.headers),
|
|
170
|
+
params: req.params,
|
|
171
|
+
query: req.query,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function validateSchemas (
|
|
176
|
+
schemas: ValidationSchemas,
|
|
177
|
+
): Middleware {
|
|
178
|
+
const entries = compileSchemas(schemas)
|
|
179
|
+
|
|
180
|
+
_schemasCompiled = true
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
req: Request,
|
|
184
|
+
res: unknown,
|
|
185
|
+
next: NextFn | null,
|
|
186
|
+
): unknown => {
|
|
187
|
+
const source = buildValidationSource(req, res)
|
|
188
|
+
|
|
189
|
+
const errors = entries.reduce((accum: FormattedError[], [
|
|
190
|
+
key,
|
|
191
|
+
validator,
|
|
192
|
+
]) => {
|
|
193
|
+
const valid = validator(source[key])
|
|
194
|
+
|
|
195
|
+
return !valid
|
|
196
|
+
? [
|
|
197
|
+
...accum,
|
|
198
|
+
...validator.errors!.map(item => formatError(key, item)),
|
|
199
|
+
]
|
|
200
|
+
: accum
|
|
201
|
+
}, [])
|
|
202
|
+
|
|
203
|
+
if (errors.length > 0) {
|
|
204
|
+
throw new UnprocessableContentError(errors)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return requireNext(next)(res)
|
|
208
|
+
}
|
|
209
|
+
}
|