@naturalcycles/nodejs-lib 15.107.4 → 15.109.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/csv/csvWriter.js +1 -1
- package/dist/fs/fs2.d.ts +2 -2
- package/dist/validation/ajv/from-data/generateJsonSchemaFromData.js +1 -1
- package/dist/validation/ajv/jSchema.js +35 -24
- package/package.json +2 -2
- package/src/csv/csvWriter.ts +1 -1
- package/src/validation/ajv/from-data/generateJsonSchemaFromData.ts +1 -1
- package/src/validation/ajv/jSchema.ts +42 -23
package/dist/csv/csvWriter.js
CHANGED
package/dist/fs/fs2.d.ts
CHANGED
|
@@ -63,9 +63,9 @@ declare class FS2 {
|
|
|
63
63
|
isDirectory(filePath: string): boolean;
|
|
64
64
|
fs: typeof fs;
|
|
65
65
|
fsp: typeof fsp;
|
|
66
|
-
lstat: fs.
|
|
66
|
+
lstat: typeof fs.lstatSync;
|
|
67
67
|
lstatAsync: typeof fsp.lstat;
|
|
68
|
-
stat: fs.
|
|
68
|
+
stat: typeof fs.statSync;
|
|
69
69
|
statAsync: typeof fsp.stat;
|
|
70
70
|
mkdir: typeof fs.mkdirSync;
|
|
71
71
|
mkdirAsync: typeof fsp.mkdir;
|
|
@@ -23,7 +23,7 @@ function objectToJsonSchema(rows) {
|
|
|
23
23
|
additionalProperties: true,
|
|
24
24
|
};
|
|
25
25
|
_stringMapEntries(typesByKey).forEach(([key, types]) => {
|
|
26
|
-
const schema = mergeTypes(
|
|
26
|
+
const schema = mergeTypes(Array.from(types), rows.map(r => r[key]));
|
|
27
27
|
if (!schema)
|
|
28
28
|
return;
|
|
29
29
|
s.properties[key] = schema;
|
|
@@ -472,14 +472,14 @@ export class JBuilder extends JSchema {
|
|
|
472
472
|
const innerSchema = {
|
|
473
473
|
...(builtSchema.type ? { type: builtSchema.type } : {}),
|
|
474
474
|
anyOf: [builtSchema, alternativesSchema],
|
|
475
|
-
optionalValues:
|
|
475
|
+
optionalValues: optionalValues.slice(),
|
|
476
476
|
};
|
|
477
477
|
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
478
478
|
// so we must allow `null` values to be parsed by Ajv,
|
|
479
479
|
// but the typing should not reflect that.
|
|
480
480
|
if (optionalValues.includes(null)) {
|
|
481
481
|
return new JSchema({
|
|
482
|
-
anyOf: [{ type: 'null', optionalValues:
|
|
482
|
+
anyOf: [{ type: 'null', optionalValues: optionalValues.slice() }, innerSchema],
|
|
483
483
|
optionalField: true,
|
|
484
484
|
});
|
|
485
485
|
}
|
|
@@ -1111,9 +1111,7 @@ export class AjvSchema {
|
|
|
1111
1111
|
cfg;
|
|
1112
1112
|
_compiledFn;
|
|
1113
1113
|
_getValidateFn() {
|
|
1114
|
-
|
|
1115
|
-
this._compiledFn = this.cfg.ajv.compile(this.schema);
|
|
1116
|
-
}
|
|
1114
|
+
this._compiledFn ||= this.cfg.ajv.compile(this.schema);
|
|
1117
1115
|
return this._compiledFn;
|
|
1118
1116
|
}
|
|
1119
1117
|
/**
|
|
@@ -1180,7 +1178,7 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
|
|
|
1180
1178
|
const dataVar = [inputName, inputId].filter(Boolean).join('.');
|
|
1181
1179
|
// Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
|
|
1182
1180
|
// .items[0].name, embedding the index into the segment and making it harder to strip without regex
|
|
1183
|
-
const fingerprint = buildAjvErrorFingerprint(errors[0], inputName);
|
|
1181
|
+
const fingerprint = buildAjvErrorFingerprint(errors[0], inputName, resolveCustomErrorMessage(builtSchema, errors[0]));
|
|
1184
1182
|
applyImprovementsOnErrorMessages(errors, builtSchema);
|
|
1185
1183
|
let message = getAjv().errorsText(errors, {
|
|
1186
1184
|
dataVar,
|
|
@@ -1204,33 +1202,46 @@ function applyImprovementsOnErrorMessages(errors, schema) {
|
|
|
1204
1202
|
if (!errors)
|
|
1205
1203
|
return;
|
|
1206
1204
|
filterNullableAnyOfErrors(errors, schema);
|
|
1207
|
-
const { errorMessages } = schema;
|
|
1208
1205
|
for (const error of errors) {
|
|
1209
|
-
const
|
|
1210
|
-
if (
|
|
1211
|
-
error.message =
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
else {
|
|
1217
|
-
const unwrapped = unwrapNullableAnyOf(schema);
|
|
1218
|
-
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
1219
|
-
error.message = unwrapped.errorMessages[error.keyword];
|
|
1220
|
-
}
|
|
1206
|
+
const customMessage = resolveCustomErrorMessage(schema, error);
|
|
1207
|
+
if (customMessage) {
|
|
1208
|
+
error.message = customMessage;
|
|
1209
|
+
// A custom `msg` signals "the underlying rule param is an implementation detail".
|
|
1210
|
+
// Drop params so consumers (e.g. HTTP responses, Sentry payloads) don't surface
|
|
1211
|
+
// the raw regex/limit/etc. — the custom message is now the canonical rule label.
|
|
1212
|
+
error.params = {};
|
|
1221
1213
|
}
|
|
1222
1214
|
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
|
|
1223
1215
|
}
|
|
1224
1216
|
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Looks up the user-provided custom error message (set via `{ msg }` / `{ name }`)
|
|
1219
|
+
* for a given AJV error, walking the schema along the error's instancePath and
|
|
1220
|
+
* falling back to top-level `errorMessages` (including through nullable wrappers).
|
|
1221
|
+
* Returns undefined if no custom message was registered for the failing keyword.
|
|
1222
|
+
*/
|
|
1223
|
+
function resolveCustomErrorMessage(schema, error) {
|
|
1224
|
+
const byPath = getErrorMessageForInstancePath(schema, error.instancePath, error.keyword);
|
|
1225
|
+
if (byPath)
|
|
1226
|
+
return byPath;
|
|
1227
|
+
if (schema.errorMessages?.[error.keyword])
|
|
1228
|
+
return schema.errorMessages[error.keyword];
|
|
1229
|
+
const unwrapped = unwrapNullableAnyOf(schema);
|
|
1230
|
+
return unwrapped?.errorMessages?.[error.keyword];
|
|
1231
|
+
}
|
|
1225
1232
|
/**
|
|
1226
1233
|
* Groups repeated validation errors by rule rather than by unique request content.
|
|
1227
1234
|
* Excludes instance-specific data like record IDs and array indices.
|
|
1235
|
+
*
|
|
1236
|
+
* When a custom error message is registered for the failing rule, prefer it over
|
|
1237
|
+
* the raw param value (e.g. regex source) — it is both stable across regex tweaks
|
|
1238
|
+
* and avoids leaking the underlying pattern into Sentry fingerprints.
|
|
1228
1239
|
*/
|
|
1229
|
-
function buildAjvErrorFingerprint(e, inputName) {
|
|
1230
|
-
const
|
|
1240
|
+
function buildAjvErrorFingerprint(e, inputName, customMessage) {
|
|
1241
|
+
const ruleValue = customMessage ?? Object.values(e.params || {})[0];
|
|
1231
1242
|
let rule = e.keyword;
|
|
1232
|
-
if (
|
|
1233
|
-
rule += `:${
|
|
1243
|
+
if (ruleValue !== undefined)
|
|
1244
|
+
rule += `:${ruleValue}`;
|
|
1234
1245
|
const path = e.instancePath
|
|
1235
1246
|
.split('/')
|
|
1236
1247
|
.filter(s => s && isNaN(Number(s)))
|
|
@@ -1394,7 +1405,7 @@ function deepCopyPreservingFunctions(obj) {
|
|
|
1394
1405
|
const value = obj[key];
|
|
1395
1406
|
copy[key] =
|
|
1396
1407
|
(key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
|
|
1397
|
-
?
|
|
1408
|
+
? value.slice()
|
|
1398
1409
|
: deepCopyPreservingFunctions(value);
|
|
1399
1410
|
}
|
|
1400
1411
|
return copy;
|
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.109.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@standard-schema/spec": "^1",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@typescript/native-preview": "beta",
|
|
21
|
-
"@naturalcycles/dev-lib": "20.
|
|
21
|
+
"@naturalcycles/dev-lib": "20.48.0"
|
|
22
22
|
},
|
|
23
23
|
"exports": {
|
|
24
24
|
".": "./dist/index.js",
|
package/src/csv/csvWriter.ts
CHANGED
|
@@ -36,7 +36,7 @@ function objectToJsonSchema<T extends AnyObject>(rows: AnyObject[]): JsonSchema<
|
|
|
36
36
|
|
|
37
37
|
_stringMapEntries(typesByKey).forEach(([key, types]) => {
|
|
38
38
|
const schema = mergeTypes(
|
|
39
|
-
|
|
39
|
+
Array.from(types),
|
|
40
40
|
rows.map(r => r[key]),
|
|
41
41
|
)
|
|
42
42
|
if (!schema) return
|
|
@@ -644,7 +644,7 @@ export class JBuilder<OUT, Opt> extends JSchema<OUT, Opt> {
|
|
|
644
644
|
const innerSchema: JsonSchema = {
|
|
645
645
|
...(builtSchema.type ? { type: builtSchema.type } : {}),
|
|
646
646
|
anyOf: [builtSchema, alternativesSchema],
|
|
647
|
-
optionalValues:
|
|
647
|
+
optionalValues: optionalValues.slice(),
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
@@ -652,7 +652,7 @@ export class JBuilder<OUT, Opt> extends JSchema<OUT, Opt> {
|
|
|
652
652
|
// but the typing should not reflect that.
|
|
653
653
|
if (optionalValues.includes(null)) {
|
|
654
654
|
return new JSchema({
|
|
655
|
-
anyOf: [{ type: 'null', optionalValues:
|
|
655
|
+
anyOf: [{ type: 'null', optionalValues: optionalValues.slice() }, innerSchema],
|
|
656
656
|
optionalField: true,
|
|
657
657
|
}) as any
|
|
658
658
|
}
|
|
@@ -1598,9 +1598,7 @@ export class AjvSchema<OUT> {
|
|
|
1598
1598
|
private _compiledFn: any
|
|
1599
1599
|
|
|
1600
1600
|
private _getValidateFn(): any {
|
|
1601
|
-
|
|
1602
|
-
this._compiledFn = this.cfg.ajv.compile(this.schema as any)
|
|
1603
|
-
}
|
|
1601
|
+
this._compiledFn ||= this.cfg.ajv.compile(this.schema as any)
|
|
1604
1602
|
return this._compiledFn
|
|
1605
1603
|
}
|
|
1606
1604
|
|
|
@@ -1694,7 +1692,11 @@ function executeValidation<OUT>(
|
|
|
1694
1692
|
|
|
1695
1693
|
// Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
|
|
1696
1694
|
// .items[0].name, embedding the index into the segment and making it harder to strip without regex
|
|
1697
|
-
const fingerprint = buildAjvErrorFingerprint(
|
|
1695
|
+
const fingerprint = buildAjvErrorFingerprint(
|
|
1696
|
+
errors[0],
|
|
1697
|
+
inputName,
|
|
1698
|
+
resolveCustomErrorMessage(builtSchema, errors[0]),
|
|
1699
|
+
)
|
|
1698
1700
|
|
|
1699
1701
|
applyImprovementsOnErrorMessages(errors, builtSchema)
|
|
1700
1702
|
|
|
@@ -1731,34 +1733,51 @@ function applyImprovementsOnErrorMessages(
|
|
|
1731
1733
|
|
|
1732
1734
|
filterNullableAnyOfErrors(errors, schema)
|
|
1733
1735
|
|
|
1734
|
-
const { errorMessages } = schema
|
|
1735
|
-
|
|
1736
1736
|
for (const error of errors) {
|
|
1737
|
-
const
|
|
1738
|
-
|
|
1739
|
-
if (
|
|
1740
|
-
error.message =
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
1746
|
-
error.message = unwrapped.errorMessages[error.keyword]
|
|
1747
|
-
}
|
|
1737
|
+
const customMessage = resolveCustomErrorMessage(schema, error)
|
|
1738
|
+
|
|
1739
|
+
if (customMessage) {
|
|
1740
|
+
error.message = customMessage
|
|
1741
|
+
// A custom `msg` signals "the underlying rule param is an implementation detail".
|
|
1742
|
+
// Drop params so consumers (e.g. HTTP responses, Sentry payloads) don't surface
|
|
1743
|
+
// the raw regex/limit/etc. — the custom message is now the canonical rule label.
|
|
1744
|
+
error.params = {}
|
|
1748
1745
|
}
|
|
1749
1746
|
|
|
1750
1747
|
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.')
|
|
1751
1748
|
}
|
|
1752
1749
|
}
|
|
1753
1750
|
|
|
1751
|
+
/**
|
|
1752
|
+
* Looks up the user-provided custom error message (set via `{ msg }` / `{ name }`)
|
|
1753
|
+
* for a given AJV error, walking the schema along the error's instancePath and
|
|
1754
|
+
* falling back to top-level `errorMessages` (including through nullable wrappers).
|
|
1755
|
+
* Returns undefined if no custom message was registered for the failing keyword.
|
|
1756
|
+
*/
|
|
1757
|
+
function resolveCustomErrorMessage(schema: JsonSchema, error: ErrorObject): string | undefined {
|
|
1758
|
+
const byPath = getErrorMessageForInstancePath(schema, error.instancePath, error.keyword)
|
|
1759
|
+
if (byPath) return byPath
|
|
1760
|
+
if (schema.errorMessages?.[error.keyword]) return schema.errorMessages[error.keyword]
|
|
1761
|
+
const unwrapped = unwrapNullableAnyOf(schema)
|
|
1762
|
+
return unwrapped?.errorMessages?.[error.keyword]
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1754
1765
|
/**
|
|
1755
1766
|
* Groups repeated validation errors by rule rather than by unique request content.
|
|
1756
1767
|
* Excludes instance-specific data like record IDs and array indices.
|
|
1768
|
+
*
|
|
1769
|
+
* When a custom error message is registered for the failing rule, prefer it over
|
|
1770
|
+
* the raw param value (e.g. regex source) — it is both stable across regex tweaks
|
|
1771
|
+
* and avoids leaking the underlying pattern into Sentry fingerprints.
|
|
1757
1772
|
*/
|
|
1758
|
-
function buildAjvErrorFingerprint(
|
|
1759
|
-
|
|
1773
|
+
function buildAjvErrorFingerprint(
|
|
1774
|
+
e: ErrorObject,
|
|
1775
|
+
inputName: string,
|
|
1776
|
+
customMessage: string | undefined,
|
|
1777
|
+
): string {
|
|
1778
|
+
const ruleValue = customMessage ?? Object.values(e.params || {})[0]
|
|
1760
1779
|
let rule = e.keyword
|
|
1761
|
-
if (
|
|
1780
|
+
if (ruleValue !== undefined) rule += `:${ruleValue}`
|
|
1762
1781
|
const path = e.instancePath
|
|
1763
1782
|
.split('/')
|
|
1764
1783
|
.filter(s => s && isNaN(Number(s)))
|
|
@@ -1953,7 +1972,7 @@ function deepCopyPreservingFunctions<T>(obj: T): T {
|
|
|
1953
1972
|
// customValidations/customConversions are arrays of functions - shallow copy the array
|
|
1954
1973
|
;(copy as any)[key] =
|
|
1955
1974
|
(key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
|
|
1956
|
-
?
|
|
1975
|
+
? value.slice()
|
|
1957
1976
|
: deepCopyPreservingFunctions(value)
|
|
1958
1977
|
}
|
|
1959
1978
|
return copy
|