@naturalcycles/nodejs-lib 15.107.0 → 15.107.2

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.
@@ -117,7 +117,7 @@ export class SlackService {
117
117
  * Returns a CommonLogger implementation based on this SlackService instance.
118
118
  */
119
119
  getCommonLogger(opt) {
120
- const { minLogLevel = 'log', debugChannel, logChannel, warnChannel, errorChannel } = opt;
120
+ const { minLogLevel, debugChannel, logChannel, warnChannel, errorChannel } = opt;
121
121
  const defaultChannel = this.cfg.defaults?.channel || DEFAULTS.channel;
122
122
  const q = new PQueue({
123
123
  concurrency: 1,
@@ -1178,6 +1178,9 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1178
1178
  const errors = fn.errors;
1179
1179
  const { inputId = _isObject(input) ? input['id'] : undefined, inputName = defaultInputName || 'Object', } = opt;
1180
1180
  const dataVar = [inputName, inputId].filter(Boolean).join('.');
1181
+ // Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
1182
+ // .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
1184
  applyImprovementsOnErrorMessages(errors, builtSchema);
1182
1185
  let message = getAjv().errorsText(errors, {
1183
1186
  dataVar,
@@ -1192,6 +1195,7 @@ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1192
1195
  errors,
1193
1196
  inputName,
1194
1197
  inputId,
1198
+ fingerprint,
1195
1199
  }));
1196
1200
  return [err, output];
1197
1201
  }
@@ -1218,6 +1222,22 @@ function applyImprovementsOnErrorMessages(errors, schema) {
1218
1222
  error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
1219
1223
  }
1220
1224
  }
1225
+ /**
1226
+ * Groups repeated validation errors by rule rather than by unique request content.
1227
+ * Excludes instance-specific data like record IDs and array indices.
1228
+ */
1229
+ function buildAjvErrorFingerprint(e, inputName) {
1230
+ const value = Object.values(e.params || {})[0];
1231
+ let rule = e.keyword;
1232
+ if (value !== undefined)
1233
+ rule += `:${value}`;
1234
+ const path = e.instancePath
1235
+ .split('/')
1236
+ .filter(s => s && isNaN(Number(s)))
1237
+ .join('.');
1238
+ const location = [inputName, path].filter(Boolean).join('.');
1239
+ return [location, rule].join(' ');
1240
+ }
1221
1241
  /**
1222
1242
  * Filters out noisy errors produced by nullable anyOf patterns.
1223
1243
  * When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.107.0",
4
+ "version": "15.107.2",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -17,8 +17,8 @@
17
17
  "yargs": "^18"
18
18
  },
19
19
  "devDependencies": {
20
- "@typescript/native-preview": "7.0.0-dev.20260401.1",
21
- "@naturalcycles/dev-lib": "20.42.0"
20
+ "@typescript/native-preview": "7.0.0-dev.20260415.1",
21
+ "@naturalcycles/dev-lib": "18.4.2"
22
22
  },
23
23
  "exports": {
24
24
  ".": "./dist/index.js",
@@ -156,7 +156,7 @@ export class SlackService<CTX = any> {
156
156
  warnChannel?: string
157
157
  errorChannel?: string
158
158
  }): CommonLogger {
159
- const { minLogLevel = 'log', debugChannel, logChannel, warnChannel, errorChannel } = opt
159
+ const { minLogLevel, debugChannel, logChannel, warnChannel, errorChannel } = opt
160
160
  const defaultChannel = this.cfg.defaults?.channel || DEFAULTS.channel!
161
161
 
162
162
  const q = new PQueue({
@@ -1692,6 +1692,10 @@ function executeValidation<OUT>(
1692
1692
  } = opt
1693
1693
  const dataVar = [inputName, inputId].filter(Boolean).join('.')
1694
1694
 
1695
+ // Build fingerprint before applyImprovementsOnErrorMessages: after it, /items/0/name becomes
1696
+ // .items[0].name, embedding the index into the segment and making it harder to strip without regex
1697
+ const fingerprint = buildAjvErrorFingerprint(errors[0], inputName)
1698
+
1695
1699
  applyImprovementsOnErrorMessages(errors, builtSchema)
1696
1700
 
1697
1701
  let message = getAjv().errorsText(errors, {
@@ -1711,6 +1715,7 @@ function executeValidation<OUT>(
1711
1715
  errors,
1712
1716
  inputName,
1713
1717
  inputId,
1718
+ fingerprint,
1714
1719
  }),
1715
1720
  )
1716
1721
  return [err, output]
@@ -1719,7 +1724,7 @@ function executeValidation<OUT>(
1719
1724
  // ==== Error formatting helpers ====
1720
1725
 
1721
1726
  function applyImprovementsOnErrorMessages(
1722
- errors: ErrorObject<string, Record<string, any>, unknown>[] | null | undefined,
1727
+ errors: ErrorObject[] | null | undefined,
1723
1728
  schema: JsonSchema,
1724
1729
  ): void {
1725
1730
  if (!errors) return
@@ -1746,16 +1751,29 @@ function applyImprovementsOnErrorMessages(
1746
1751
  }
1747
1752
  }
1748
1753
 
1754
+ /**
1755
+ * Groups repeated validation errors by rule rather than by unique request content.
1756
+ * Excludes instance-specific data like record IDs and array indices.
1757
+ */
1758
+ function buildAjvErrorFingerprint(e: ErrorObject, inputName: string): string {
1759
+ const value = Object.values(e.params || {})[0]
1760
+ let rule = e.keyword
1761
+ if (value !== undefined) rule += `:${value}`
1762
+ const path = e.instancePath
1763
+ .split('/')
1764
+ .filter(s => s && isNaN(Number(s)))
1765
+ .join('.')
1766
+ const location = [inputName, path].filter(Boolean).join('.')
1767
+ return [location, rule].join(' ')
1768
+ }
1769
+
1749
1770
  /**
1750
1771
  * Filters out noisy errors produced by nullable anyOf patterns.
1751
1772
  * When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
1752
1773
  * AJV produces "must be null" and "must match a schema in anyOf" errors
1753
1774
  * that are confusing. This method splices them out, keeping only the real errors.
1754
1775
  */
1755
- function filterNullableAnyOfErrors(
1756
- errors: ErrorObject<string, Record<string, any>, unknown>[],
1757
- schema: JsonSchema,
1758
- ): void {
1776
+ function filterNullableAnyOfErrors(errors: ErrorObject[], schema: JsonSchema): void {
1759
1777
  // Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
1760
1778
  const exactPaths: string[] = []
1761
1779
  const nullBranchPrefixes: string[] = []