@naturalcycles/nodejs-lib 15.37.2 → 15.39.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.
@@ -1,10 +1,12 @@
1
- /* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
2
- /* eslint-disable unicorn/prefer-code-point */
3
1
  import { _lazyValue } from '@naturalcycles/js-lib'
4
- import type { Options } from 'ajv'
5
- import { Ajv } from 'ajv'
6
- import ajvFormats from 'ajv-formats'
7
- import ajvKeywords from 'ajv-keywords'
2
+ import { Set2 } from '@naturalcycles/js-lib/object'
3
+ import { _substringAfterLast } from '@naturalcycles/js-lib/string'
4
+ import { _, Ajv, type Options, type ValidateFunction } from 'ajv'
5
+ import type { JsonSchemaStringEmailOptions } from './jsonSchemaBuilder.js'
6
+ import { validTLDs } from './tlds.js'
7
+
8
+ /* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
9
+ // oxlint-disable unicorn/prefer-code-point
8
10
 
9
11
  const AJV_OPTIONS: Options = {
10
12
  removeAdditional: true,
@@ -51,101 +53,256 @@ export function createAjv(opt?: Options): Ajv {
51
53
  ...opt,
52
54
  })
53
55
 
54
- // Add custom formats
55
- addCustomAjvFormats(ajv)
56
-
57
- // todo: review and possibly cherry-pick/vendor the formats
58
- // Adds ajv "formats"
59
- // https://ajv.js.org/guide/formats.html
60
- // @ts-expect-error types are wrong
61
- ajvFormats(ajv)
62
-
63
- // https://ajv.js.org/packages/ajv-keywords.html
64
- // @ts-expect-error types are wrong
65
- ajvKeywords(ajv, [
66
- 'transform', // trim, toLowerCase, etc.
67
- 'uniqueItemProperties',
68
- 'instanceof',
69
- ])
70
-
71
56
  // Adds $merge, $patch keywords
72
57
  // https://github.com/ajv-validator/ajv-merge-patch
73
58
  // Kirill: temporarily disabled, as it creates a noise of CVE warnings
74
59
  // require('ajv-merge-patch')(ajv)
75
60
 
76
- return ajv
77
- }
61
+ ajv.addKeyword({
62
+ keyword: 'transform',
63
+ type: 'string',
64
+ modifying: true,
65
+ schemaType: 'object',
66
+ errors: true,
67
+ code(ctx) {
68
+ const { gen, data, schema, it } = ctx
69
+ const { parentData, parentDataProperty } = it
70
+
71
+ if (schema.trim) {
72
+ gen.assign(_`${data}`, _`${data}.trim()`)
73
+ }
74
+
75
+ if (schema.toLowerCase) {
76
+ gen.assign(_`${data}`, _`${data}.toLowerCase()`)
77
+ }
78
+
79
+ if (schema.toUpperCase) {
80
+ gen.assign(_`${data}`, _`${data}.toUpperCase()`)
81
+ }
82
+
83
+ if (typeof schema.truncate === 'number' && schema.truncate >= 0) {
84
+ gen.assign(_`${data}`, _`${data}.slice(0, ${schema.truncate})`)
85
+
86
+ if (schema.trim) {
87
+ gen.assign(_`${data}`, _`${data}.trim()`)
88
+ }
89
+ }
90
+
91
+ gen.if(_`${parentData} !== undefined`, () => {
92
+ gen.assign(_`${parentData}[${parentDataProperty}]`, data)
93
+ })
94
+ },
95
+ })
78
96
 
79
- const TS_2500 = 16725225600 // 2500-01-01
80
- const TS_2500_MILLIS = TS_2500 * 1000
81
- const TS_2000 = 946684800 // 2000-01-01
82
- const TS_2000_MILLIS = TS_2000 * 1000
97
+ ajv.addKeyword({
98
+ keyword: 'instanceof',
99
+ modifying: true,
100
+ schemaType: 'string',
101
+ validate(instanceOf: string, data: unknown, _schema, _ctx) {
102
+ if (typeof data !== 'object') return false
103
+ if (data === null) return false
104
+
105
+ let proto = Object.getPrototypeOf(data)
106
+ while (proto) {
107
+ if (proto.constructor?.name === instanceOf) return true
108
+ proto = Object.getPrototypeOf(proto)
109
+ }
110
+
111
+ return false
112
+ },
113
+ })
83
114
 
84
- const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
115
+ ajv.addKeyword({
116
+ keyword: 'Set2',
117
+ type: ['array', 'object'],
118
+ modifying: true,
119
+ errors: true,
120
+ schemaType: 'object',
121
+ compile(innerSchema, _parentSchema, _it) {
122
+ const validateItem: ValidateFunction = ajv.compile(innerSchema)
123
+
124
+ function validateSet(data: any, ctx: any): boolean {
125
+ let set: Set2
126
+
127
+ const isIterable = data === null || typeof data[Symbol.iterator] === 'function'
128
+
129
+ if (data instanceof Set2) {
130
+ set = data
131
+ } else if (isIterable && ctx?.parentData) {
132
+ set = new Set2(data)
133
+ } else if (isIterable && !ctx?.parentData) {
134
+ ;(validateSet as any).errors = [
135
+ {
136
+ instancePath: ctx?.instancePath ?? '',
137
+ message:
138
+ 'can only transform an Iterable into a Set2 when the schema is in an object or an array schema. This is an Ajv limitation.',
139
+ },
140
+ ]
141
+ return false
142
+ } else {
143
+ ;(validateSet as any).errors = [
144
+ {
145
+ instancePath: ctx?.instancePath ?? '',
146
+ message: 'must be a Set2 object (or optionally an Iterable in some cases)',
147
+ },
148
+ ]
149
+ return false
150
+ }
151
+
152
+ let idx = 0
153
+ for (const value of set.values()) {
154
+ if (!validateItem(value)) {
155
+ ;(validateSet as any).errors = [
156
+ {
157
+ instancePath: (ctx?.instancePath ?? '') + '/' + idx,
158
+ message: `invalid set item at index ${idx}`,
159
+ params: { errors: validateItem.errors },
160
+ },
161
+ ]
162
+ return false
163
+ }
164
+ idx++
165
+ }
166
+
167
+ if (ctx?.parentData && ctx.parentDataProperty) {
168
+ ctx.parentData[ctx.parentDataProperty] = set
169
+ }
170
+
171
+ return true
172
+ }
173
+
174
+ return validateSet
175
+ },
176
+ })
85
177
 
86
- function addCustomAjvFormats(ajv: Ajv): Ajv {
87
- return (
88
- ajv
89
- .addFormat('id', /^[a-z0-9_]{6,64}$/)
90
- .addFormat('slug', /^[a-z0-9-]+$/)
91
- .addFormat('semVer', /^[0-9]+\.[0-9]+\.[0-9]+$/)
92
- // IETF language tag (https://en.wikipedia.org/wiki/IETF_language_tag)
93
- .addFormat('languageTag', /^[a-z]{2}(-[A-Z]{2})?$/)
94
- .addFormat('countryCode', /^[A-Z]{2}$/)
95
- .addFormat('currency', /^[A-Z]{3}$/)
96
- .addFormat('unixTimestamp', {
97
- type: 'number',
98
- validate: (n: number) => {
99
- return n >= 0 && n < TS_2500
100
- },
101
- })
102
- .addFormat('unixTimestamp2000', {
103
- type: 'number',
104
- validate: (n: number) => {
105
- return n >= TS_2000 && n < TS_2500
106
- },
107
- })
108
- .addFormat('unixTimestampMillis', {
109
- type: 'number',
110
- validate: (n: number) => {
111
- return n >= 0 && n < TS_2500_MILLIS
112
- },
113
- })
114
- .addFormat('unixTimestampMillis2000', {
115
- type: 'number',
116
- validate: (n: number) => {
117
- return n >= TS_2000_MILLIS && n < TS_2500_MILLIS
178
+ ajv.addKeyword({
179
+ keyword: 'Buffer',
180
+ modifying: true,
181
+ errors: true,
182
+ schemaType: 'boolean',
183
+ compile(_innerSchema, _parentSchema, _it) {
184
+ function validateBuffer(data: any, ctx: any): boolean {
185
+ let buffer: Buffer
186
+
187
+ if (data === null) return false
188
+
189
+ const isValid =
190
+ data instanceof Buffer ||
191
+ data instanceof ArrayBuffer ||
192
+ Array.isArray(data) ||
193
+ typeof data === 'string'
194
+ if (!isValid) return false
195
+
196
+ if (data instanceof Buffer) {
197
+ buffer = data
198
+ } else if (isValid && ctx?.parentData) {
199
+ buffer = Buffer.from(data as any)
200
+ } else if (isValid && !ctx?.parentData) {
201
+ ;(validateBuffer as any).errors = [
202
+ {
203
+ instancePath: ctx?.instancePath ?? '',
204
+ message:
205
+ 'can only transform data into a Buffer when the schema is in an object or an array schema. This is an Ajv limitation.',
206
+ },
207
+ ]
208
+ return false
209
+ } else {
210
+ ;(validateBuffer as any).errors = [
211
+ {
212
+ instancePath: ctx?.instancePath ?? '',
213
+ message:
214
+ 'must be a Buffer object (or optionally an Array-like object or ArrayBuffer in some cases)',
215
+ },
216
+ ]
217
+ return false
218
+ }
219
+
220
+ if (ctx?.parentData && ctx.parentDataProperty) {
221
+ ctx.parentData[ctx.parentDataProperty] = buffer
222
+ }
223
+
224
+ return true
225
+ }
226
+
227
+ return validateBuffer
228
+ },
229
+ })
230
+
231
+ ajv.addKeyword({
232
+ keyword: 'email',
233
+ type: 'string',
234
+ modifying: false,
235
+ errors: true,
236
+ schemaType: 'object',
237
+ validate: function validate(opt: JsonSchemaStringEmailOptions, data: string, _schema, ctx) {
238
+ const { checkTLD } = opt
239
+ if (!checkTLD) return true
240
+
241
+ const tld = _substringAfterLast(data, '.')
242
+ if (validTLDs.has(tld)) return true
243
+ ;(validate as any).errors = [
244
+ {
245
+ instancePath: ctx?.instancePath ?? '',
246
+ message: `has an invalid TLD`,
118
247
  },
119
- })
120
- .addFormat('utcOffset', {
121
- type: 'number',
122
- validate: (n: number) => {
123
- // min: -14 hours
124
- // max +14 hours
125
- // multipleOf 15 (minutes)
126
- return n >= -14 * 60 && n <= 14 * 60 && Number.isInteger(n)
248
+ ]
249
+ return false
250
+ },
251
+ })
252
+
253
+ ajv.addKeyword({
254
+ keyword: 'IsoDate',
255
+ type: 'string',
256
+ modifying: false,
257
+ errors: true,
258
+ schemaType: 'boolean',
259
+ validate: function validate(_opt: true, data: string, _schema, ctx) {
260
+ const isValid = isIsoDateValid(data)
261
+ if (isValid) return true
262
+ ;(validate as any).errors = [
263
+ {
264
+ instancePath: ctx?.instancePath ?? '',
265
+ message: `is an invalid IsoDate`,
127
266
  },
128
- })
129
- .addFormat('utcOffsetHours', {
130
- type: 'number',
131
- validate: (n: number) => {
132
- // min: -14 hours
133
- // max +14 hours
134
- // multipleOf 15 (minutes)
135
- return n >= -14 && n <= 14 && Number.isInteger(n)
267
+ ]
268
+ return false
269
+ },
270
+ })
271
+
272
+ ajv.addKeyword({
273
+ keyword: 'IsoDateTime',
274
+ type: 'string',
275
+ modifying: false,
276
+ errors: true,
277
+ schemaType: 'boolean',
278
+ validate: function validate(_opt: true, data: string, _schema, ctx) {
279
+ const isValid = isIsoDateTimeValid(data)
280
+ if (isValid) return true
281
+ ;(validate as any).errors = [
282
+ {
283
+ instancePath: ctx?.instancePath ?? '',
284
+ message: `is an invalid IsoDateTime`,
136
285
  },
137
- })
138
- .addFormat('IsoDate', {
139
- type: 'string',
140
- validate: isIsoDateValid,
141
- })
142
- .addFormat('IsoDateTime', {
143
- type: 'string',
144
- validate: isIsoDateTimeValid,
145
- })
146
- )
286
+ ]
287
+ return false
288
+ },
289
+ })
290
+
291
+ ajv.addKeyword({
292
+ keyword: 'errorMessages',
293
+ schemaType: 'object',
294
+ })
295
+
296
+ ajv.addKeyword({
297
+ keyword: 'hasIsOfTypeCheck',
298
+ schemaType: 'boolean',
299
+ })
300
+
301
+ return ajv
147
302
  }
148
303
 
304
+ const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
305
+
149
306
  const DASH_CODE = '-'.charCodeAt(0)
150
307
  const ZERO_CODE = '0'.charCodeAt(0)
151
308
  const PLUS_CODE = '+'.charCodeAt(0)
@@ -3,5 +3,6 @@ import Ajv from 'ajv'
3
3
  export * from './ajvSchema.js'
4
4
  export * from './ajvValidationError.js'
5
5
  export * from './getAjv.js'
6
+ export * from './jsonSchemaBuilder.js'
6
7
 
7
8
  export { Ajv }