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.
@@ -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
+ }