@naturalcycles/nodejs-lib 15.139.0 → 15.141.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/dist/exec2/exec2.js +5 -4
- package/dist/fs/kpy.js +3 -3
- package/dist/jwt/jwt.service.js +1 -0
- package/dist/stream/progressLogger.js +3 -3
- package/dist/stream/transform/transformMap.js +3 -3
- package/dist/stream/transform/transformMapSync.js +2 -1
- package/dist/stream/transform/transformThrottle.js +1 -1
- package/dist/stream/transform/transformThrottleByRSS.js +2 -2
- package/dist/validation/ajv/getAjv.js +113 -121
- package/dist/validation/ajv/jSchema.d.ts +10 -1
- package/dist/validation/ajv/jSchema.js +19 -20
- package/dist/validation/ajv/jsonSchemaBuilder.util.js +8 -4
- package/dist/zip/zipInternal.js +1 -0
- package/dist/zip/zipWriter.js +2 -1
- package/package.json +2 -2
- package/src/exec2/exec2.ts +5 -4
- package/src/fs/kpy.ts +3 -3
- package/src/jwt/jwt.service.ts +1 -0
- package/src/stream/progressLogger.ts +3 -3
- package/src/stream/transform/transformMap.ts +3 -3
- package/src/stream/transform/transformMapSync.ts +3 -2
- package/src/stream/transform/transformThrottle.ts +1 -1
- package/src/stream/transform/transformThrottleByRSS.ts +2 -2
- package/src/stream/transform/worker/workerClassProxy.js +2 -0
- package/src/validation/ajv/getAjv.ts +128 -131
- package/src/validation/ajv/jSchema.ts +27 -21
- package/src/validation/ajv/jsonSchemaBuilder.util.ts +8 -4
- package/src/zip/zipInternal.ts +2 -0
- package/src/zip/zipWriter.ts +2 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/nodejs-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.141.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@standard-schema/spec": "^1",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"yaml": "^2"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@naturalcycles/dev-lib": "
|
|
17
|
+
"@naturalcycles/dev-lib": "0.0.0",
|
|
18
18
|
"typescript": "^7"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
package/src/exec2/exec2.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execSync, spawn, spawnSync } from 'node:child_process'
|
|
2
2
|
import type { StdioOptions } from 'node:child_process'
|
|
3
|
+
import { nowUnixMillis } from '@naturalcycles/js-lib/datetime/localTime.js'
|
|
3
4
|
import { _since } from '@naturalcycles/js-lib/datetime/time.util.js'
|
|
4
5
|
import { AppError } from '@naturalcycles/js-lib/error/error.util.js'
|
|
5
6
|
import { _substringAfterLast } from '@naturalcycles/js-lib/string/string.util.js'
|
|
@@ -69,7 +70,7 @@ class Exec2 {
|
|
|
69
70
|
const log = opt.log ?? true // by default log should be true, as we are printing the output
|
|
70
71
|
// Copy, to not mutate the input options
|
|
71
72
|
opt = { ...opt, log, logStart: opt.logStart ?? log, logFinish: opt.logFinish ?? log }
|
|
72
|
-
const started =
|
|
73
|
+
const started = nowUnixMillis()
|
|
73
74
|
this.logStart(cmd, opt)
|
|
74
75
|
|
|
75
76
|
const r = spawnSync(cmd, opt.args, {
|
|
@@ -118,7 +119,7 @@ class Exec2 {
|
|
|
118
119
|
logStart: opt.logStart ?? opt.log ?? false,
|
|
119
120
|
logFinish: opt.logFinish ?? opt.log ?? false,
|
|
120
121
|
}
|
|
121
|
-
const started =
|
|
122
|
+
const started = nowUnixMillis()
|
|
122
123
|
this.logStart(cmd, opt)
|
|
123
124
|
|
|
124
125
|
try {
|
|
@@ -177,7 +178,7 @@ class Exec2 {
|
|
|
177
178
|
const log = opt.log ?? true // by default log should be true, as we are printing the output
|
|
178
179
|
// Copy, to not mutate the input options
|
|
179
180
|
opt = { ...opt, log, logStart: opt.logStart ?? log, logFinish: opt.logFinish ?? log }
|
|
180
|
-
const started =
|
|
181
|
+
const started = nowUnixMillis()
|
|
181
182
|
this.logStart(cmd, opt)
|
|
182
183
|
|
|
183
184
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -245,7 +246,7 @@ class Exec2 {
|
|
|
245
246
|
const log = opt.log ?? printWhileRunning // by default log should be true, as we are printing the output
|
|
246
247
|
// Copy, to not mutate the input options
|
|
247
248
|
opt = { ...opt, log, logStart: opt.logStart ?? log, logFinish: opt.logFinish ?? log }
|
|
248
|
-
const started =
|
|
249
|
+
const started = nowUnixMillis()
|
|
249
250
|
this.logStart(cmd, opt)
|
|
250
251
|
let stdout = ''
|
|
251
252
|
let stderr = ''
|
package/src/fs/kpy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import { _since,
|
|
2
|
+
import { _since, nowUnixMillis } from '@naturalcycles/js-lib/datetime'
|
|
3
3
|
import type { UnixTimestampMillis } from '@naturalcycles/js-lib/types'
|
|
4
4
|
import { glob, globSync } from 'tinyglobby'
|
|
5
5
|
import { boldWhite, dimGrey, grey, yellow } from '../colors/colors.js'
|
|
@@ -45,7 +45,7 @@ export interface KpyOptions {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export async function kpy(opt: KpyOptions): Promise<void> {
|
|
48
|
-
const started =
|
|
48
|
+
const started = nowUnixMillis()
|
|
49
49
|
|
|
50
50
|
opt = kpyPrepare(opt)
|
|
51
51
|
|
|
@@ -84,7 +84,7 @@ export async function kpy(opt: KpyOptions): Promise<void> {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export function kpySync(opt: KpyOptions): void {
|
|
87
|
-
const started =
|
|
87
|
+
const started = nowUnixMillis()
|
|
88
88
|
|
|
89
89
|
opt = kpyPrepare(opt)
|
|
90
90
|
|
package/src/jwt/jwt.service.ts
CHANGED
|
@@ -263,6 +263,7 @@ export class JWTService<T extends AnyObject = AnyObject> {
|
|
|
263
263
|
const { payload } = await jwtVerify(token, key, {
|
|
264
264
|
algorithms: this.cfg.verifyAlgorithms || [this.cfg.algorithm],
|
|
265
265
|
...joseOpt,
|
|
266
|
+
// oxlint-disable-next-line no-restricted-globals -- jose expects a Date
|
|
266
267
|
currentDate: now === undefined ? undefined : new Date(now * 1000),
|
|
267
268
|
})
|
|
268
269
|
data = payload as TT
|
|
@@ -168,8 +168,8 @@ export class ProgressLogger<T> implements Disposable {
|
|
|
168
168
|
private peakRSS!: number
|
|
169
169
|
|
|
170
170
|
private start(): void {
|
|
171
|
-
this.started =
|
|
172
|
-
this.lastSecondStarted =
|
|
171
|
+
this.started = localTime.nowUnixMillis()
|
|
172
|
+
this.lastSecondStarted = localTime.nowUnixMillis()
|
|
173
173
|
this.sma = new SimpleMovingAverage(10)
|
|
174
174
|
this.processedLastSecond = 0
|
|
175
175
|
this.progress = 0
|
|
@@ -213,7 +213,7 @@ export class ProgressLogger<T> implements Disposable {
|
|
|
213
213
|
|
|
214
214
|
const mem = process.memoryUsage()
|
|
215
215
|
|
|
216
|
-
const now =
|
|
216
|
+
const now = localTime.nowUnixMillis()
|
|
217
217
|
const batchedProgress = this.progress * chunkSize
|
|
218
218
|
const lastRPS =
|
|
219
219
|
(this.processedLastSecond * chunkSize) / ((now - this.lastSecondStarted) / 1000) || 0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Transform } from 'node:stream'
|
|
2
2
|
import { _hc } from '@naturalcycles/js-lib'
|
|
3
3
|
import type { AbortableSignal } from '@naturalcycles/js-lib'
|
|
4
|
-
import { _since } from '@naturalcycles/js-lib/datetime'
|
|
4
|
+
import { _since, nowUnixMillis } from '@naturalcycles/js-lib/datetime'
|
|
5
5
|
import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error'
|
|
6
6
|
import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
|
|
7
7
|
import type { DeferredPromise } from '@naturalcycles/js-lib/promise'
|
|
@@ -141,7 +141,7 @@ export function transformMap<IN = any, OUT = IN>(
|
|
|
141
141
|
async transform(this: Transform, chunk: IN, _, cb) {
|
|
142
142
|
// Initialize start time on first item
|
|
143
143
|
if (started === 0) {
|
|
144
|
-
started =
|
|
144
|
+
started = nowUnixMillis()
|
|
145
145
|
lastWarmupCheck = started
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -262,7 +262,7 @@ export function transformMap<IN = any, OUT = IN>(
|
|
|
262
262
|
})
|
|
263
263
|
|
|
264
264
|
function updateConcurrency(): void {
|
|
265
|
-
const now =
|
|
265
|
+
const now = nowUnixMillis()
|
|
266
266
|
if (now - lastWarmupCheck < WARMUP_CHECK_INTERVAL_MS) return
|
|
267
267
|
lastWarmupCheck = now
|
|
268
268
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Transform } from 'node:stream'
|
|
2
2
|
import type { AbortableSignal } from '@naturalcycles/js-lib'
|
|
3
|
+
import { nowUnixMillis } from '@naturalcycles/js-lib/datetime/localTime.js'
|
|
3
4
|
import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error'
|
|
4
5
|
import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
|
|
5
|
-
import type { IndexedMapper, Predicate
|
|
6
|
+
import type { IndexedMapper, Predicate } from '@naturalcycles/js-lib/types'
|
|
6
7
|
import { END, SKIP } from '@naturalcycles/js-lib/types'
|
|
7
8
|
import { yellow } from '../../colors/colors.js'
|
|
8
9
|
import type { TransformOptions, TransformTyped } from '../stream.model.js'
|
|
@@ -78,7 +79,7 @@ export function transformMapSync<IN = any, OUT = IN>(
|
|
|
78
79
|
signal,
|
|
79
80
|
} = opt
|
|
80
81
|
|
|
81
|
-
const started =
|
|
82
|
+
const started = nowUnixMillis()
|
|
82
83
|
let index = -1
|
|
83
84
|
let countOut = 0
|
|
84
85
|
let isSettled = false
|
|
@@ -73,7 +73,7 @@ export function transformThrottle<T>(opt: TransformThrottleOptions): TransformTy
|
|
|
73
73
|
// console.log('pausing now after', {item, count})
|
|
74
74
|
lock = pDefer()
|
|
75
75
|
logger.log(
|
|
76
|
-
`${localTime.now().toPretty()} transformThrottle activated: ${count} items passed in ${_since(start)}, will pause for ${_ms(interval * 1000 - (
|
|
76
|
+
`${localTime.now().toPretty()} transformThrottle activated: ${count} items passed in ${_since(start)}, will pause for ${_ms(interval * 1000 - (localTime.nowUnixMillis() - start))}`,
|
|
77
77
|
)
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -87,7 +87,7 @@ export function transformThrottleByRSS<T>(
|
|
|
87
87
|
|
|
88
88
|
if (!disabled && lastRSS > maxRSSBytes && !lock) {
|
|
89
89
|
lock = pDefer()
|
|
90
|
-
pausedSince =
|
|
90
|
+
pausedSince = localTime.nowUnixMillis()
|
|
91
91
|
logger.log(
|
|
92
92
|
`${localTime.now().toPretty()} transformThrottleByRSS paused: RSS ${_mb(lastRSS)} > ${maxRSS} MB`,
|
|
93
93
|
)
|
|
@@ -122,7 +122,7 @@ export function transformThrottleByRSS<T>(
|
|
|
122
122
|
)
|
|
123
123
|
lock!.resolve()
|
|
124
124
|
lock = undefined
|
|
125
|
-
} else if (pollTimeout &&
|
|
125
|
+
} else if (pollTimeout && localTime.nowUnixMillis() - pausedSince >= pollTimeout) {
|
|
126
126
|
clearTimeout(rssCheckTimer)
|
|
127
127
|
if (onPollTimeout === 'throw') {
|
|
128
128
|
lock!.reject(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// oxlint-disable-next-line no-restricted-globals -- the worker entry stays dependency-free
|
|
1
2
|
const started = Date.now()
|
|
2
3
|
import { inspect } from 'node:util'
|
|
3
4
|
import { workerData, parentPort } from 'node:worker_threads'
|
|
@@ -20,6 +21,7 @@ try {
|
|
|
20
21
|
const { WorkerClass } = await import(workerFile)
|
|
21
22
|
const worker = new WorkerClass(workerData)
|
|
22
23
|
|
|
24
|
+
// oxlint-disable-next-line no-restricted-globals -- the worker entry stays dependency-free
|
|
23
25
|
log(`${metric}#${workerIndex} loaded in ${Date.now() - started} ms`)
|
|
24
26
|
|
|
25
27
|
let errors = 0
|
|
@@ -3,15 +3,14 @@ import { _assert } from '@naturalcycles/js-lib/error'
|
|
|
3
3
|
import { _deepCopy, _mapObject, Set2 } from '@naturalcycles/js-lib/object'
|
|
4
4
|
import { _substringAfterLast } from '@naturalcycles/js-lib/string'
|
|
5
5
|
import type { AnyObject } from '@naturalcycles/js-lib/types'
|
|
6
|
-
import type { Options, ValidateFunction } from 'ajv/dist/2020.js'
|
|
7
|
-
import { Ajv2020 } from 'ajv/dist/2020.js'
|
|
6
|
+
import type { Code, KeywordCxt, Options, ValidateFunction } from 'ajv/dist/2020.js'
|
|
7
|
+
import { _, Ajv2020, str } from 'ajv/dist/2020.js'
|
|
8
8
|
import { validTLDs } from '../tlds.js'
|
|
9
9
|
import type {
|
|
10
10
|
CustomConverterFn,
|
|
11
11
|
CustomValidatorFn,
|
|
12
12
|
JSchema,
|
|
13
13
|
JsonSchemaIsoDateOptions,
|
|
14
|
-
JsonSchemaIsoMonthOptions,
|
|
15
14
|
JsonSchemaStringEmailOptions,
|
|
16
15
|
JsonSchemaUtcOffsetOptions,
|
|
17
16
|
} from './jSchema.js'
|
|
@@ -310,140 +309,79 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
310
309
|
},
|
|
311
310
|
})
|
|
312
311
|
|
|
312
|
+
// The date/time keywords are `code` keywords, which Ajv inlines into the compiled validator.
|
|
313
|
+
// A `validate` keyword is a function call with a context object, whose instancePath Ajv builds
|
|
314
|
+
// on every call (escaping the key of every map entry), even for valid data.
|
|
313
315
|
ajv.addKeyword({
|
|
314
316
|
keyword: 'IsoDate',
|
|
315
317
|
type: 'string',
|
|
316
|
-
modifying: false,
|
|
317
|
-
errors: true,
|
|
318
318
|
schemaType: 'object',
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (sameOrBefore) {
|
|
352
|
-
const isParamValid = isIsoDateValid(sameOrBefore)
|
|
353
|
-
const isRuleValid = isParamValid && sameOrBefore >= data
|
|
354
|
-
|
|
355
|
-
if (!isRuleValid) {
|
|
356
|
-
errors.push({
|
|
357
|
-
instancePath: ctx?.instancePath ?? '',
|
|
358
|
-
message: `is not the same or before ${sameOrBefore}`,
|
|
359
|
-
})
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (after) {
|
|
364
|
-
const isParamValid = isIsoDateValid(after)
|
|
365
|
-
const isRuleValid = isParamValid && after < data
|
|
366
|
-
|
|
367
|
-
if (!isRuleValid) {
|
|
368
|
-
errors.push({
|
|
369
|
-
instancePath: ctx?.instancePath ?? '',
|
|
370
|
-
message: `is not after ${after}`,
|
|
371
|
-
})
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (sameOrAfter) {
|
|
376
|
-
const isParamValid = isIsoDateValid(sameOrAfter)
|
|
377
|
-
const isRuleValid = isParamValid && sameOrAfter <= data
|
|
378
|
-
|
|
379
|
-
if (!isRuleValid) {
|
|
380
|
-
errors.push({
|
|
381
|
-
instancePath: ctx?.instancePath ?? '',
|
|
382
|
-
message: `is not the same or after ${sameOrAfter}`,
|
|
383
|
-
})
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (errors.length === 0) return true
|
|
388
|
-
;(validate as any).errors = errors
|
|
389
|
-
return false
|
|
319
|
+
error: {
|
|
320
|
+
message: ({ params }) =>
|
|
321
|
+
params['rule']
|
|
322
|
+
? str`is not ${isoDateRuleLabels[params['rule'] as IsoDateRule]} ${params['limit']}`
|
|
323
|
+
: str`is an invalid IsoDate`,
|
|
324
|
+
params: ({ params }) => (params['rule'] ? _`{${params['rule']}: ${params['limit']}}` : _`{}`),
|
|
325
|
+
},
|
|
326
|
+
code(cxt: KeywordCxt) {
|
|
327
|
+
const { gen, data } = cxt
|
|
328
|
+
const opt: JsonSchemaIsoDateOptions = cxt.schema
|
|
329
|
+
const isValid = gen.scopeValue('func', { ref: isIsoDateValid })
|
|
330
|
+
|
|
331
|
+
gen.if(
|
|
332
|
+
_`!${isValid}(${data})`,
|
|
333
|
+
() => cxt.error(),
|
|
334
|
+
() => {
|
|
335
|
+
for (const rule of isoDateRules) {
|
|
336
|
+
const limit = opt[rule]
|
|
337
|
+
if (!limit) continue
|
|
338
|
+
|
|
339
|
+
const params = { rule, limit }
|
|
340
|
+
if (!isIsoDateValid(limit)) {
|
|
341
|
+
// An invalid limit rejects every date
|
|
342
|
+
cxt.error(false, params)
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
gen.if(isoDateRuleFails(rule, data, limit), () => cxt.error(false, params))
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
)
|
|
390
349
|
},
|
|
391
350
|
})
|
|
392
351
|
|
|
393
352
|
ajv.addKeyword({
|
|
394
353
|
keyword: 'IsoDateTime',
|
|
395
354
|
type: 'string',
|
|
396
|
-
modifying: false,
|
|
397
|
-
errors: true,
|
|
398
355
|
schemaType: 'boolean',
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
{
|
|
404
|
-
instancePath: ctx?.instancePath ?? '',
|
|
405
|
-
message: `is an invalid IsoDateTime`,
|
|
406
|
-
},
|
|
407
|
-
]
|
|
408
|
-
return false
|
|
356
|
+
error: { message: 'is an invalid IsoDateTime' },
|
|
357
|
+
code(cxt: KeywordCxt) {
|
|
358
|
+
const isValid = cxt.gen.scopeValue('func', { ref: isIsoDateTimeValid })
|
|
359
|
+
cxt.fail(_`!${isValid}(${cxt.data})`)
|
|
409
360
|
},
|
|
410
361
|
})
|
|
411
362
|
|
|
412
363
|
ajv.addKeyword({
|
|
413
364
|
keyword: 'IsoMonth',
|
|
414
365
|
type: 'string',
|
|
415
|
-
modifying: false,
|
|
416
|
-
errors: true,
|
|
417
366
|
schemaType: 'object',
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
{
|
|
423
|
-
instancePath: ctx?.instancePath ?? '',
|
|
424
|
-
message: `is an invalid IsoMonth`,
|
|
425
|
-
},
|
|
426
|
-
]
|
|
427
|
-
return false
|
|
367
|
+
error: { message: 'is an invalid IsoMonth' },
|
|
368
|
+
code(cxt: KeywordCxt) {
|
|
369
|
+
const isValid = cxt.gen.scopeValue('func', { ref: isIsoMonthValid })
|
|
370
|
+
cxt.fail(_`!${isValid}(${cxt.data})`)
|
|
428
371
|
},
|
|
429
372
|
})
|
|
430
373
|
|
|
431
374
|
ajv.addKeyword({
|
|
432
375
|
keyword: 'UtcOffset',
|
|
433
376
|
type: 'string',
|
|
434
|
-
modifying: false,
|
|
435
|
-
errors: true,
|
|
436
377
|
schemaType: 'object',
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
},
|
|
445
|
-
]
|
|
446
|
-
return false
|
|
378
|
+
error: { message: 'is an invalid UtcOffset' },
|
|
379
|
+
code(cxt: KeywordCxt) {
|
|
380
|
+
const opt: JsonSchemaUtcOffsetOptions = cxt.schema
|
|
381
|
+
const isValid = cxt.gen.scopeValue('func', {
|
|
382
|
+
ref: opt.allowZ ? isUtcOffsetOrZValid : isUtcOffsetValid,
|
|
383
|
+
})
|
|
384
|
+
cxt.fail(_`!${isValid}(${cxt.data})`)
|
|
447
385
|
},
|
|
448
386
|
})
|
|
449
387
|
|
|
@@ -496,39 +434,69 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
496
434
|
},
|
|
497
435
|
})
|
|
498
436
|
|
|
437
|
+
// Strips the keys that don't match the key schema; the values are validated by `additionalProperties`.
|
|
438
|
+
// The key schema is inlined per key with error reporting off, and the error count is reset after
|
|
439
|
+
// a mismatch, so a stripped key never surfaces as an error. Keys listed in `properties` are skipped.
|
|
499
440
|
ajv.addKeyword({
|
|
500
441
|
keyword: 'keySchema',
|
|
501
442
|
type: 'object',
|
|
502
|
-
modifying: true,
|
|
503
|
-
errors: false,
|
|
504
443
|
schemaType: 'object',
|
|
505
|
-
|
|
506
|
-
|
|
444
|
+
trackErrors: true,
|
|
445
|
+
code(cxt: KeywordCxt) {
|
|
446
|
+
const { gen, data, parentSchema } = cxt
|
|
447
|
+
const propertyNames = Object.keys(parentSchema['properties'] || {})
|
|
448
|
+
const valid = gen.name('valid')
|
|
449
|
+
|
|
450
|
+
gen.forIn('key', data, key => {
|
|
451
|
+
const checkKey = (): void => {
|
|
452
|
+
cxt.subschema(
|
|
453
|
+
{
|
|
454
|
+
keyword: 'keySchema',
|
|
455
|
+
data: key,
|
|
456
|
+
dataTypes: ['string'],
|
|
457
|
+
compositeRule: true,
|
|
458
|
+
createErrors: false,
|
|
459
|
+
allErrors: false,
|
|
460
|
+
},
|
|
461
|
+
valid,
|
|
462
|
+
)
|
|
463
|
+
gen.if(_`!${valid}`, () => {
|
|
464
|
+
cxt.reset()
|
|
465
|
+
gen.code(_`delete ${data}[${key}]`)
|
|
466
|
+
})
|
|
467
|
+
}
|
|
507
468
|
|
|
508
|
-
|
|
509
|
-
if (typeof data !== 'object' || data === null) return true
|
|
469
|
+
if (!propertyNames.length) return checkKey()
|
|
510
470
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
471
|
+
let isNotProperty: Code = _`${key} !== ${propertyNames[0]}`
|
|
472
|
+
for (const name of propertyNames.slice(1)) {
|
|
473
|
+
isNotProperty = _`${isNotProperty} && ${key} !== ${name}`
|
|
515
474
|
}
|
|
475
|
+
gen.if(isNotProperty, checkKey)
|
|
476
|
+
})
|
|
477
|
+
},
|
|
478
|
+
})
|
|
516
479
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
480
|
+
// Validates the inner schema only when the value is defined. Used for the optional values of
|
|
481
|
+
// `stringMap`/`record`, whose `additionalProperties` loop visits the keys with undefined values too.
|
|
482
|
+
ajv.addKeyword({
|
|
483
|
+
keyword: 'ifDefined',
|
|
484
|
+
schemaType: ['object', 'boolean'],
|
|
485
|
+
code(cxt: KeywordCxt) {
|
|
486
|
+
const { gen, data } = cxt
|
|
487
|
+
gen.if(_`${data} !== undefined`, () => {
|
|
488
|
+
cxt.subschema({ keyword: 'ifDefined' }, gen.name('valid'))
|
|
489
|
+
})
|
|
521
490
|
},
|
|
522
491
|
})
|
|
523
492
|
|
|
524
|
-
// Validates that the value is undefined.
|
|
525
|
-
// to allow undefined values in patternProperties via anyOf.
|
|
493
|
+
// Validates that the value is undefined. Superseded by `ifDefined`, kept for raw JSON schemas.
|
|
526
494
|
ajv.addKeyword({
|
|
527
495
|
keyword: 'isUndefined',
|
|
528
|
-
modifying: false,
|
|
529
|
-
errors: false,
|
|
530
496
|
schemaType: 'boolean',
|
|
531
|
-
|
|
497
|
+
code(cxt: KeywordCxt) {
|
|
498
|
+
cxt.fail(_`${cxt.data} !== undefined`)
|
|
499
|
+
},
|
|
532
500
|
})
|
|
533
501
|
|
|
534
502
|
// This is added because Ajv validates the `min/maxProperties` before validating the properties.
|
|
@@ -777,6 +745,27 @@ export function createAjv(opt?: Options): Ajv2020 {
|
|
|
777
745
|
return ajv
|
|
778
746
|
}
|
|
779
747
|
|
|
748
|
+
type IsoDateRule = keyof JsonSchemaIsoDateOptions
|
|
749
|
+
|
|
750
|
+
const isoDateRules: IsoDateRule[] = ['before', 'sameOrBefore', 'after', 'sameOrAfter']
|
|
751
|
+
|
|
752
|
+
const isoDateRuleLabels: Record<IsoDateRule, string> = {
|
|
753
|
+
before: 'before',
|
|
754
|
+
sameOrBefore: 'the same or before',
|
|
755
|
+
after: 'after',
|
|
756
|
+
sameOrAfter: 'the same or after',
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* IsoDate strings compare chronologically as plain strings.
|
|
761
|
+
*/
|
|
762
|
+
function isoDateRuleFails(rule: IsoDateRule, data: Code, limit: string): Code {
|
|
763
|
+
if (rule === 'before') return _`${data} >= ${limit}`
|
|
764
|
+
if (rule === 'sameOrBefore') return _`${data} > ${limit}`
|
|
765
|
+
if (rule === 'after') return _`${data} <= ${limit}`
|
|
766
|
+
return _`${data} < ${limit}`
|
|
767
|
+
}
|
|
768
|
+
|
|
780
769
|
const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
781
770
|
|
|
782
771
|
const DASH_CODE = '-'.charCodeAt(0)
|
|
@@ -904,6 +893,14 @@ function isIsoTimezoneValid(s: string): boolean {
|
|
|
904
893
|
return true
|
|
905
894
|
}
|
|
906
895
|
|
|
896
|
+
function isUtcOffsetValid(s: string): boolean {
|
|
897
|
+
return s !== 'Z' && s !== '' && isIsoTimezoneValid(s)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function isUtcOffsetOrZValid(s: string): boolean {
|
|
901
|
+
return s === 'Z' || (s !== '' && isIsoTimezoneValid(s))
|
|
902
|
+
}
|
|
903
|
+
|
|
907
904
|
/**
|
|
908
905
|
* This is a performance optimized correct validation
|
|
909
906
|
* for ISO month formatted as "YYYY-MM".
|