@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.
@@ -1,4 +1,5 @@
1
1
  import { execSync, spawn, spawnSync } from 'node:child_process';
2
+ import { nowUnixMillis } from '@naturalcycles/js-lib/datetime/localTime.js';
2
3
  import { _since } from '@naturalcycles/js-lib/datetime/time.util.js';
3
4
  import { AppError } from '@naturalcycles/js-lib/error/error.util.js';
4
5
  import { _substringAfterLast } from '@naturalcycles/js-lib/string/string.util.js';
@@ -53,7 +54,7 @@ class Exec2 {
53
54
  const log = opt.log ?? true; // by default log should be true, as we are printing the output
54
55
  // Copy, to not mutate the input options
55
56
  opt = { ...opt, log, logStart: opt.logStart ?? log, logFinish: opt.logFinish ?? log };
56
- const started = Date.now();
57
+ const started = nowUnixMillis();
57
58
  this.logStart(cmd, opt);
58
59
  const r = spawnSync(cmd, opt.args, {
59
60
  encoding: 'utf8',
@@ -98,7 +99,7 @@ class Exec2 {
98
99
  logStart: opt.logStart ?? opt.log ?? false,
99
100
  logFinish: opt.logFinish ?? opt.log ?? false,
100
101
  };
101
- const started = Date.now();
102
+ const started = nowUnixMillis();
102
103
  this.logStart(cmd, opt);
103
104
  try {
104
105
  const s = execSync(cmd, {
@@ -148,7 +149,7 @@ class Exec2 {
148
149
  const log = opt.log ?? true; // by default log should be true, as we are printing the output
149
150
  // Copy, to not mutate the input options
150
151
  opt = { ...opt, log, logStart: opt.logStart ?? log, logFinish: opt.logFinish ?? log };
151
- const started = Date.now();
152
+ const started = nowUnixMillis();
152
153
  this.logStart(cmd, opt);
153
154
  await new Promise((resolve, reject) => {
154
155
  const p = spawn(cmd, opt.args || [], {
@@ -203,7 +204,7 @@ class Exec2 {
203
204
  const log = opt.log ?? printWhileRunning; // by default log should be true, as we are printing the output
204
205
  // Copy, to not mutate the input options
205
206
  opt = { ...opt, log, logStart: opt.logStart ?? log, logFinish: opt.logFinish ?? log };
206
- const started = Date.now();
207
+ const started = nowUnixMillis();
207
208
  this.logStart(cmd, opt);
208
209
  let stdout = '';
209
210
  let stderr = '';
package/dist/fs/kpy.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
- import { _since, localTime } from '@naturalcycles/js-lib/datetime';
2
+ import { _since, nowUnixMillis } from '@naturalcycles/js-lib/datetime';
3
3
  import { glob, globSync } from 'tinyglobby';
4
4
  import { boldWhite, dimGrey, grey, yellow } from '../colors/colors.js';
5
5
  import { fs2 } from './fs2.js';
6
6
  export async function kpy(opt) {
7
- const started = localTime.nowUnixMillis();
7
+ const started = nowUnixMillis();
8
8
  opt = kpyPrepare(opt);
9
9
  const filenames = await glob(opt.inputPatterns, {
10
10
  cwd: opt.baseDir,
@@ -33,7 +33,7 @@ export async function kpy(opt) {
33
33
  kpyLogResult(opt, filenames, started);
34
34
  }
35
35
  export function kpySync(opt) {
36
- const started = localTime.nowUnixMillis();
36
+ const started = nowUnixMillis();
37
37
  opt = kpyPrepare(opt);
38
38
  const filenames = globSync(opt.inputPatterns, {
39
39
  cwd: opt.baseDir,
@@ -78,6 +78,7 @@ export class JWTService {
78
78
  const { payload } = await jwtVerify(token, key, {
79
79
  algorithms: this.cfg.verifyAlgorithms || [this.cfg.algorithm],
80
80
  ...joseOpt,
81
+ // oxlint-disable-next-line no-restricted-globals -- jose expects a Date
81
82
  currentDate: now === undefined ? undefined : new Date(now * 1000),
82
83
  });
83
84
  data = payload;
@@ -35,8 +35,8 @@ export class ProgressLogger {
35
35
  progress;
36
36
  peakRSS;
37
37
  start() {
38
- this.started = Date.now();
39
- this.lastSecondStarted = Date.now();
38
+ this.started = localTime.nowUnixMillis();
39
+ this.lastSecondStarted = localTime.nowUnixMillis();
40
40
  this.sma = new SimpleMovingAverage(10);
41
41
  this.processedLastSecond = 0;
42
42
  this.progress = 0;
@@ -60,7 +60,7 @@ export class ProgressLogger {
60
60
  return;
61
61
  const { metric, extra, chunkSize, heapUsed: logHeapUsed, heapTotal: logHeapTotal, rss: logRss, peakRSS: logPeakRss, rssMinusHeap, external, arrayBuffers, logRPS, logger, } = this.cfg;
62
62
  const mem = process.memoryUsage();
63
- const now = Date.now();
63
+ const now = localTime.nowUnixMillis();
64
64
  const batchedProgress = this.progress * chunkSize;
65
65
  const lastRPS = (this.processedLastSecond * chunkSize) / ((now - this.lastSecondStarted) / 1000) || 0;
66
66
  const rpsTotal = Math.round(batchedProgress / ((now - this.started) / 1000)) || 0;
@@ -1,6 +1,6 @@
1
1
  import { Transform } from 'node:stream';
2
2
  import { _hc } from '@naturalcycles/js-lib';
3
- import { _since } from '@naturalcycles/js-lib/datetime';
3
+ import { _since, nowUnixMillis } from '@naturalcycles/js-lib/datetime';
4
4
  import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error';
5
5
  import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
6
6
  import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
@@ -43,7 +43,7 @@ export function transformMap(mapper, opt = {}) {
43
43
  async transform(chunk, _, cb) {
44
44
  // Initialize start time on first item
45
45
  if (started === 0) {
46
- started = Date.now();
46
+ started = nowUnixMillis();
47
47
  lastWarmupCheck = started;
48
48
  }
49
49
  if (isSettled)
@@ -150,7 +150,7 @@ export function transformMap(mapper, opt = {}) {
150
150
  },
151
151
  });
152
152
  function updateConcurrency() {
153
- const now = Date.now();
153
+ const now = nowUnixMillis();
154
154
  if (now - lastWarmupCheck < WARMUP_CHECK_INTERVAL_MS)
155
155
  return;
156
156
  lastWarmupCheck = now;
@@ -1,4 +1,5 @@
1
1
  import { Transform } from 'node:stream';
2
+ import { nowUnixMillis } from '@naturalcycles/js-lib/datetime/localTime.js';
2
3
  import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error';
3
4
  import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
4
5
  import { END, SKIP } from '@naturalcycles/js-lib/types';
@@ -11,7 +12,7 @@ import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js';
11
12
  export function transformMapSync(mapper, opt = {}) {
12
13
  const { predicate, // defaults to "no predicate" (pass everything)
13
14
  errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', objectMode = true, signal, } = opt;
14
- const started = Date.now();
15
+ const started = nowUnixMillis();
15
16
  let index = -1;
16
17
  let countOut = 0;
17
18
  let isSettled = false;
@@ -48,7 +48,7 @@ export function transformThrottle(opt) {
48
48
  if (++count >= throughput) {
49
49
  // console.log('pausing now after', {item, count})
50
50
  lock = pDefer();
51
- logger.log(`${localTime.now().toPretty()} transformThrottle activated: ${count} items passed in ${_since(start)}, will pause for ${_ms(interval * 1000 - (Date.now() - start))}`);
51
+ logger.log(`${localTime.now().toPretty()} transformThrottle activated: ${count} items passed in ${_since(start)}, will pause for ${_ms(interval * 1000 - (localTime.nowUnixMillis() - start))}`);
52
52
  }
53
53
  cb(null, item); // pass the item through
54
54
  },
@@ -39,7 +39,7 @@ export function transformThrottleByRSS(opt) {
39
39
  }
40
40
  if (!disabled && lastRSS > maxRSSBytes && !lock) {
41
41
  lock = pDefer();
42
- pausedSince = Date.now();
42
+ pausedSince = localTime.nowUnixMillis();
43
43
  logger.log(`${localTime.now().toPretty()} transformThrottleByRSS paused: RSS ${_mb(lastRSS)} > ${maxRSS} MB`);
44
44
  pollTimer = setTimeout(() => pollRSS(), pollInterval);
45
45
  }
@@ -67,7 +67,7 @@ export function transformThrottleByRSS(opt) {
67
67
  lock.resolve();
68
68
  lock = undefined;
69
69
  }
70
- else if (pollTimeout && Date.now() - pausedSince >= pollTimeout) {
70
+ else if (pollTimeout && localTime.nowUnixMillis() - pausedSince >= pollTimeout) {
71
71
  clearTimeout(rssCheckTimer);
72
72
  if (onPollTimeout === 'throw') {
73
73
  lock.reject(new Error(`transformThrottleByRSS pollTimeout of ${_ms(pollTimeout)} reached, RSS ${_mb(rss)} still > ${maxRSS} MB`));
@@ -2,7 +2,7 @@ import { _isBetween, _lazyValue, _round } from '@naturalcycles/js-lib';
2
2
  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
- import { Ajv2020 } from 'ajv/dist/2020.js';
5
+ import { _, Ajv2020, str } from 'ajv/dist/2020.js';
6
6
  import { validTLDs } from '../tlds.js';
7
7
  import { jFormats } from './jsonSchemaBuilder.util.js';
8
8
  // oxlint-disable unicorn/prefer-code-point
@@ -265,130 +265,70 @@ export function createAjv(opt) {
265
265
  return true;
266
266
  },
267
267
  });
268
+ // The date/time keywords are `code` keywords, which Ajv inlines into the compiled validator.
269
+ // A `validate` keyword is a function call with a context object, whose instancePath Ajv builds
270
+ // on every call (escaping the key of every map entry), even for valid data.
268
271
  ajv.addKeyword({
269
272
  keyword: 'IsoDate',
270
273
  type: 'string',
271
- modifying: false,
272
- errors: true,
273
274
  schemaType: 'object',
274
- validate: function validate(opt, data, _schema, ctx) {
275
- const hasOptions = Object.keys(opt).length > 0;
276
- const isValid = isIsoDateValid(data);
277
- if (!isValid) {
278
- ;
279
- validate.errors = [
280
- {
281
- instancePath: ctx?.instancePath ?? '',
282
- message: `is an invalid IsoDate`,
283
- },
284
- ];
285
- return false;
286
- }
287
- if (!hasOptions)
288
- return true;
289
- const { before, sameOrBefore, after, sameOrAfter } = opt;
290
- const errors = [];
291
- if (before) {
292
- const isParamValid = isIsoDateValid(before);
293
- const isRuleValid = isParamValid && before > data;
294
- if (!isRuleValid) {
295
- errors.push({
296
- instancePath: ctx?.instancePath ?? '',
297
- message: `is not before ${before}`,
298
- });
299
- }
300
- }
301
- if (sameOrBefore) {
302
- const isParamValid = isIsoDateValid(sameOrBefore);
303
- const isRuleValid = isParamValid && sameOrBefore >= data;
304
- if (!isRuleValid) {
305
- errors.push({
306
- instancePath: ctx?.instancePath ?? '',
307
- message: `is not the same or before ${sameOrBefore}`,
308
- });
309
- }
310
- }
311
- if (after) {
312
- const isParamValid = isIsoDateValid(after);
313
- const isRuleValid = isParamValid && after < data;
314
- if (!isRuleValid) {
315
- errors.push({
316
- instancePath: ctx?.instancePath ?? '',
317
- message: `is not after ${after}`,
318
- });
319
- }
320
- }
321
- if (sameOrAfter) {
322
- const isParamValid = isIsoDateValid(sameOrAfter);
323
- const isRuleValid = isParamValid && sameOrAfter <= data;
324
- if (!isRuleValid) {
325
- errors.push({
326
- instancePath: ctx?.instancePath ?? '',
327
- message: `is not the same or after ${sameOrAfter}`,
328
- });
275
+ error: {
276
+ message: ({ params }) => params['rule']
277
+ ? str `is not ${isoDateRuleLabels[params['rule']]} ${params['limit']}`
278
+ : str `is an invalid IsoDate`,
279
+ params: ({ params }) => (params['rule'] ? _ `{${params['rule']}: ${params['limit']}}` : _ `{}`),
280
+ },
281
+ code(cxt) {
282
+ const { gen, data } = cxt;
283
+ const opt = cxt.schema;
284
+ const isValid = gen.scopeValue('func', { ref: isIsoDateValid });
285
+ gen.if(_ `!${isValid}(${data})`, () => cxt.error(), () => {
286
+ for (const rule of isoDateRules) {
287
+ const limit = opt[rule];
288
+ if (!limit)
289
+ continue;
290
+ const params = { rule, limit };
291
+ if (!isIsoDateValid(limit)) {
292
+ // An invalid limit rejects every date
293
+ cxt.error(false, params);
294
+ continue;
295
+ }
296
+ gen.if(isoDateRuleFails(rule, data, limit), () => cxt.error(false, params));
329
297
  }
330
- }
331
- if (errors.length === 0)
332
- return true;
333
- validate.errors = errors;
334
- return false;
298
+ });
335
299
  },
336
300
  });
337
301
  ajv.addKeyword({
338
302
  keyword: 'IsoDateTime',
339
303
  type: 'string',
340
- modifying: false,
341
- errors: true,
342
304
  schemaType: 'boolean',
343
- validate: function validate(_opt, data, _schema, ctx) {
344
- const isValid = isIsoDateTimeValid(data);
345
- if (isValid)
346
- return true;
347
- validate.errors = [
348
- {
349
- instancePath: ctx?.instancePath ?? '',
350
- message: `is an invalid IsoDateTime`,
351
- },
352
- ];
353
- return false;
305
+ error: { message: 'is an invalid IsoDateTime' },
306
+ code(cxt) {
307
+ const isValid = cxt.gen.scopeValue('func', { ref: isIsoDateTimeValid });
308
+ cxt.fail(_ `!${isValid}(${cxt.data})`);
354
309
  },
355
310
  });
356
311
  ajv.addKeyword({
357
312
  keyword: 'IsoMonth',
358
313
  type: 'string',
359
- modifying: false,
360
- errors: true,
361
314
  schemaType: 'object',
362
- validate: function validate(_opt, data, _schema, ctx) {
363
- const isValid = isIsoMonthValid(data);
364
- if (isValid)
365
- return true;
366
- validate.errors = [
367
- {
368
- instancePath: ctx?.instancePath ?? '',
369
- message: `is an invalid IsoMonth`,
370
- },
371
- ];
372
- return false;
315
+ error: { message: 'is an invalid IsoMonth' },
316
+ code(cxt) {
317
+ const isValid = cxt.gen.scopeValue('func', { ref: isIsoMonthValid });
318
+ cxt.fail(_ `!${isValid}(${cxt.data})`);
373
319
  },
374
320
  });
375
321
  ajv.addKeyword({
376
322
  keyword: 'UtcOffset',
377
323
  type: 'string',
378
- modifying: false,
379
- errors: true,
380
324
  schemaType: 'object',
381
- validate: function validate(opt, data, _schema, ctx) {
382
- const isValid = data === 'Z' ? opt.allowZ : data !== '' && isIsoTimezoneValid(data);
383
- if (isValid)
384
- return true;
385
- validate.errors = [
386
- {
387
- instancePath: ctx?.instancePath ?? '',
388
- message: `is an invalid UtcOffset`,
389
- },
390
- ];
391
- return false;
325
+ error: { message: 'is an invalid UtcOffset' },
326
+ code(cxt) {
327
+ const opt = cxt.schema;
328
+ const isValid = cxt.gen.scopeValue('func', {
329
+ ref: opt.allowZ ? isUtcOffsetOrZValid : isUtcOffsetValid,
330
+ });
331
+ cxt.fail(_ `!${isValid}(${cxt.data})`);
392
332
  },
393
333
  });
394
334
  // Annotation only (the values are validated by `enum`), registered so strict mode doesn't reject it
@@ -425,35 +365,62 @@ export function createAjv(opt) {
425
365
  return true;
426
366
  },
427
367
  });
368
+ // Strips the keys that don't match the key schema; the values are validated by `additionalProperties`.
369
+ // The key schema is inlined per key with error reporting off, and the error count is reset after
370
+ // a mismatch, so a stripped key never surfaces as an error. Keys listed in `properties` are skipped.
428
371
  ajv.addKeyword({
429
372
  keyword: 'keySchema',
430
373
  type: 'object',
431
- modifying: true,
432
- errors: false,
433
374
  schemaType: 'object',
434
- compile(innerSchema, _parentSchema, _it) {
435
- const isValidKeyFn = ajv.compile(innerSchema);
436
- function validate(data, _ctx) {
437
- if (typeof data !== 'object' || data === null)
438
- return true;
439
- for (const key of Object.keys(data)) {
440
- if (!isValidKeyFn(key)) {
441
- delete data[key];
442
- }
375
+ trackErrors: true,
376
+ code(cxt) {
377
+ const { gen, data, parentSchema } = cxt;
378
+ const propertyNames = Object.keys(parentSchema['properties'] || {});
379
+ const valid = gen.name('valid');
380
+ gen.forIn('key', data, key => {
381
+ const checkKey = () => {
382
+ cxt.subschema({
383
+ keyword: 'keySchema',
384
+ data: key,
385
+ dataTypes: ['string'],
386
+ compositeRule: true,
387
+ createErrors: false,
388
+ allErrors: false,
389
+ }, valid);
390
+ gen.if(_ `!${valid}`, () => {
391
+ cxt.reset();
392
+ gen.code(_ `delete ${data}[${key}]`);
393
+ });
394
+ };
395
+ if (!propertyNames.length)
396
+ return checkKey();
397
+ let isNotProperty = _ `${key} !== ${propertyNames[0]}`;
398
+ for (const name of propertyNames.slice(1)) {
399
+ isNotProperty = _ `${isNotProperty} && ${key} !== ${name}`;
443
400
  }
444
- return true;
445
- }
446
- return validate;
401
+ gen.if(isNotProperty, checkKey);
402
+ });
447
403
  },
448
404
  });
449
- // Validates that the value is undefined. Used in record/stringMap with optional value schemas
450
- // to allow undefined values in patternProperties via anyOf.
405
+ // Validates the inner schema only when the value is defined. Used for the optional values of
406
+ // `stringMap`/`record`, whose `additionalProperties` loop visits the keys with undefined values too.
407
+ ajv.addKeyword({
408
+ keyword: 'ifDefined',
409
+ schemaType: ['object', 'boolean'],
410
+ code(cxt) {
411
+ const { gen, data } = cxt;
412
+ gen.if(_ `${data} !== undefined`, () => {
413
+ cxt.subschema({ keyword: 'ifDefined' }, gen.name('valid'));
414
+ });
415
+ },
416
+ });
417
+ // Validates that the value is undefined. Superseded by `ifDefined`, kept for raw JSON schemas.
451
418
  ajv.addKeyword({
452
419
  keyword: 'isUndefined',
453
- modifying: false,
454
- errors: false,
455
420
  schemaType: 'boolean',
456
- validate: (_schema, data) => data === undefined,
421
+ code(cxt) {
422
+ cxt.fail(_ `${cxt.data} !== undefined`);
423
+ },
457
424
  });
458
425
  // This is added because Ajv validates the `min/maxProperties` before validating the properties.
459
426
  // So, in case of `minProperties(1)` and `{ foo: 'bar' }` Ajv will let it pass, even
@@ -660,6 +627,25 @@ export function createAjv(opt) {
660
627
  });
661
628
  return ajv;
662
629
  }
630
+ const isoDateRules = ['before', 'sameOrBefore', 'after', 'sameOrAfter'];
631
+ const isoDateRuleLabels = {
632
+ before: 'before',
633
+ sameOrBefore: 'the same or before',
634
+ after: 'after',
635
+ sameOrAfter: 'the same or after',
636
+ };
637
+ /**
638
+ * IsoDate strings compare chronologically as plain strings.
639
+ */
640
+ function isoDateRuleFails(rule, data, limit) {
641
+ if (rule === 'before')
642
+ return _ `${data} >= ${limit}`;
643
+ if (rule === 'sameOrBefore')
644
+ return _ `${data} > ${limit}`;
645
+ if (rule === 'after')
646
+ return _ `${data} <= ${limit}`;
647
+ return _ `${data} < ${limit}`;
648
+ }
663
649
  const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
664
650
  const DASH_CODE = '-'.charCodeAt(0);
665
651
  const ZERO_CODE = '0'.charCodeAt(0);
@@ -788,6 +774,12 @@ function isIsoTimezoneValid(s) {
788
774
  return false; // min is -12:00
789
775
  return true;
790
776
  }
777
+ function isUtcOffsetValid(s) {
778
+ return s !== 'Z' && s !== '' && isIsoTimezoneValid(s);
779
+ }
780
+ function isUtcOffsetOrZValid(s) {
781
+ return s === 'Z' || (s !== '' && isIsoTimezoneValid(s));
782
+ }
791
783
  /**
792
784
  * This is a performance optimized correct validation
793
785
  * for ISO month formatted as "YYYY-MM".
@@ -307,6 +307,7 @@ export declare class JIsoDate<Opt extends boolean = false> extends JBuilder<IsoD
307
307
  after(date: string): this;
308
308
  sameOrAfter(date: string): this;
309
309
  between(fromDate: string, toDate: string, incl: Inclusiveness): this;
310
+ private addRules;
310
311
  }
311
312
  export interface JsonSchemaIsoDateOptions {
312
313
  before?: string;
@@ -387,6 +388,10 @@ export declare class JObject<OUT extends AnyObject, Opt extends boolean = false>
387
388
  }
388
389
  interface JObjectOpts {
389
390
  hasIsOfTypeCheck?: false;
391
+ /**
392
+ * Schema for every property not listed in `properties`, e.g the values of a string map.
393
+ */
394
+ additionalProperties?: JsonSchema<any>;
390
395
  patternProperties?: StringMap<JsonSchema<any>>;
391
396
  keySchema?: JsonSchema;
392
397
  }
@@ -574,7 +579,7 @@ export interface JsonSchema<OUT = unknown> {
574
579
  };
575
580
  patternProperties?: StringMap<JsonSchema<any>>;
576
581
  required?: string[];
577
- additionalProperties?: boolean;
582
+ additionalProperties?: boolean | JsonSchema<any>;
578
583
  minProperties?: number;
579
584
  maxProperties?: number;
580
585
  default?: OUT;
@@ -626,6 +631,10 @@ export interface JsonSchema<OUT = unknown> {
626
631
  errorMessages?: StringMap<string>;
627
632
  optionalValues?: (string | number | boolean | null)[];
628
633
  keySchema?: JsonSchema;
634
+ /**
635
+ * Validates the value against the given schema only when it's defined.
636
+ */
637
+ ifDefined?: JsonSchema;
629
638
  isUndefined?: true;
630
639
  minProperties2?: number;
631
640
  exclusiveProperties?: (readonly string[])[];
@@ -38,13 +38,13 @@ export const j = {
38
38
  const isValueOptional = schema.getSchema().optionalField;
39
39
  const builtSchema = schema.build();
40
40
  const finalValueSchema = isValueOptional
41
- ? { anyOf: [{ isUndefined: true }, builtSchema] }
41
+ ? { ifDefined: builtSchema }
42
42
  : builtSchema;
43
+ // `additionalProperties` instead of `patternProperties: { '^.+$': ... }`, which made Ajv
44
+ // regex-test every key twice (once more for `additionalProperties: false`)
43
45
  return new JObject({}, {
44
46
  hasIsOfTypeCheck: false,
45
- patternProperties: {
46
- '^.+$': finalValueSchema,
47
- },
47
+ additionalProperties: finalValueSchema,
48
48
  });
49
49
  },
50
50
  /**
@@ -695,26 +695,25 @@ export class JIsoDate extends JBuilder {
695
695
  });
696
696
  }
697
697
  before(date) {
698
- return this.cloneAndUpdateSchema({ IsoDate: { before: date } });
698
+ return this.addRules({ before: date });
699
699
  }
700
700
  sameOrBefore(date) {
701
- return this.cloneAndUpdateSchema({ IsoDate: { sameOrBefore: date } });
701
+ return this.addRules({ sameOrBefore: date });
702
702
  }
703
703
  after(date) {
704
- return this.cloneAndUpdateSchema({ IsoDate: { after: date } });
704
+ return this.addRules({ after: date });
705
705
  }
706
706
  sameOrAfter(date) {
707
- return this.cloneAndUpdateSchema({ IsoDate: { sameOrAfter: date } });
707
+ return this.addRules({ sameOrAfter: date });
708
708
  }
709
709
  between(fromDate, toDate, incl) {
710
- let schemaPatch = {};
711
710
  if (incl === '[)') {
712
- schemaPatch = { IsoDate: { sameOrAfter: fromDate, before: toDate } };
713
- }
714
- else if (incl === '[]') {
715
- schemaPatch = { IsoDate: { sameOrAfter: fromDate, sameOrBefore: toDate } };
711
+ return this.addRules({ sameOrAfter: fromDate, before: toDate });
716
712
  }
717
- return this.cloneAndUpdateSchema(schemaPatch);
713
+ return this.addRules({ sameOrAfter: fromDate, sameOrBefore: toDate });
714
+ }
715
+ addRules(rules) {
716
+ return this.cloneAndUpdateSchema({ IsoDate: { ...this.schema.IsoDate, ...rules } });
718
717
  }
719
718
  }
720
719
  export class JNumber extends JBuilder {
@@ -832,7 +831,7 @@ export class JObject extends JBuilder {
832
831
  type: 'object',
833
832
  properties: {},
834
833
  required: [],
835
- additionalProperties: false,
834
+ additionalProperties: opt?.additionalProperties ?? false,
836
835
  hasIsOfTypeCheck: opt?.hasIsOfTypeCheck ?? true,
837
836
  patternProperties: opt?.patternProperties ?? undefined,
838
837
  keySchema: opt?.keySchema ?? undefined,
@@ -844,6 +843,9 @@ export class JObject extends JBuilder {
844
843
  * When set, the validation will not strip away properties that are not specified explicitly in the schema.
845
844
  */
846
845
  allowAdditionalProperties() {
846
+ // A string map already accepts every property, validated by its value schema
847
+ if (typeof this.schema.additionalProperties === 'object')
848
+ return this.clone();
847
849
  return this.cloneAndUpdateSchema({ additionalProperties: true });
848
850
  }
849
851
  extend(props) {
@@ -1024,16 +1026,13 @@ function record(keySchema, valueSchema) {
1024
1026
  // Check if value schema is optional before build() strips the optionalField flag
1025
1027
  const isValueOptional = valueSchema.getSchema().optionalField;
1026
1028
  const valueJsonSchema = valueSchema.build();
1027
- // When value schema is optional, wrap in anyOf to allow undefined values
1028
1029
  const finalValueSchema = isValueOptional
1029
- ? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
1030
+ ? { ifDefined: valueJsonSchema }
1030
1031
  : valueJsonSchema;
1031
1032
  return new JObject([], {
1032
1033
  hasIsOfTypeCheck: false,
1033
1034
  keySchema: keyJsonSchema,
1034
- patternProperties: {
1035
- ['^.*$']: finalValueSchema,
1036
- },
1035
+ additionalProperties: finalValueSchema,
1037
1036
  });
1038
1037
  }
1039
1038
  function withRegexKeys(keyRegex, schema) {
@@ -53,16 +53,20 @@ export function mergeJsonSchemaObjects(schema1, schema2) {
53
53
  s1.properties[k] = v;
54
54
  });
55
55
  // Merge `patternProperties`
56
- Object.entries(s2.patternProperties || {}).forEach(([k, v]) => {
57
- s1.patternProperties[k] = v;
58
- });
56
+ if (s2.patternProperties) {
57
+ s1.patternProperties = { ...s1.patternProperties, ...s2.patternProperties };
58
+ }
59
59
  s1.propertyNames = s2.propertyNames || s1.propertyNames;
60
+ s1.keySchema = s2.keySchema || s1.keySchema;
60
61
  s1.minProperties = s2.minProperties ?? s1.minProperties;
61
62
  s1.maxProperties = s2.maxProperties ?? s1.maxProperties;
62
63
  // Merge `required`
63
64
  s1.required.push(...s2.required);
64
65
  s1.required = _uniq(s1.required).sort();
65
- // `additionalProperties` remains the same
66
+ // `additionalProperties` remains the same, unless s2 is a string map (schema-valued)
67
+ if (typeof s2.additionalProperties === 'object') {
68
+ s1.additionalProperties = s2.additionalProperties;
69
+ }
66
70
  return _filterNullishValues(s1, { mutate: true });
67
71
  }
68
72
  export function isEveryItemString(arr) {
@@ -8,6 +8,7 @@ integer read/write helpers, DOS date/time conversion (both directions) and
8
8
  entry-name normalization/validation.
9
9
 
10
10
  */
11
+ // oxlint-disable no-restricted-globals -- DOS date/time fields are local-time components, converted via the native Date
11
12
  // oxlint-disable no-bitwise -- the ZIP format packs DOS date/time into bit fields
12
13
  // Compression methods.
13
14
  export const STORED = 0;
@@ -29,6 +29,7 @@ import { finished, pipeline } from 'node:stream/promises';
29
29
  import { promisify } from 'node:util';
30
30
  import zlib from 'node:zlib';
31
31
  import { comparators } from '@naturalcycles/js-lib/array/sort.js';
32
+ import { nowUnix } from '@naturalcycles/js-lib/datetime/localTime.js';
32
33
  import { _assert } from '@naturalcycles/js-lib/error/assert.js';
33
34
  import { glob } from 'tinyglobby';
34
35
  import { assertSafeZipEntryName, CDFH_SIG, CDFH_SIZE, DATA_DESCRIPTOR_SIG, DEFLATE, EOCDR_SIG, EOCDR_SIZE, LOCAL_FILE_HEADER_SIG, MAX_COMMENT_SIZE, normalizeZipEntryName, STORED, unixToDosDateTime, writeUInt64LE, ZIP64_EOCDL_SIG, ZIP64_EOCDL_SIZE, ZIP64_EOCDR_SIG, ZIP64_EOCDR_SIZE, } from './zipInternal.js';
@@ -222,7 +223,7 @@ export class ZipWriter {
222
223
  if (nameBuf.length > MAX_COMMENT_SIZE) {
223
224
  throw new Error(`zip entry name is too long: ${nameBuf.length} > ${MAX_COMMENT_SIZE} bytes`);
224
225
  }
225
- const mtime = opt.mtime ?? Math.floor(Date.now() / 1000);
226
+ const mtime = opt.mtime ?? nowUnix();
226
227
  const { date, time } = unixToDosDateTime(mtime);
227
228
  const mode = opt.mode ?? (isDirectory ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE);
228
229
  if ((mode & 0xffff) !== mode) {