@sanity/validation 6.12.0-next.63 → 6.12.0-next.75

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.
@@ -2,18 +2,64 @@ import { isArrayOfBlocksSchemaType, isKeyedObject, isReference, isSlug, isTypedO
2
2
  import { createClientConcurrencyLimiter } from "@sanity/util/client";
3
3
  import { ConcurrencyLimiter } from "@sanity/util/concurrency-limiter";
4
4
  import flatten from "lodash-es/flatten.js";
5
- import uniqBy from "lodash-es/uniqBy.js";
5
+ import isEqual from "lodash-es/isEqual.js";
6
6
  import { Observable, Subject, bufferTime, concat, defer, filter, finalize, firstValueFrom, from, lastValueFrom, map, merge, mergeMap, of, share, switchMap } from "rxjs";
7
7
  import { catchError, map as map$1, mergeAll, mergeMap as mergeMap$1, switchMap as switchMap$1, toArray } from "rxjs/operators";
8
8
  import { createInstance } from "i18next";
9
- import isEqual from "lodash-es/isEqual.js";
10
9
  import { Rule as Rule$1 } from "@sanity/schema";
11
10
  import get from "lodash-es/get.js";
12
11
  import isPlainObject from "lodash-es/isPlainObject.js";
13
12
  import * as legacyDateFormat from "@sanity/util/legacyDateFormat";
14
13
  import { getPublishedId } from "@sanity/id-utils";
15
14
  import memoize from "lodash-es/memoize.js";
16
- const validationLocaleStrings = {
15
+ /** Machine-readable codes emitted by built-in document validation. @beta */
16
+ const validationMarkerCodes = {
17
+ arrayDuplicateItem: "array.duplicate-item",
18
+ arrayExactLength: "array.exact-length",
19
+ arrayMaximumLength: "array.maximum-length",
20
+ arrayMinimumLength: "array.minimum-length",
21
+ assetRequired: "asset.required",
22
+ custom: "custom",
23
+ dateInvalidFormat: "date.invalid-format",
24
+ dateMaximum: "date.maximum",
25
+ dateMinimum: "date.minimum",
26
+ documentUnknownType: "document.unknown-type",
27
+ mediaCustom: "media.custom",
28
+ mediaInvalidReference: "media.invalid-reference",
29
+ mediaNotFound: "media.not-found",
30
+ numberGreaterThan: "number.greater-than",
31
+ numberInteger: "number.integer",
32
+ numberLessThan: "number.less-than",
33
+ numberMaximum: "number.maximum",
34
+ numberMinimum: "number.minimum",
35
+ numberPrecision: "number.precision",
36
+ objectUnknownField: "object.unknown-field",
37
+ referenceInvalid: "reference.invalid",
38
+ referenceNotPublished: "reference.not-published",
39
+ ruleAllFailed: "rule.all-failed",
40
+ ruleEitherFailed: "rule.either-failed",
41
+ slugInvalidType: "slug.invalid-type",
42
+ slugMissingCurrent: "slug.missing-current",
43
+ slugNotUnique: "slug.not-unique",
44
+ stringEmail: "string.email",
45
+ stringExactLength: "string.exact-length",
46
+ stringLowercase: "string.lowercase",
47
+ stringMaximumLength: "string.maximum-length",
48
+ stringMinimumLength: "string.minimum-length",
49
+ stringRegexMatch: "string.regex-match",
50
+ stringRegexMismatch: "string.regex-mismatch",
51
+ stringUppercase: "string.uppercase",
52
+ stringUrlCredentialsNotAllowed: "string.url.credentials-not-allowed",
53
+ stringUrlInvalid: "string.url.invalid",
54
+ stringUrlNotAbsolute: "string.url.not-absolute",
55
+ stringUrlNotRelative: "string.url.not-relative",
56
+ stringUrlSchemeNotAllowed: "string.url.scheme-not-allowed",
57
+ validationException: "validation.exception",
58
+ validationFailed: "validation.failed",
59
+ valueNotAllowed: "value.not-allowed",
60
+ valueRequired: "value.required",
61
+ valueTypeMismatch: "value.type-mismatch"
62
+ }, validationLocaleStrings = {
17
63
  "array.exact-length": "Must have exactly {{wantedLength}} items",
18
64
  "array.exact-length_blocks": "Must have exactly {{wantedLength}} blocks",
19
65
  "array.item-duplicate": "Can't be a duplicate",
@@ -121,25 +167,26 @@ function pathToString(path = []) {
121
167
  throw Error(`Unsupported path segment "${segment}"`);
122
168
  }, "");
123
169
  }
124
- function isNonNullable(t) {
125
- return t !== null || t !== void 0;
126
- }
127
- function convertToValidationMarker(validatorResult, level, context) {
170
+ function convertToValidationMarker(validatorResult, level, context, fallback = { code: validationMarkerCodes.validationFailed }) {
128
171
  if (!context) throw Error("missing context");
129
172
  if (validatorResult === !0) return [];
130
- if (Array.isArray(validatorResult)) return validatorResult.flatMap((child) => convertToValidationMarker(child, level, context)).filter(isNonNullable);
131
- if (typeof validatorResult == "string") return convertToValidationMarker({ message: validatorResult }, level, context);
173
+ if (Array.isArray(validatorResult)) return validatorResult.flatMap((child) => convertToValidationMarker(child, level, context, fallback));
174
+ if (typeof validatorResult == "string") return convertToValidationMarker({ message: validatorResult }, level, context, fallback);
132
175
  if (typeof validatorResult.message != "string") throw Error(`${pathToString(context.path)}: Validator must return 'true' if valid or an error message as a string on errors`);
133
- let { message, __internal_metadata } = validatorResult, normalizedPaths = [];
176
+ let { message, __internal_metadata } = validatorResult, code = validatorResult.code || fallback.code, details = validatorResult.details || fallback.details, normalizedPaths = [];
134
177
  validatorResult.path && normalizedPaths.push(validatorResult.path);
135
178
  for (let path of validatorResult.paths || []) normalizedPaths.push(path);
136
179
  return normalizedPaths.length ? normalizedPaths.map((path) => ({
180
+ code,
181
+ ...details && { details },
137
182
  path: (context.path || []).concat(path),
138
183
  level: level || "error",
139
184
  item: { message },
140
185
  message,
141
186
  __internal_metadata
142
187
  })) : [{
188
+ code,
189
+ ...details && { details },
143
190
  level: level || "error",
144
191
  item: { message },
145
192
  message,
@@ -223,21 +270,43 @@ function typeString(obj) {
223
270
  let constructorType = obj.constructor;
224
271
  return constructorType && !isBuiltIn(constructorType) ? constructorType.name : stringType;
225
272
  }
226
- const SLOW_VALIDATOR_TIMEOUT = 5e3, formatValidationErrors = (options) => options.message ? options.message : options.results.length === 1 ? options.results[0]?.message : options.i18n.t("{{messages, list}}", {
227
- messages: options.results.map((err) => err.message || err.item?.message),
228
- formatParams: { messages: {
229
- style: "long",
230
- type: options.operation
231
- } }
232
- }), genericValidators = {
273
+ const SLOW_VALIDATOR_TIMEOUT = 5e3, formatValidationErrors = (options) => {
274
+ let message = options.message || (options.results.length === 1 ? options.results[0]?.message : options.i18n.t("{{messages, list}}", {
275
+ messages: options.results.map((err) => err.message || err.item?.message),
276
+ formatParams: { messages: {
277
+ style: "long",
278
+ type: options.operation
279
+ } }
280
+ }));
281
+ return {
282
+ code: options.operation === "conjunction" ? validationMarkerCodes.ruleAllFailed : validationMarkerCodes.ruleEitherFailed,
283
+ details: { causes: options.results.map(({ code, details, item, message: causeMessage, path }) => ({
284
+ code: code || validationMarkerCodes.validationFailed,
285
+ details,
286
+ message: causeMessage || item?.message,
287
+ path
288
+ })) },
289
+ message: message || "Validation failed"
290
+ };
291
+ }, genericValidators = {
233
292
  type: (expectedType, value, message, { i18n }) => {
234
293
  let actualType = typeString(value);
235
- return actualType !== expectedType && actualType !== "undefined" ? message || i18n.t("validation:generic.incorrect-type", {
236
- actualType,
237
- expectedType
238
- }) : !0;
294
+ return actualType !== expectedType && actualType !== "undefined" ? {
295
+ code: validationMarkerCodes.valueTypeMismatch,
296
+ details: {
297
+ actualType,
298
+ expectedType
299
+ },
300
+ message: message || i18n.t("validation:generic.incorrect-type", {
301
+ actualType,
302
+ expectedType
303
+ })
304
+ } : !0;
239
305
  },
240
- presence: (expected, value, message, { i18n }) => value === void 0 && expected === "required" ? message || i18n.t("validation:generic.required") : !0,
306
+ presence: (expected, value, message, { i18n }) => value === void 0 && expected === "required" ? {
307
+ code: validationMarkerCodes.valueRequired,
308
+ message: message || i18n.t("validation:generic.required")
309
+ } : !0,
241
310
  all: async (children, value, message, context) => {
242
311
  let results = (await Promise.all(children.map((child) => child.validate(value, context)))).flat();
243
312
  return results.length === 0 || formatValidationErrors({
@@ -260,10 +329,14 @@ const SLOW_VALIDATOR_TIMEOUT = 5e3, formatValidationErrors = (options) => option
260
329
  let valueType = typeof actual;
261
330
  if (valueType === "undefined") return !0;
262
331
  let value = (valueType === "number" || valueType === "string") && `${actual}`, strValue = value && value.length > 30 ? `${value.slice(0, 30)}…` : value;
263
- return allowedValues.some((expected) => deepEqualsIgnoreKey(expected, actual)) ? !0 : message || i18n.t("validation:generic.not-allowed", value ? {
264
- context: "hint",
265
- replace: { hint: strValue }
266
- } : {});
332
+ return allowedValues.some((expected) => deepEqualsIgnoreKey(expected, actual)) ? !0 : {
333
+ code: validationMarkerCodes.valueNotAllowed,
334
+ details: { allowedValuesCount: allowedValues.length },
335
+ message: message || i18n.t("validation:generic.not-allowed", value ? {
336
+ context: "hint",
337
+ replace: { hint: strValue }
338
+ } : {})
339
+ };
267
340
  },
268
341
  custom: async (fn, value, message, context) => {
269
342
  let slowTimer = setTimeout(() => {
@@ -281,28 +354,52 @@ const SLOW_VALIDATOR_TIMEOUT = 5e3, formatValidationErrors = (options) => option
281
354
  min: (minLength, value, message, { i18n, type }) => {
282
355
  if (!value || value.length >= minLength) return !0;
283
356
  let context = isArrayOfBlocksSchemaType(type) ? "blocks" : void 0;
284
- return message || i18n.t("validation:array.minimum-length", {
285
- minLength,
286
- context
287
- });
357
+ return {
358
+ code: validationMarkerCodes.arrayMinimumLength,
359
+ details: {
360
+ actualLength: value.length,
361
+ minimumLength: minLength
362
+ },
363
+ message: message || i18n.t("validation:array.minimum-length", {
364
+ minLength,
365
+ context
366
+ })
367
+ };
288
368
  },
289
369
  max: (maxLength, value, message, { i18n, type }) => {
290
370
  if (!value || value.length <= maxLength) return !0;
291
371
  let context = isArrayOfBlocksSchemaType(type) ? "blocks" : void 0;
292
- return message || i18n.t("validation:array.maximum-length", {
293
- maxLength,
294
- context
295
- });
372
+ return {
373
+ code: validationMarkerCodes.arrayMaximumLength,
374
+ details: {
375
+ actualLength: value.length,
376
+ maximumLength: maxLength
377
+ },
378
+ message: message || i18n.t("validation:array.maximum-length", {
379
+ maxLength,
380
+ context
381
+ })
382
+ };
296
383
  },
297
384
  length: (wantedLength, value, message, { i18n, type }) => {
298
385
  if (!value || value.length === wantedLength) return !0;
299
386
  let context = isArrayOfBlocksSchemaType(type) ? "blocks" : void 0;
300
- return message || i18n.t("validation:array.exact-length", {
301
- wantedLength,
302
- context
303
- });
387
+ return {
388
+ code: validationMarkerCodes.arrayExactLength,
389
+ details: {
390
+ actualLength: value.length,
391
+ expectedLength: wantedLength
392
+ },
393
+ message: message || i18n.t("validation:array.exact-length", {
394
+ wantedLength,
395
+ context
396
+ })
397
+ };
304
398
  },
305
- presence: (flag, value, message, { i18n }) => flag === "required" && !value ? message || i18n.t("validation:generic.required", { context: "array" }) : !0,
399
+ presence: (flag, value, message, { i18n }) => flag === "required" && !value ? {
400
+ code: validationMarkerCodes.valueRequired,
401
+ message: message || i18n.t("validation:generic.required", { context: "array" })
402
+ } : !0,
306
403
  valid: (allowedValues, values, message, { i18n }) => {
307
404
  if (values === void 0) return !0;
308
405
  let paths = [];
@@ -314,6 +411,8 @@ const SLOW_VALIDATOR_TIMEOUT = 5e3, formatValidationErrors = (options) => option
314
411
  }
315
412
  let sharedMessage = message || i18n.t("validation:generic.not-allowed");
316
413
  return paths.map((path) => ({
414
+ code: validationMarkerCodes.valueNotAllowed,
415
+ details: { allowedValuesCount: allowedValues.length },
317
416
  message: sharedMessage,
318
417
  path
319
418
  }));
@@ -330,13 +429,17 @@ const SLOW_VALIDATOR_TIMEOUT = 5e3, formatValidationErrors = (options) => option
330
429
  return [item && item._key ? { _key: item._key } : idx];
331
430
  }), sharedMessage = message || i18n.t("validation:array.item-duplicate");
332
431
  return paths.map((path) => ({
432
+ code: validationMarkerCodes.arrayDuplicateItem,
333
433
  message: sharedMessage,
334
434
  path
335
435
  }));
336
436
  }
337
437
  }, booleanValidators = {
338
438
  ...genericValidators,
339
- presence: (flag, value, message, { i18n }) => flag === "required" && typeof value != "boolean" ? message || i18n.t("validation:generic.required", { context: "boolean" }) : !0
439
+ presence: (flag, value, message, { i18n }) => flag === "required" && typeof value != "boolean" ? {
440
+ code: validationMarkerCodes.valueRequired,
441
+ message: message || i18n.t("validation:generic.required", { context: "boolean" })
442
+ } : !0
340
443
  };
341
444
  function isRecord$1(obj) {
342
445
  return typeof obj == "object" && !!obj && !Array.isArray(obj);
@@ -354,39 +457,96 @@ function parseDate(date, throwOnError = !1) {
354
457
  }
355
458
  const dateValidators = {
356
459
  ...genericValidators,
357
- type: (_unused, value, message, { i18n }) => value === void 0 || isoDate.test(`${value}`) ? !0 : message || i18n.t("validation:date.invalid-format"),
460
+ type: (_unused, value, message, { i18n }) => value === void 0 || isoDate.test(`${value}`) ? !0 : {
461
+ code: validationMarkerCodes.dateInvalidFormat,
462
+ details: { actualValue: value },
463
+ message: message || i18n.t("validation:date.invalid-format")
464
+ },
358
465
  min: (minDate, value, message, { type, i18n }) => {
359
466
  let dateVal = parseDate(value), minDateVal = parseDate(minDate, !0);
360
467
  if (!dateVal || !value || dateVal >= minDateVal) return !0;
361
468
  if (!type) throw Error("`type` was not provided in validation context.");
362
469
  let dateTimeOptions = isRecord$1(type.options) ? type.options : {};
363
- return message || i18n.t("validation:date.minimum", {
364
- minDate: getFormattedDate(type.name, minDateVal, dateTimeOptions),
365
- providedMinDate: minDate
366
- });
470
+ return {
471
+ code: validationMarkerCodes.dateMinimum,
472
+ details: {
473
+ actualValue: value,
474
+ minimum: minDate
475
+ },
476
+ message: message || i18n.t("validation:date.minimum", {
477
+ minDate: getFormattedDate(type.name, minDateVal, dateTimeOptions),
478
+ providedMinDate: minDate
479
+ })
480
+ };
367
481
  },
368
482
  max: (maxDate, value, message, { type, i18n }) => {
369
483
  let dateVal = parseDate(value), maxDateVal = parseDate(maxDate, !0);
370
484
  if (!dateVal || !value || dateVal <= maxDateVal) return !0;
371
485
  if (!type) throw Error("`type` was not provided in validation context.");
372
486
  let dateTimeOptions = isRecord$1(type.options) ? type.options : {};
373
- return message || i18n.t("validation:date.maximum", {
374
- maxDate: getFormattedDate(type.name, maxDateVal, dateTimeOptions),
375
- providedMaxDate: maxDate
376
- });
487
+ return {
488
+ code: validationMarkerCodes.dateMaximum,
489
+ details: {
490
+ actualValue: value,
491
+ maximum: maxDate
492
+ },
493
+ message: message || i18n.t("validation:date.maximum", {
494
+ maxDate: getFormattedDate(type.name, maxDateVal, dateTimeOptions),
495
+ providedMaxDate: maxDate
496
+ })
497
+ };
377
498
  }
378
499
  }, precisionRx = /(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/, numberValidators = {
379
500
  ...genericValidators,
380
- integer: (_unused, value, message, { i18n }) => Number.isInteger(value) ? !0 : message || i18n.t("validation:number.non-integer"),
501
+ integer: (_unused, value, message, { i18n }) => Number.isInteger(value) ? !0 : {
502
+ code: validationMarkerCodes.numberInteger,
503
+ details: { actualValue: value },
504
+ message: message || i18n.t("validation:number.non-integer")
505
+ },
381
506
  precision: (limit, value, message, { i18n }) => {
382
507
  if (value === void 0) return !0;
383
- let places = value.toString().match(precisionRx);
384
- return Math.max((places[1] ? places[1].length : 0) - (places[2] ? parseInt(places[2], 10) : 0), 0) > limit ? message || i18n.t("validation:number.maximum-precision", { limit }) : !0;
508
+ let places = value.toString().match(precisionRx), decimals = Math.max((places[1] ? places[1].length : 0) - (places[2] ? parseInt(places[2], 10) : 0), 0);
509
+ return decimals > limit ? {
510
+ code: validationMarkerCodes.numberPrecision,
511
+ details: {
512
+ actualPrecision: decimals,
513
+ maximumPrecision: limit
514
+ },
515
+ message: message || i18n.t("validation:number.maximum-precision", { limit })
516
+ } : !0;
385
517
  },
386
- min: (minNumber, value, message, { i18n }) => value >= minNumber || message || i18n.t("validation:number.minimum", { minNumber }),
387
- max: (maxNumber, value, message, { i18n }) => value <= maxNumber || message || i18n.t("validation:number.maximum", { maxNumber }),
388
- greaterThan: (threshold, value, message, { i18n }) => value > threshold || message || i18n.t("validation:number.greater-than", { threshold }),
389
- lessThan: (threshold, value, message, { i18n }) => value < threshold || message || i18n.t("validation:number.less-than", { threshold })
518
+ min: (minNumber, value, message, { i18n }) => value >= minNumber || {
519
+ code: validationMarkerCodes.numberMinimum,
520
+ details: {
521
+ actualValue: value,
522
+ minimum: minNumber
523
+ },
524
+ message: message || i18n.t("validation:number.minimum", { minNumber })
525
+ },
526
+ max: (maxNumber, value, message, { i18n }) => value <= maxNumber || {
527
+ code: validationMarkerCodes.numberMaximum,
528
+ details: {
529
+ actualValue: value,
530
+ maximum: maxNumber
531
+ },
532
+ message: message || i18n.t("validation:number.maximum", { maxNumber })
533
+ },
534
+ greaterThan: (threshold, value, message, { i18n }) => value > threshold || {
535
+ code: validationMarkerCodes.numberGreaterThan,
536
+ details: {
537
+ actualValue: value,
538
+ threshold
539
+ },
540
+ message: message || i18n.t("validation:number.greater-than", { threshold })
541
+ },
542
+ lessThan: (threshold, value, message, { i18n }) => value < threshold || {
543
+ code: validationMarkerCodes.numberLessThan,
544
+ details: {
545
+ actualValue: value,
546
+ threshold
547
+ },
548
+ message: message || i18n.t("validation:number.less-than", { threshold })
549
+ }
390
550
  }, metaKeys = [
391
551
  "_key",
392
552
  "_type",
@@ -396,20 +556,33 @@ const dateValidators = {
396
556
  presence: (expected, value, message, { i18n }) => {
397
557
  if (expected !== "required") return !0;
398
558
  let keys = value && Object.keys(value).filter((key) => !metaKeys.includes(key));
399
- return value === void 0 || keys && keys.length === 0 ? message || i18n.t("validation:generic.required", { context: "object" }) : !0;
559
+ return value === void 0 || keys && keys.length === 0 ? {
560
+ code: validationMarkerCodes.valueRequired,
561
+ message: message || i18n.t("validation:generic.required", { context: "object" })
562
+ } : !0;
400
563
  },
401
564
  reference: async (_unused, value, message, context) => {
402
565
  if (!value) return !0;
403
566
  let { type, document, getDocumentExists, i18n } = context;
404
- if (!isReference(value)) return message || i18n.t("validation:object.not-reference");
567
+ if (!isReference(value)) return {
568
+ code: validationMarkerCodes.referenceInvalid,
569
+ details: { actualType: typeString(value) },
570
+ message: message || i18n.t("validation:object.not-reference")
571
+ };
405
572
  if (!type) throw Error("`type` was not provided in validation context");
406
573
  if ("weak" in type && type.weak) return !0;
407
574
  if (!getDocumentExists) throw Error("`getDocumentExists` was not provided in validation context");
408
575
  let documentId = document?._id;
409
- return documentId && value._ref == getPublishedId(documentId) || await getDocumentExists({ id: value._ref }) ? !0 : i18n.t("validation:object.reference-not-published", { documentId: value._ref });
576
+ return documentId && value._ref == getPublishedId(documentId) || await getDocumentExists({ id: value._ref }) ? !0 : {
577
+ code: validationMarkerCodes.referenceNotPublished,
578
+ details: { referenceId: value._ref },
579
+ message: i18n.t("validation:object.reference-not-published", { documentId: value._ref })
580
+ };
410
581
  },
411
582
  assetRequired: (flag, value, message, { i18n }) => !value || !value.asset || !value.asset._ref ? {
412
583
  __internal_metadata: { name: "assetRequired" },
584
+ code: validationMarkerCodes.assetRequired,
585
+ details: { assetType: flag.assetType },
413
586
  message: message || i18n.t("validation:object.asset-required", { context: flag.assetType || "" })
414
587
  } : !0,
415
588
  media: async (fn, value, message, context) => {
@@ -417,14 +590,21 @@ const dateValidators = {
417
590
  context.environment === "studio" && console.warn(`Media validator at ${pathToString(context.path)} has taken more than ${SLOW_VALIDATOR_TIMEOUT}ms to respond`);
418
591
  }, SLOW_VALIDATOR_TIMEOUT);
419
592
  if (!value) return !0;
420
- if (!value || !value.media || !value.media._ref) return context.i18n.t("validation:object.not-media-library-asset");
593
+ if (!value || !value.media || !value.media._ref) return {
594
+ code: validationMarkerCodes.mediaInvalidReference,
595
+ message: context.i18n.t("validation:object.not-media-library-asset")
596
+ };
421
597
  let result = !0;
422
598
  try {
423
599
  let [type, libraryId, documentId] = value.media._ref.split(":", 3), resourceConfig = { resource: {
424
600
  type,
425
601
  id: libraryId
426
602
  } }, asset = await context.getClient({ apiVersion: "2025-02-19" }).withConfig(resourceConfig).fetch("*[_id == $id] { ..., 'currentVersion': @.currentVersion-> { ... } }[0]", { id: documentId });
427
- if (!asset) return console.warn(`${context.i18n.t("validation:object.media-not-found")}\nAsset ID: ${value.media._ref}`), context.i18n.t("validation:object.media-not-found");
603
+ if (!asset) return console.warn(`${context.i18n.t("validation:object.media-not-found")}\nAsset ID: ${value.media._ref}`), {
604
+ code: validationMarkerCodes.mediaNotFound,
605
+ details: { referenceId: value.media._ref },
606
+ message: context.i18n.t("validation:object.media-not-found")
607
+ };
428
608
  result = await fn({
429
609
  media: { asset },
430
610
  value
@@ -452,9 +632,33 @@ const dateValidators = {
452
632
  Number: numberValidators,
453
633
  String: {
454
634
  ...genericValidators,
455
- min: (minLength, value, message, { i18n }) => !value || value.length >= minLength || message || i18n.t("validation:string.minimum-length", { minLength }),
456
- max: (maxLength, value, message, { i18n }) => !value || value.length <= maxLength || message || i18n.t("validation:string.maximum-length", { maxLength }),
457
- length: (wantedLength, value, message, { i18n }) => (value || "").length === wantedLength || message || i18n.t("validation:string.exact-length", { wantedLength }),
635
+ min: (minLength, value, message, { i18n }) => !value || value.length >= minLength || {
636
+ code: validationMarkerCodes.stringMinimumLength,
637
+ details: {
638
+ actualLength: value.length,
639
+ minimumLength: minLength
640
+ },
641
+ message: message || i18n.t("validation:string.minimum-length", { minLength })
642
+ },
643
+ max: (maxLength, value, message, { i18n }) => !value || value.length <= maxLength || {
644
+ code: validationMarkerCodes.stringMaximumLength,
645
+ details: {
646
+ actualLength: value.length,
647
+ maximumLength: maxLength
648
+ },
649
+ message: message || i18n.t("validation:string.maximum-length", { maxLength })
650
+ },
651
+ length: (wantedLength, value, message, { i18n }) => {
652
+ let strValue = value || "";
653
+ return strValue.length === wantedLength || {
654
+ code: validationMarkerCodes.stringExactLength,
655
+ details: {
656
+ actualLength: strValue.length,
657
+ expectedLength: wantedLength
658
+ },
659
+ message: message || i18n.t("validation:string.exact-length", { wantedLength })
660
+ };
661
+ },
458
662
  uri: (constraints, value, message, { i18n }) => {
459
663
  let strValue = value || "";
460
664
  if (!strValue) return !0;
@@ -462,34 +666,70 @@ const dateValidators = {
462
666
  try {
463
667
  url = allowRelative ? new URL(strValue, DUMMY_ORIGIN) : new URL(strValue);
464
668
  } catch {
465
- return message || i18n.t("validation:string.url.invalid");
669
+ return {
670
+ code: validationMarkerCodes.stringUrlInvalid,
671
+ message: message || i18n.t("validation:string.url.invalid")
672
+ };
466
673
  }
467
- if (relativeOnly && url.origin !== DUMMY_ORIGIN) return message || i18n.t("validation:string.url.not-relative");
468
- if (!allowRelative && url.origin === DUMMY_ORIGIN && isRelativeUrl(strValue)) return message || i18n.t("validation:string.url.not-absolute");
469
- if (!allowCredentials && (url.username || url.password)) return message || i18n.t("validation:string.url.includes-credentials");
674
+ if (relativeOnly && url.origin !== DUMMY_ORIGIN) return {
675
+ code: validationMarkerCodes.stringUrlNotRelative,
676
+ message: message || i18n.t("validation:string.url.not-relative")
677
+ };
678
+ if (!allowRelative && url.origin === DUMMY_ORIGIN && isRelativeUrl(strValue)) return {
679
+ code: validationMarkerCodes.stringUrlNotAbsolute,
680
+ message: message || i18n.t("validation:string.url.not-absolute")
681
+ };
682
+ if (!allowCredentials && (url.username || url.password)) return {
683
+ code: validationMarkerCodes.stringUrlCredentialsNotAllowed,
684
+ message: message || i18n.t("validation:string.url.includes-credentials")
685
+ };
470
686
  let urlScheme = url.protocol.replace(/:$/, "");
471
- return isRelativeUrl(strValue) || options.scheme.some((scheme) => scheme.test(urlScheme)) ? !0 : message || i18n.t("validation:string.url.disallowed-scheme", { scheme: urlScheme });
687
+ return isRelativeUrl(strValue) || options.scheme.some((scheme) => scheme.test(urlScheme)) ? !0 : {
688
+ code: validationMarkerCodes.stringUrlSchemeNotAllowed,
689
+ details: { scheme: urlScheme },
690
+ message: message || i18n.t("validation:string.url.disallowed-scheme", { scheme: urlScheme })
691
+ };
472
692
  },
473
693
  stringCasing: (casing, value, message, { i18n }) => {
474
694
  let strValue = value || "";
475
- return casing === "uppercase" && strValue !== strValue.toLocaleUpperCase() ? message || i18n.t("validation:string.uppercase") : casing === "lowercase" && strValue !== strValue.toLocaleLowerCase() ? message || i18n.t("validation:string.lowercase") : !0;
695
+ return casing === "uppercase" && strValue !== strValue.toLocaleUpperCase() ? {
696
+ code: validationMarkerCodes.stringUppercase,
697
+ message: message || i18n.t("validation:string.uppercase")
698
+ } : casing === "lowercase" && strValue !== strValue.toLocaleLowerCase() ? {
699
+ code: validationMarkerCodes.stringLowercase,
700
+ message: message || i18n.t("validation:string.lowercase")
701
+ } : !0;
476
702
  },
477
- presence: (flag, value, message, { i18n }) => flag === "required" && !value ? message || i18n.t("validation:generic.required", { context: "string" }) : !0,
703
+ presence: (flag, value, message, { i18n }) => flag === "required" && !value ? {
704
+ code: validationMarkerCodes.valueRequired,
705
+ message: message || i18n.t("validation:generic.required", { context: "string" })
706
+ } : !0,
478
707
  regex: (options, value, message, { i18n }) => {
479
708
  let { pattern, name, invert } = options, regName = name || `${pattern.toString()}`, strValue = value || "";
480
709
  pattern.lastIndex = 0;
481
710
  let matches = pattern.test(strValue);
482
- return !invert && !matches || invert && matches ? message || (invert ? i18n.t("validation:string.regex-match", { name: regName }) : i18n.t("validation:string.regex-does-not-match", { name: regName })) : !0;
711
+ return !invert && !matches || invert && matches ? {
712
+ code: invert ? validationMarkerCodes.stringRegexMatch : validationMarkerCodes.stringRegexMismatch,
713
+ details: {
714
+ invert,
715
+ name: regName,
716
+ pattern: pattern.source
717
+ },
718
+ message: message || (invert ? i18n.t("validation:string.regex-match", { name: regName }) : i18n.t("validation:string.regex-does-not-match", { name: regName }))
719
+ } : !0;
483
720
  },
484
721
  email: (_unused, value, message, { i18n }) => {
485
722
  let strValue = `${value || ""}`.trim();
486
- return !strValue || emailRegex.test(strValue) ? !0 : message || i18n.t("validation:string.email");
723
+ return !strValue || emailRegex.test(strValue) ? !0 : {
724
+ code: validationMarkerCodes.stringEmail,
725
+ message: message || i18n.t("validation:string.email")
726
+ };
487
727
  }
488
728
  },
489
729
  Array: arrayValidators,
490
730
  Object: objectValidators,
491
731
  Date: dateValidators
492
- }, isFieldRef = (constraint) => typeof constraint != "object" || !constraint ? !1 : constraint.type === Rule$2.FIELD_REF, EMPTY_ARRAY = [], Rule$2 = class Rule extends Rule$1 {
732
+ }, isFieldRef = (constraint) => typeof constraint != "object" || !constraint ? !1 : constraint.type === Rule$2.FIELD_REF, EMPTY_ARRAY = [], fallbackCodeForRule = (flag) => flag === "custom" ? validationMarkerCodes.custom : flag === "media" ? validationMarkerCodes.mediaCustom : flag === "all" ? validationMarkerCodes.ruleAllFailed : flag === "either" ? validationMarkerCodes.ruleEitherFailed : validationMarkerCodes.validationFailed, Rule$2 = class Rule extends Rule$1 {
493
733
  static array = (def) => new Rule(def).type("Array");
494
734
  static object = (def) => new Rule(def).type("Object");
495
735
  static string = (def) => new Rule(def).type("String");
@@ -525,9 +765,13 @@ const dateValidators = {
525
765
  }
526
766
  let message = isLocalizedMessages(this._message) ? localizeMessage(this._message, context.i18n) : this._message;
527
767
  try {
528
- return convertToValidationMarker(await validator(specConstraint, value, message, context), this._level, context);
768
+ return convertToValidationMarker(await validator(specConstraint, value, message, context), this._level, context, { code: fallbackCodeForRule(curr.flag) });
529
769
  } catch (err) {
530
- return convertToValidationMarker({ message: `${pathToString(context.path)}: Exception occurred while validating value: ${err.message}` }, "error", context);
770
+ let errorMessage = `${pathToString(context.path)}: Exception occurred while validating value: ${err.message}`;
771
+ return convertToValidationMarker({
772
+ code: validationMarkerCodes.validationException,
773
+ message: errorMessage
774
+ }, "error", context);
531
775
  }
532
776
  }))).flat();
533
777
  }
@@ -571,15 +815,26 @@ function warnOnArraySlug(serializedPath) {
571
815
  const slugValidator = async (value, context) => {
572
816
  if (!value) return !0;
573
817
  let { i18n } = context;
574
- if (typeof value != "object" || Array.isArray(value)) return i18n.t("validation:slug.not-object");
575
- if (!isSlug(value) || value.current.trim().length === 0) return i18n.t("validation:slug.missing-current");
818
+ if (typeof value != "object" || Array.isArray(value)) return {
819
+ code: validationMarkerCodes.slugInvalidType,
820
+ details: { actualType: typeString(value) },
821
+ message: i18n.t("validation:slug.not-object")
822
+ };
823
+ if (!isSlug(value) || value.current.trim().length === 0) return {
824
+ code: validationMarkerCodes.slugMissingCurrent,
825
+ message: i18n.t("validation:slug.missing-current")
826
+ };
576
827
  let isUnique = context?.type?.options?.isUnique || defaultIsUnique, slugContext = {
577
828
  ...context,
578
829
  parent: context.parent,
579
830
  type: context.type,
580
831
  defaultIsUnique
581
832
  };
582
- return await isUnique(value.current, slugContext) ? !0 : i18n.t("validation:slug.not-unique", { slug: value.current });
833
+ return await isUnique(value.current, slugContext) ? !0 : {
834
+ code: validationMarkerCodes.slugNotUnique,
835
+ details: { slug: value.current },
836
+ message: i18n.t("validation:slug.not-unique", { slug: value.current })
837
+ };
583
838
  }, ruleConstraintTypes = {
584
839
  array: !0,
585
840
  boolean: !0,
@@ -648,6 +903,11 @@ const requestIdleCallbackShim = function requestIdleCallbackShim(callback, _opti
648
903
  if (typeof value != "object" || !value) return !0;
649
904
  let fieldNames = new Set(type.fields?.map((field) => field.name));
650
905
  return Object.keys(value).filter((key) => !key.startsWith("_")).filter((key) => !fieldNames.has(key)).map((unknownField) => ({
906
+ code: validationMarkerCodes.objectUnknownField,
907
+ details: {
908
+ fieldName: unknownField,
909
+ typeName: type.name
910
+ },
651
911
  message: `Field '${unknownField}' does not exist on type '${type.name}'`,
652
912
  path: [unknownField]
653
913
  }));
@@ -727,6 +987,8 @@ function validateDocumentObservable({ document, getClient, i18n = getFallbackLoc
727
987
  if (typeof document?._type != "string") throw Error("Tried to validate a value without a '_type'");
728
988
  let documentType = schema.get(document._type);
729
989
  if (!documentType) return environment === "studio" ? (console.warn("Schema type for object type \"%s\" not found, skipping validation", document._type), of([])) : of([{
990
+ code: validationMarkerCodes.documentUnknownType,
991
+ details: { documentType: document._type },
730
992
  level: "warning",
731
993
  message: `Could not find schema type for type '${document._type}', skipping validation`,
732
994
  path: []
@@ -747,15 +1009,16 @@ function validateDocumentObservable({ document, getClient, i18n = getFallbackLoc
747
1009
  customValidationConcurrencyLimiter,
748
1010
  currentUser
749
1011
  };
750
- return from(i18n.loadNamespaces(["validation"])).pipe(switchMap$1(() => validateItemObservable(validationOptions)), catchError((err) => {
1012
+ return from(i18n.loadNamespaces(["validation"])).pipe(switchMap$1(() => validateItemObservable(validationOptions)), map$1((markers) => markers.map(toDocumentValidationMarker)), catchError((err) => {
751
1013
  console.error(err);
752
- let message = err?.message || "Unknown error";
753
- return of([{
1014
+ let message = err?.message || "Unknown error", errorMarker = {
1015
+ code: validationMarkerCodes.validationException,
754
1016
  level: "error",
755
1017
  message,
756
1018
  item: { message },
757
1019
  path: []
758
- }]);
1020
+ };
1021
+ return of([errorMarker]);
759
1022
  }));
760
1023
  }
761
1024
  function validateItem(opts) {
@@ -828,7 +1091,28 @@ function validateItemObservable({ value, type, path = [], parent, customValidati
828
1091
  type: resolveTypeForArrayItem(item, type.of),
829
1092
  environment,
830
1093
  customValidationConcurrencyLimiter
831
- })))), defer(() => merge([...selfChecks, ...nestedChecks])).pipe(mergeMap$1((validateNode) => concat(idle(), validateNode), 40), mergeAll(), toArray(), map$1(flatten), map$1((results) => rules.some((rule) => extractFieldRulesFromRule(rule).length > 0) ? uniqBy(results, (rule) => JSON.stringify(rule)) : results));
1094
+ })))), defer(() => merge([...selfChecks, ...nestedChecks])).pipe(mergeMap$1((validateNode) => concat(idle(), validateNode), 40), mergeAll(), toArray(), map$1(flatten), map$1((results) => rules.some((rule) => extractFieldRulesFromRule(rule).length > 0) ? deduplicateMarkers(results) : results));
1095
+ }
1096
+ function deduplicateMarkers(markers) {
1097
+ let buckets = /* @__PURE__ */ new Map();
1098
+ return markers.filter((marker) => {
1099
+ let key = JSON.stringify([
1100
+ marker.level,
1101
+ marker.code,
1102
+ marker.message,
1103
+ marker.path
1104
+ ]), bucket = buckets.get(key);
1105
+ return bucket ? !bucket.some((existing) => isEqual(existing, marker)) && (bucket.push(marker), !0) : (buckets.set(key, [marker]), !0);
1106
+ });
1107
+ }
1108
+ function hasValidationMarkerCode(marker) {
1109
+ return typeof marker.code == "string";
1110
+ }
1111
+ function toDocumentValidationMarker(marker) {
1112
+ return hasValidationMarkerCode(marker) ? marker : {
1113
+ ...marker,
1114
+ code: validationMarkerCodes.validationFailed
1115
+ };
832
1116
  }
833
1117
  function idle(timeout) {
834
1118
  return new Observable((observer) => {
@@ -838,6 +1122,6 @@ function idle(timeout) {
838
1122
  return () => cancelIdleCallback(handle);
839
1123
  });
840
1124
  }
841
- export { validateDocumentWithWorkspace as a, normalizeValidationRules as c, convertToValidationMarker as d, pathToString as f, validateDocumentObservable as i, Rule$2 as l, validationLocaleStrings as m, validateDocument as n, validateItem as o, getFallbackLocaleSource as p, validateDocumentInternal as r, getTypeChain as s, resolveTypeForArrayItem as t, typeString as u };
1125
+ export { validateDocumentWithWorkspace as a, normalizeValidationRules as c, convertToValidationMarker as d, pathToString as f, validationMarkerCodes as h, validateDocumentObservable as i, Rule$2 as l, validationLocaleStrings as m, validateDocument as n, validateItem as o, getFallbackLocaleSource as p, validateDocumentInternal as r, getTypeChain as s, resolveTypeForArrayItem as t, typeString as u };
842
1126
 
843
- //# sourceMappingURL=validateDocument-C77_Oewa.js.map
1127
+ //# sourceMappingURL=validateDocument-Cq33kUmN.js.map