@naturalcycles/nodejs-lib 15.121.1 → 15.122.1

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.
@@ -277,8 +277,15 @@ class Exec2 {
277
277
  }
278
278
  }
279
279
  logFinish(cmd, opt, started, isSuccessful) {
280
- if (isSuccessful && !opt.logFinish)
281
- return;
280
+ if (!opt.logFinish) {
281
+ if (isSuccessful)
282
+ return;
283
+ // Failures are logged even with logFinish: false,
284
+ // unless explicitly silenced with `log: false`
285
+ // (e.g when the caller buffers the output to print it later, like runCheck does)
286
+ if (opt.log === false)
287
+ return;
288
+ }
282
289
  console.log([
283
290
  isSuccessful ? ' ✓' : ' ×',
284
291
  white(opt.name || _substringAfterLast(cmd, '/')),
@@ -2,7 +2,7 @@ import type { ValidationFunction, ValidationFunctionResult } from '@naturalcycle
2
2
  import type { Set2 } from '@naturalcycles/js-lib/object';
3
3
  import type { AnyObject, BaseDBEntity, IANATimezone, Inclusiveness, IsoDate, IsoDateTime, IsoMonth, NumberEnum, StringEnum, StringMap, UnixTimestamp, UnixTimestampMillis } from '@naturalcycles/js-lib/types';
4
4
  import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
5
- import type { Ajv } from 'ajv';
5
+ import type { Ajv, ValidateFunction } from 'ajv';
6
6
  import { AjvValidationError } from './ajvValidationError.js';
7
7
  export declare const j: {
8
8
  /**
@@ -480,12 +480,12 @@ export declare class AjvSchema<OUT> {
480
480
  * Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
481
481
  * Used internally by JSchema to cache a compatible AjvSchema instance.
482
482
  */
483
- static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: any): AjvSchema<OUT>;
483
+ static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: ValidateFunction): AjvSchema<OUT>;
484
484
  static isSchemaWithCachedAjvSchema<Base, OUT>(schema: Base): schema is WithCachedAjvSchema<Base, OUT>;
485
485
  static cacheAjvSchema<Base extends AnyObject, OUT>(schema: Base, ajvSchema: AjvSchema<OUT>): WithCachedAjvSchema<Base, OUT>;
486
486
  static requireCachedAjvSchema<Base, OUT>(schema: WithCachedAjvSchema<Base, OUT>): AjvSchema<OUT>;
487
487
  readonly cfg: AjvSchemaCfg;
488
- private _compiledFn;
488
+ private _compiledFn?;
489
489
  private _getValidateFn;
490
490
  /**
491
491
  * It returns the original object just for convenience.
@@ -1174,6 +1174,9 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1174
1174
  fn.errors = [
1175
1175
  {
1176
1176
  instancePath: '',
1177
+ schemaPath: '',
1178
+ keyword: 'postValidation',
1179
+ params: {},
1177
1180
  message: err.message,
1178
1181
  },
1179
1182
  ];
@@ -1190,15 +1193,40 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1190
1193
  // Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
1191
1194
  // .items[0].name, embedding the index into the segment and making it harder to strip without regex
1192
1195
  const fingerprint = buildAjvErrorFingerprint(errors[0], inputName, resolveCustomErrorMessage(builtSchema, errors[0]));
1193
- applyImprovementsOnErrorMessages(errors, builtSchema);
1194
- let message = getAjv().errorsText(errors, {
1195
- dataVar,
1196
- separator,
1197
- });
1198
1196
  // Note: if we mutated the input already, e.g stripped unknown properties,
1199
1197
  // the error message Input would contain already mutated object print, such as Input: {}
1200
1198
  // Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
1201
- const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 });
1199
+ const inputForPrint = opt.getOriginalInput?.() || input;
1200
+ // Resolve each error's failing value while its instancePath is still a raw JSON pointer
1201
+ // (e.g `/events/2/name`), which is unambiguous to walk - unlike the dot notation it gets
1202
+ // rewritten into below (a property name may itself contain a dot).
1203
+ // Root errors (empty instancePath) resolve to undefined - there the value is the whole Input,
1204
+ // which is printed anyway.
1205
+ // `required` errors also resolve to undefined - their instancePath points at the parent object
1206
+ // that lacks the property, so `got:` would print that whole object - noise next to the Input print.
1207
+ const failingValueByError = new Map(errors.map(e => [
1208
+ e,
1209
+ e.instancePath && e.keyword !== 'required'
1210
+ ? getValueAtJsonPointer(inputForPrint, e.instancePath)
1211
+ : undefined,
1212
+ ]));
1213
+ applyImprovementsOnErrorMessages(errors, builtSchema);
1214
+ // Same line format as Ajv's errorsText().
1215
+ // Each error line is followed by a `Got: ...` line printing the narrow failing value itself,
1216
+ // so it stays visible even when the Input print below gets truncated for large inputs.
1217
+ // `Got` is a separate line (and is allowed to print multi-line) on purpose: consumers that
1218
+ // only take the first line of the message keep getting the unchanged (pre-Got) error line.
1219
+ let message = errors
1220
+ .flatMap(e => {
1221
+ const lines = [`${dataVar}${e.instancePath} ${e.message}`];
1222
+ const value = failingValueByError.get(e);
1223
+ if (value !== undefined) {
1224
+ lines.push(`Got: ${_inspect(value, { maxLen: 500 })}`);
1225
+ }
1226
+ return lines;
1227
+ })
1228
+ .join(separator);
1229
+ const inputStringified = _inspect(inputForPrint, { maxLen: 4000 });
1202
1230
  message = [message, 'Input: ' + inputStringified].join(separator);
1203
1231
  const err = new AjvValidationError(message, _filterNullishValues({
1204
1232
  errors,
@@ -1209,6 +1237,19 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1209
1237
  return [err, output];
1210
1238
  }
1211
1239
  // ==== Error formatting helpers ====
1240
+ /**
1241
+ * Resolves the value at an Ajv error's raw instancePath, which is a JSON pointer, e.g `/events/2/name`.
1242
+ * Returns undefined if the path cannot be resolved (e.g the input was mutated).
1243
+ */
1244
+ function getValueAtJsonPointer(input, pointer) {
1245
+ let value = input;
1246
+ for (const segment of pointer.split('/').slice(1)) {
1247
+ if (value === null || typeof value !== 'object')
1248
+ return undefined;
1249
+ value = value[segment.replaceAll('~1', '/').replaceAll('~0', '~')];
1250
+ }
1251
+ return value;
1252
+ }
1212
1253
  function applyImprovementsOnErrorMessages(errors, schema) {
1213
1254
  if (!errors)
1214
1255
  return;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.121.1",
4
+ "version": "15.122.1",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -14,8 +14,8 @@
14
14
  "yaml": "^2"
15
15
  },
16
16
  "devDependencies": {
17
- "typescript": "^7",
18
- "@naturalcycles/dev-lib": "0.0.0"
17
+ "@naturalcycles/dev-lib": "0.0.0",
18
+ "typescript": "^7"
19
19
  },
20
20
  "exports": {
21
21
  ".": "./dist/index.js",
@@ -332,7 +332,13 @@ class Exec2 {
332
332
  started: UnixTimestampMillis,
333
333
  isSuccessful: boolean,
334
334
  ): void {
335
- if (isSuccessful && !opt.logFinish) return
335
+ if (!opt.logFinish) {
336
+ if (isSuccessful) return
337
+ // Failures are logged even with logFinish: false,
338
+ // unless explicitly silenced with `log: false`
339
+ // (e.g when the caller buffers the output to print it later, like runCheck does)
340
+ if (opt.log === false) return
341
+ }
336
342
 
337
343
  console.log(
338
344
  [
@@ -27,7 +27,7 @@ import type {
27
27
  } from '@naturalcycles/js-lib/types'
28
28
  import { _objectAssign, _typeCast, JWT_REGEX } from '@naturalcycles/js-lib/types'
29
29
  import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec'
30
- import type { Ajv, ErrorObject } from 'ajv'
30
+ import type { Ajv, ErrorObject, ValidateFunction } from 'ajv'
31
31
  import { _inspect } from '../../string/inspect.js'
32
32
  import {
33
33
  BASE64URL_REGEX,
@@ -367,7 +367,7 @@ export class JSchema<OUT, Opt>
367
367
  }
368
368
 
369
369
  private _builtSchema?: JsonSchema
370
- private _compiledFns?: WeakMap<Ajv, any>
370
+ private _compiledFns?: WeakMap<Ajv, ValidateFunction>
371
371
 
372
372
  private _getBuiltSchema(): JsonSchema {
373
373
  if (!this._builtSchema) {
@@ -387,7 +387,7 @@ export class JSchema<OUT, Opt>
387
387
  return this._builtSchema
388
388
  }
389
389
 
390
- private _getCompiled(overrideAjv?: Ajv): { fn: any; builtSchema: JsonSchema } {
390
+ private _getCompiled(overrideAjv?: Ajv): { fn: ValidateFunction; builtSchema: JsonSchema } {
391
391
  const builtSchema = this._getBuiltSchema()
392
392
  const ajv = overrideAjv ?? this._cfg?.ajv ?? getAjv()
393
393
 
@@ -1105,18 +1105,22 @@ export class JObject<OUT extends AnyObject, Opt extends boolean = false> extends
1105
1105
  OUT,
1106
1106
  {
1107
1107
  // required keys
1108
- [K in keyof P as P[K] extends JSchema<any, infer IsOpt>
1109
- ? IsOpt extends true
1110
- ? never
1111
- : K
1112
- : never]: P[K] extends JSchema<infer OUT2, any> ? OUT2 : never
1108
+ [
1109
+ K in keyof P as P[K] extends JSchema<any, infer IsOpt>
1110
+ ? IsOpt extends true
1111
+ ? never
1112
+ : K
1113
+ : never
1114
+ ]: P[K] extends JSchema<infer OUT2, any> ? OUT2 : never
1113
1115
  } & {
1114
1116
  // optional keys
1115
- [K in keyof P as P[K] extends JSchema<any, infer IsOpt>
1116
- ? IsOpt extends true
1117
- ? K
1117
+ [
1118
+ K in keyof P as P[K] extends JSchema<any, infer IsOpt>
1119
+ ? IsOpt extends true
1120
+ ? K
1121
+ : never
1118
1122
  : never
1119
- : never]?: P[K] extends JSchema<infer OUT2, any> ? OUT2 : never
1123
+ ]?: P[K] extends JSchema<infer OUT2, any> ? OUT2 : never
1120
1124
  }
1121
1125
  >
1122
1126
  >,
@@ -1185,17 +1189,21 @@ export class JObjectInfer<
1185
1189
  > extends JBuilder<
1186
1190
  Expand<
1187
1191
  {
1188
- [K in keyof PROPS as PROPS[K] extends JSchema<any, infer IsOpt>
1189
- ? IsOpt extends true
1190
- ? never
1191
- : K
1192
- : never]: PROPS[K] extends JSchema<infer OUT, any> ? OUT : never
1192
+ [
1193
+ K in keyof PROPS as PROPS[K] extends JSchema<any, infer IsOpt>
1194
+ ? IsOpt extends true
1195
+ ? never
1196
+ : K
1197
+ : never
1198
+ ]: PROPS[K] extends JSchema<infer OUT, any> ? OUT : never
1193
1199
  } & {
1194
- [K in keyof PROPS as PROPS[K] extends JSchema<any, infer IsOpt>
1195
- ? IsOpt extends true
1196
- ? K
1200
+ [
1201
+ K in keyof PROPS as PROPS[K] extends JSchema<any, infer IsOpt>
1202
+ ? IsOpt extends true
1203
+ ? K
1204
+ : never
1197
1205
  : never
1198
- : never]?: PROPS[K] extends JSchema<infer OUT, any> ? OUT : never
1206
+ ]?: PROPS[K] extends JSchema<infer OUT, any> ? OUT : never
1199
1207
  }
1200
1208
  >,
1201
1209
  Opt
@@ -1516,7 +1524,7 @@ export class AjvSchema<OUT> {
1516
1524
  private constructor(
1517
1525
  public schema: JsonSchema<OUT>,
1518
1526
  cfg: Partial<AjvSchemaCfg> = {},
1519
- preCompiledFn?: any,
1527
+ preCompiledFn?: ValidateFunction,
1520
1528
  ) {
1521
1529
  this.cfg = {
1522
1530
  lazy: false,
@@ -1585,7 +1593,7 @@ export class AjvSchema<OUT> {
1585
1593
  * Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
1586
1594
  * Used internally by JSchema to cache a compatible AjvSchema instance.
1587
1595
  */
1588
- static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: any): AjvSchema<OUT> {
1596
+ static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: ValidateFunction): AjvSchema<OUT> {
1589
1597
  return new AjvSchema<OUT>(schema, {}, compiledFn)
1590
1598
  }
1591
1599
 
@@ -1608,9 +1616,9 @@ export class AjvSchema<OUT> {
1608
1616
 
1609
1617
  readonly cfg: AjvSchemaCfg
1610
1618
 
1611
- private _compiledFn: any
1619
+ private _compiledFn?: ValidateFunction
1612
1620
 
1613
- private _getValidateFn(): any {
1621
+ private _getValidateFn(): ValidateFunction {
1614
1622
  this._compiledFn ||= this.cfg.ajv.compile(this.schema as any)
1615
1623
  return this._compiledFn
1616
1624
  }
@@ -1663,7 +1671,7 @@ export class AjvSchema<OUT> {
1663
1671
  const separator = '\n'
1664
1672
 
1665
1673
  function executeValidation<OUT>(
1666
- fn: any,
1674
+ fn: ValidateFunction,
1667
1675
  builtSchema: JsonSchema,
1668
1676
  input: unknown,
1669
1677
  opt: AjvValidationOptions = {},
@@ -1685,6 +1693,9 @@ function executeValidation<OUT>(
1685
1693
  fn.errors = [
1686
1694
  {
1687
1695
  instancePath: '',
1696
+ schemaPath: '',
1697
+ keyword: 'postValidation',
1698
+ params: {},
1688
1699
  message: err.message,
1689
1700
  },
1690
1701
  ]
@@ -1706,22 +1717,51 @@ function executeValidation<OUT>(
1706
1717
  // Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
1707
1718
  // .items[0].name, embedding the index into the segment and making it harder to strip without regex
1708
1719
  const fingerprint = buildAjvErrorFingerprint(
1709
- errors[0],
1720
+ errors[0]!,
1710
1721
  inputName,
1711
- resolveCustomErrorMessage(builtSchema, errors[0]),
1722
+ resolveCustomErrorMessage(builtSchema, errors[0]!),
1712
1723
  )
1713
1724
 
1714
- applyImprovementsOnErrorMessages(errors, builtSchema)
1715
-
1716
- let message = getAjv().errorsText(errors, {
1717
- dataVar,
1718
- separator,
1719
- })
1720
-
1721
1725
  // Note: if we mutated the input already, e.g stripped unknown properties,
1722
1726
  // the error message Input would contain already mutated object print, such as Input: {}
1723
1727
  // Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
1724
- const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 })
1728
+ const inputForPrint = opt.getOriginalInput?.() || input
1729
+
1730
+ // Resolve each error's failing value while its instancePath is still a raw JSON pointer
1731
+ // (e.g `/events/2/name`), which is unambiguous to walk - unlike the dot notation it gets
1732
+ // rewritten into below (a property name may itself contain a dot).
1733
+ // Root errors (empty instancePath) resolve to undefined - there the value is the whole Input,
1734
+ // which is printed anyway.
1735
+ // `required` errors also resolve to undefined - their instancePath points at the parent object
1736
+ // that lacks the property, so `got:` would print that whole object - noise next to the Input print.
1737
+ const failingValueByError = new Map<ErrorObject, unknown>(
1738
+ errors.map(e => [
1739
+ e,
1740
+ e.instancePath && e.keyword !== 'required'
1741
+ ? getValueAtJsonPointer(inputForPrint, e.instancePath)
1742
+ : undefined,
1743
+ ]),
1744
+ )
1745
+
1746
+ applyImprovementsOnErrorMessages(errors, builtSchema)
1747
+
1748
+ // Same line format as Ajv's errorsText().
1749
+ // Each error line is followed by a `Got: ...` line printing the narrow failing value itself,
1750
+ // so it stays visible even when the Input print below gets truncated for large inputs.
1751
+ // `Got` is a separate line (and is allowed to print multi-line) on purpose: consumers that
1752
+ // only take the first line of the message keep getting the unchanged (pre-Got) error line.
1753
+ let message = errors
1754
+ .flatMap(e => {
1755
+ const lines = [`${dataVar}${e.instancePath} ${e.message}`]
1756
+ const value = failingValueByError.get(e)
1757
+ if (value !== undefined) {
1758
+ lines.push(`Got: ${_inspect(value, { maxLen: 500 })}`)
1759
+ }
1760
+ return lines
1761
+ })
1762
+ .join(separator)
1763
+
1764
+ const inputStringified = _inspect(inputForPrint, { maxLen: 4000 })
1725
1765
  message = [message, 'Input: ' + inputStringified].join(separator)
1726
1766
 
1727
1767
  const err = new AjvValidationError(
@@ -1738,6 +1778,19 @@ function executeValidation<OUT>(
1738
1778
 
1739
1779
  // ==== Error formatting helpers ====
1740
1780
 
1781
+ /**
1782
+ * Resolves the value at an Ajv error's raw instancePath, which is a JSON pointer, e.g `/events/2/name`.
1783
+ * Returns undefined if the path cannot be resolved (e.g the input was mutated).
1784
+ */
1785
+ function getValueAtJsonPointer(input: unknown, pointer: string): unknown {
1786
+ let value: any = input
1787
+ for (const segment of pointer.split('/').slice(1)) {
1788
+ if (value === null || typeof value !== 'object') return undefined
1789
+ value = value[segment.replaceAll('~1', '/').replaceAll('~0', '~')]
1790
+ }
1791
+ return value
1792
+ }
1793
+
1741
1794
  function applyImprovementsOnErrorMessages(
1742
1795
  errors: ErrorObject[] | null | undefined,
1743
1796
  schema: JsonSchema,
@@ -2146,13 +2199,15 @@ type StripIndexSignatureDeep<T> = T extends readonly unknown[]
2146
2199
  ? T
2147
2200
  : T extends Record<string, any>
2148
2201
  ? {
2149
- [K in keyof T as string extends K
2150
- ? never
2151
- : number extends K
2202
+ [
2203
+ K in keyof T as string extends K
2152
2204
  ? never
2153
- : symbol extends K
2205
+ : number extends K
2154
2206
  ? never
2155
- : K]: StripIndexSignatureDeep<T[K]>
2207
+ : symbol extends K
2208
+ ? never
2209
+ : K
2210
+ ]: StripIndexSignatureDeep<T[K]>
2156
2211
  }
2157
2212
  : T
2158
2213