sanity-advanced-validators 0.4.0 → 0.5.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/README.md CHANGED
@@ -9,11 +9,13 @@ This package includes a set of Sanity validators for aggressive and weird edge c
9
9
  - [fileExtension](#fileExtension)
10
10
  - [minDimensions](#minDimensions)
11
11
  - [maxDimensions](#maxDimensions)
12
+ - [minCount](#minCount) 🆕
13
+ - [maxCount](#maxCount) 🆕
12
14
  - [requiredIfSiblingEq](#requiredIfSiblingEq)
13
15
  - [requiredIfSiblingNeq](#requiredIfSiblingNeq)
14
16
  - [requiredIfSlugEq](#requiredIfSlugEq)
15
17
  - [requiredIfSlugNeq](#requiredIfSlugNeq)
16
- - [regex](#regex) 🆕
18
+ - [regex](#regex)
17
19
  - [referencedDocumentRequires](#referencedDocumentRequires)
18
20
  - [maxDepth](#maxDepth)
19
21
 
@@ -38,17 +40,27 @@ const Page = defineType({
38
40
  name: "someVideoFile",
39
41
  type: "file",
40
42
  validation: (rule) =>
41
- rule.custom(requiredIfSlugEq('about', 'A video is required if {slugKey} is {operand}.'))
42
- .custom(fileExtension(['mp4', 'mov']))
43
+ rule.custom(
44
+ requiredIfSlugEq(
45
+ 'about',
46
+ 'A video is required if {slugKey} is {operand}.'
47
+ )
48
+ ).custom(
49
+ fileExtension(['mp4', 'mov'])
50
+ )
43
51
  })
44
52
  defineField({
45
53
  name: "posterImage",
46
54
  type: "image",
47
55
  hidden: ({ parent }) => parent.someVideoFile === null,
48
56
  validation: (rule) =>
49
- rule.custom(requiredIfSiblingNeq('someVideoFile', null))
50
- .custom(minDimensions({ x: 1250, y: 800 }))
51
- .custom(maxDimensions({ x: 2500, y: 1600 })),
57
+ rule.custom(
58
+ requiredIfSiblingNeq('someVideoFile', null)
59
+ ).custom(
60
+ minDimensions({ x: 1250, y: 800 })
61
+ ).custom(
62
+ maxDimensions({ x: 2500, y: 1600 })
63
+ ),
52
64
  })
53
65
  ]
54
66
  })
@@ -75,12 +87,18 @@ const Page = defineType({
75
87
  defineField({
76
88
  name: "catalog",
77
89
  type: "file",
78
- validation: (rule) => rule.custom(fileExtension("pdf")),
90
+ validation: (rule) =>
91
+ rule.custom(
92
+ fileExtension("pdf")
93
+ ),
79
94
  }),
80
95
  defineField({
81
96
  name: "video",
82
97
  type: "file",
83
- validation: (rule) => rule.custom(fileExtension(["mp4", "mov", "webm"])),
98
+ validation: (rule) =>
99
+ rule.custom(
100
+ fileExtension(["mp4", "mov", "webm"])
101
+ ),
84
102
  }),
85
103
  ],
86
104
  })
@@ -108,7 +126,10 @@ const ImageWithCaption = defineType({
108
126
  defineField({
109
127
  name: "heroImage",
110
128
  type: "image",
111
- validation: (rule) => rule.custom(minDimensions({ x: 1200, y: 800 })),
129
+ validation: (rule) =>
130
+ rule.custom(
131
+ minDimensions({ x: 1200, y: 800 })
132
+ ),
112
133
  }),
113
134
  ],
114
135
  })
@@ -129,7 +150,10 @@ message?: string // optional custom error message; replaces {x} and {y} with you
129
150
  defineField({
130
151
  name: "heroImage",
131
152
  type: "image",
132
- validation: (rule) => rule.custom(maxDimensions({ x: 2400, y: 1600 })),
153
+ validation: (rule) =>
154
+ rule.custom(
155
+ maxDimensions({ x: 2400, y: 1600 })
156
+ ),
133
157
  }),
134
158
  ```
135
159
 
@@ -141,15 +165,66 @@ defineField({
141
165
  type: "image",
142
166
  description: "Min: 1200x800, max: 2400x1600.",
143
167
  validation: (rule) =>
144
- rule
145
- .required()
146
- .custom(minDimensions({ x: 1200, y: 800 }))
147
- .custom(maxDimensions({ x: 2400, y: 1600 })),
168
+ rule.required()
169
+ .custom(
170
+ minDimensions({ x: 1200, y: 800 })
171
+ ).custom(
172
+ maxDimensions({ x: 2400, y: 1600 })
173
+ ),
148
174
  })
149
175
  ```
150
176
 
151
177
  ---
152
178
 
179
+ ### minCount
180
+
181
+ Enforces that an array contains at least _n_ items.
182
+
183
+ Note that null values are fine; use `rule.required()` to enforce non-nulls.
184
+
185
+ ```typescript
186
+ n: number,
187
+ message?: string // optional custom error message; replaces {n} with your minimum count
188
+ ```
189
+
190
+ ```typescript
191
+ defineField({
192
+ name: "thumbnails",
193
+ type: "array",
194
+ of: [ {type: 'image'} ],
195
+ validation: (rule) =>
196
+ rule.required()
197
+ .custom(
198
+ minCount(3, "At least {n} thumbnails are required.")
199
+ ),
200
+ }),
201
+ ```
202
+
203
+ ---
204
+
205
+ ### maxCount
206
+
207
+ Enforces that an array contains at most _n_ items.
208
+
209
+ ```typescript
210
+ n: number,
211
+ message?: string // optional custom error message; replaces {n} with your maximum count
212
+ ```
213
+
214
+ ```typescript
215
+ defineField({
216
+ name: "thumbnails",
217
+ type: "array",
218
+ of: [ {type: 'image'} ],
219
+ validation: (rule) =>
220
+ rule.custom(
221
+ maxCount(3, "No more than {n} thumbnails.")
222
+ ),
223
+ }),
224
+ ```
225
+
226
+ ---
227
+
153
228
  ### requiredIfSiblingEq
154
229
 
155
230
  Mark a field as `required` if a sibling field has a particular value. This is the validator we use most. _It’s super effective!_
@@ -160,7 +235,7 @@ _note:_ This does not work for slugs, because they have to match a nested `.curr
160
235
 
161
236
  ```typescript
162
237
  key: string, // name of sibling
163
- operand: string | number | null | Array<string, number, null> // value that you’re testing for (i.e. if 'name' === operand)
238
+ operand: string | number | boolean | null | Array<string, number> // value that you’re testing for (i.e. if 'name' === operand)
164
239
  message?: string // optional custom error message; replaces {key} and {operand} with your input, and {siblingValue} with the value of the sibling you’re testing against.
165
240
  ```
166
241
 
@@ -187,73 +262,77 @@ defineType({
187
262
  type: 'string',
188
263
  options: {
189
264
  list: [
190
- 'javascript', 'rust', 'python', 'swift'
265
+ 'typescript', 'rust', 'python', 'swift'
191
266
  ]
192
267
  },
193
- validation: rule => rule.custom(requiredIfSiblingEq('occupation', 'software engineer')),
268
+ validation: rule =>
269
+ rule.custom(
270
+ requiredIfSiblingEq('occupation', 'software engineer')
271
+ ),
194
272
  hidden: ({parent}) => parent.occuption !== 'software engineer',
195
273
  }),
196
274
  ],
197
275
  })
198
276
  ```
199
277
 
200
- “If not that, then this.” This also works for null.
278
+ And it also works for arrays.
201
279
 
202
280
  ```typescript
203
281
  defineType({
204
- name: 'person',
205
- type: 'object',
282
+ name: "person",
283
+ type: "object",
206
284
  fields: [
285
+ // ...
207
286
  defineField({
208
- name: 'name',
209
- type: 'string'
287
+ name: "occupation",
288
+ type: "string",
289
+ options: {
290
+ list: ["doctor", "lawyer", "software engineer", "linguist"],
291
+ },
210
292
  }),
211
293
  defineField({
212
- name: 'email',
213
- type: 'string',
214
- })
215
- defineField({
216
- name: 'phone',
217
- type: 'string',
218
- validation: rule => rule.custom(requiredIfSiblingEq(
219
- 'email',
220
- null,
221
- "If you don’t have an email address, a phone number is required."
222
- ))
223
- })
294
+ name: "favoriteLanguage",
295
+ type: "string",
296
+ options: {
297
+ list: ["typescript", "rust", "python", "swift", "latin", "urdu", "klingon"],
298
+ },
299
+ validation: (rule) =>
300
+ rule.custom(
301
+ requiredIfSiblingEq("occupation", ["software engineer", "linguist"])
302
+ ),
303
+ hidden: ({ parent }) => !["software engineer", "linguist"].includes(parent.occupation),
304
+ }),
224
305
  ],
225
306
  })
226
307
  ```
227
308
 
228
- And it even works for arrays.
309
+ It even works for null.
229
310
 
230
311
  ```typescript
231
312
  defineType({
232
- name: "person",
233
- type: "object",
313
+ name: 'person',
314
+ type: 'object',
234
315
  fields: [
235
316
  defineField({
236
- name: "name",
237
- type: "string",
238
- }),
239
- defineField({
240
- name: "name",
241
- type: "string",
317
+ name: 'name',
318
+ type: 'string'
242
319
  }),
243
320
  defineField({
244
- name: "occupation",
245
- type: "string",
246
- options: {
247
- list: ["doctor", "lawyer", "software engineer"],
248
- },
249
- }),
321
+ name: 'email',
322
+ type: 'string',
323
+ })
250
324
  defineField({
251
- name: "explanation",
252
- description: "Why are you wasting your life this way?",
253
- type: "text",
254
- validation: (rule) => rule.custom(requiredIfSiblingEq("occupation", ["doctor", "lawyer"])),
255
- hidden: ({ parent }) => parent.occuption === "software engineer",
256
- }),
325
+ name: 'phone',
326
+ type: 'string',
327
+ validation: rule =>
328
+ rule.custom(
329
+ requiredIfSiblingEq(
330
+ 'email',
331
+ null,
332
+ "If you don’t have an email address, a phone number is required."
333
+ )
334
+ )
335
+ })
257
336
  ],
258
337
  })
259
338
  ```
@@ -262,13 +341,13 @@ defineType({
262
341
 
263
342
  ### requiredIfSiblingNeq
264
343
 
265
- For a given object that has multiple fields, mark a field as `required` if a sibling does _not_ have a particular value.
344
+ For a given object that has multiple fields, mark a field as `required` if a sibling does _not_ have a particular value (or member of an array of values).
266
345
 
267
346
  _note:_ This does not work for slugs, because they have to match a nested `.current` value. Use the [requiredIfSlugNeq validator](#requiredIfSlugNeq) instead.
268
347
 
269
348
  ```typescript
270
349
  key: string, // name of sibling
271
- operand: string | number | null | Array<string, number, null> // value that you’re testing for (i.e. if 'name' === operand)
350
+ operand: string | number | boolean | null | Array<string, number> // value that you’re testing for (i.e. if 'name' === operand)
272
351
  message?: string // optional custom error message; replaces {key} and {operand} with your input, and {siblingValue} with the value of the sibling you’re testing against.
273
352
  ```
274
353
 
@@ -291,12 +370,15 @@ defineType({
291
370
  }
292
371
  })
293
372
  defineField({
294
- name: 'why',
295
- description: 'How many years will you spend paying off your degree?',
296
- type: 'number',
297
- validation: rule => rule.custom(requiredIfSiblingNeq('occupation', 'software engineer'))
298
- }),
299
- ],
373
+ name: "explanation",
374
+ description: "Why are you wasting your life this way?",
375
+ type: "text",
376
+ validation: (rule) =>
377
+ rule.custom(
378
+ requiredIfSiblingNeq("occupation", "software engineer")
379
+ ),
380
+ hidden: ({ parent }) => parent.occuption === "software engineer",
381
+ }), ],
300
382
  })
301
383
  ```
302
384
 
@@ -307,7 +389,7 @@ defineType({
307
389
  Mark a field as `required` for documents with matching slugs.
308
390
 
309
391
  ```typescript
310
- operand: string | number | null | Array<string, number, null> // possible slug or slugs you’re testing
392
+ operand: string | number | null | Array<string, number> // possible slug or slugs you’re testing
311
393
  key?: string, // name of sibling if not "slug"
312
394
  message?: string // optional custom error message; replaces {slugKey} and {operand} with your input, and {siblingSlugValue} with the value of the sibling you’re testing against.
313
395
  ```
@@ -327,7 +409,10 @@ defineType({
327
409
  name: "questionsAndAnswers",
328
410
  type: "array",
329
411
  of: [{ type: "qaItem" }],
330
- validation: (rule) => rule.custom(requiredIfSlugEq("faq")),
412
+ validation: (rule) =>
413
+ rule.custom(
414
+ requiredIfSlugEq("faq")
415
+ ),
331
416
  hidden: ({ parent }) => parent.slug.current !== "faq",
332
417
  }),
333
418
  ],
@@ -339,7 +424,10 @@ And this can apply to multiple slugs…
339
424
  ```typescript
340
425
  defineField({
341
426
  name: "questionsAndAnswers",
342
- validation: (rule) => rule.custom(requiredIfSlugEq(["faq", "about"])),
427
+ validation: (rule) =>
428
+ rule.custom(
429
+ requiredIfSlugEq(["faq", "about"])
430
+ ),
343
431
  }),
344
432
  ```
345
433
 
@@ -350,7 +438,7 @@ defineField({
350
438
  Require fields on pages that don't match one or more slugs.
351
439
 
352
440
  ```typescript
353
- operand: string | number | null | Array<string, number, null> // possible slug or slugs you’re testing
441
+ operand: string | number | null | Array<string, number> // possible slug or slugs you’re testing
354
442
  key?: string, // name of sibling if not "slug"
355
443
  message?: string // optional custom error message; replaces {slugKey} and {operand} with your input, and {siblingSlugValue} with the value of the sibling you’re testing against.
356
444
  ```
@@ -371,7 +459,10 @@ defineType({
371
459
  description: `Subnav is required on documents that aren’t '/home'`,
372
460
  type: "array",
373
461
  of: [{ type: "navLink" }],
374
- validation: (rule) => rule.custom(requiredIfSlugNeq("home")),
462
+ validation: (rule) =>
463
+ rule.custom(
464
+ requiredIfSlugNeq("home")
465
+ ),
375
466
  hidden: ({ parent }) => parent.slug.current !== "home",
376
467
  }),
377
468
  ],
@@ -395,16 +486,18 @@ message?: string // optional custom error message; replaces {pattern} with your
395
486
  defineField({
396
487
  name: 'email',
397
488
  type: 'string',
398
- validation: (rule) => rule.custom(
399
- regex(
400
- /^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})*$/,
401
- "“{value}” is not a valid email address."
402
- )
403
- ),
489
+ validation: (rule) =>
490
+ rule.custom(
491
+ regex(
492
+ /^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})*$/,
493
+ "“{value}” is not a valid email address."
494
+ )
495
+ ),
404
496
  }),
405
497
  ```
406
498
 
407
499
  **Custom error messages are highly recommended here.** Without the custom message above, the default response would be:
500
+
408
501
  ```
409
502
  “me@googlecom” does not match the pattern /^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})*$/.
410
503
  ```
@@ -427,7 +520,10 @@ defineField({
427
520
  description: 'An article (must include a valid poster image)',
428
521
  type: 'reference',
429
522
  to: [{type: 'article'}],
430
- validation: (rule) => rule.custom(referencedDocumentRequires('article', 'poster')),
523
+ validation: (rule) =>
524
+ rule.custom(
525
+ referencedDocumentRequires('article', 'poster')
526
+ ),
431
527
  }),
432
528
  ```
433
529
 
@@ -503,7 +599,10 @@ const navLink = defineType({
503
599
  name: "subnav",
504
600
  type: "array",
505
601
  of: [{ type: navigation }],
506
- validation: (rule) => rule.custom(maxDepth(3, "subnav")),
602
+ validation: (rule) =>
603
+ rule.custom(
604
+ maxDepth(3, "subnav")
605
+ ),
507
606
  }),
508
607
  ],
509
608
  })
@@ -536,7 +635,7 @@ defineField({
536
635
 
537
636
  Most of these validators rely on a function called `getSibling()`. If you’re thinking about picking this apart and writing your own custom validator, take a close look at how these validators use it.
538
637
 
539
- ## Upcoming
638
+ ## Roadmap
540
639
 
541
640
  ### Nested pathfinders
542
641
 
package/dist/index.cjs CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  maxDepth: () => maxDepth,
26
26
  minDimensions: () => minDimensions,
27
27
  referencedDocumentRequires: () => referencedDocumentRequires,
28
+ regex: () => regex,
28
29
  requiredIfSiblingEq: () => requiredIfSiblingEq,
29
30
  requiredIfSlugEq: () => requiredIfSlugEq
30
31
  });
@@ -69,20 +70,20 @@ var minDimensions = ({ x, y }, message) => (value) => {
69
70
  }
70
71
  const { width, height } = (0, import_asset_utils2.getImageDimensions)(value.asset._ref);
71
72
  if (!!x && width < x) {
72
- return message ? message.replace("{x}", x.toString()).replace("{y}", !y ? "(any)" : y.toString()) : `Image must be at least ${x} pixels wide.`;
73
+ return message ? message.replace("{width}", width.toString()).replace("{height}", height.toString()).replace("{x}", x.toString()).replace("{y}", !y ? "(any)" : y.toString()) : `Image must be at least ${x} pixels wide.`;
73
74
  }
74
75
  if (!!y && height < y) {
75
- return message ? message.replace("{x}", !x ? "(any)" : x.toString()).replace("{y}", y.toString()) : `Image must be at least ${y} pixels tall.`;
76
+ return message ? message.replace("{width}", width.toString()).replace("{height}", height.toString()).replace("{x}", !x ? "(any)" : x.toString()).replace("{y}", y.toString()) : `Image must be at least ${y} pixels tall.`;
76
77
  }
77
78
  return true;
78
79
  };
79
80
 
80
81
  // src/maxDepth.ts
81
- var maxDepth = (maxDepth2, nestedValueName, message = `Error: You can only nest {nestedValueName} {maxDepth} levels deep.`) => (_, context) => {
82
- let regex = new RegExp(String.raw`topLevelItems|${nestedValueName}`);
83
- const paths = context.path.filter((e) => typeof e === "string" && e.match(regex));
82
+ var maxDepth = (maxDepth2, key, message = `Error: You can only nest {key} {maxDepth} levels deep.`) => (_, context) => {
83
+ let regex2 = new RegExp(String.raw`topLevelItems|${key}`);
84
+ const paths = context.path.filter((e) => typeof e === "string" && e.match(regex2));
84
85
  if (paths.length > maxDepth2) {
85
- return message.replace("{nestedValueName}", nestedValueName).replace("{maxDepth}", maxDepth2.toString());
86
+ return message.replace("{key}", key).replace("{nestedValueName}", key).replace("{maxDepth}", maxDepth2.toString());
86
87
  }
87
88
  return true;
88
89
  };
@@ -91,20 +92,20 @@ var maxDepth = (maxDepth2, nestedValueName, message = `Error: You can only nest
91
92
  var requiredIfSlugEq = (slug, slugKey = "slug", message = `This is a required field.`) => (value, context) => {
92
93
  var _a, _b;
93
94
  const slugs = typeof slug === "string" ? [slug] : slug;
94
- const currentSlugValue = (_b = (_a = context.parent) == null ? void 0 : _a[slugKey]) == null ? void 0 : _b.current;
95
- if (!value && !!currentSlugValue && slugs.includes(currentSlugValue)) {
96
- return message.replace("{slugKey}", slugKey).replace("{slug}", slugs.join(", or "));
95
+ const slugValue = (_b = (_a = context.parent) == null ? void 0 : _a[slugKey]) == null ? void 0 : _b.current;
96
+ if (!value && !!slugValue && slugs.includes(slugValue)) {
97
+ return message.replace("{slugKey}", slugKey).replace("{operand}", slugs.join(", or ")).replace("{siblingSlugValue}", slugValue);
97
98
  }
98
99
  return true;
99
100
  };
100
101
 
101
102
  // src/requiredIfSiblingEq.ts
102
- var requiredIfSiblingEq = (key, comparison, message = "Required if {key} equals {value}.") => (value, context) => {
103
- var _a;
104
- const sibling = getSibling(key, context);
105
- const comparisons = Array.isArray(comparison) ? comparison : [comparison];
106
- if (!value && comparisons.includes(sibling)) {
107
- return message.replace("{key}", key).replace("{value}", (_a = comparisons.join(", or ")) != null ? _a : "null");
103
+ var requiredIfSiblingEq = (key, operand, message = "Required if {key} equals {operand}.") => (value, context) => {
104
+ var _a, _b;
105
+ const siblingValue = getSibling(key, context);
106
+ const operands = Array.isArray(operand) ? operand : [operand];
107
+ if (!value && operands.includes(siblingValue)) {
108
+ return message.replace("{key}", key).replace("{operand}", (_a = operands.join(", or ")) != null ? _a : "null").replace("{value}", (_b = operands.join(", or ")) != null ? _b : "null").replace("{siblingValue}", siblingValue);
108
109
  }
109
110
  return true;
110
111
  };
@@ -116,6 +117,15 @@ var getSibling = (key, context) => {
116
117
  const sibling = (0, import_lodash_es.get)(context.document, [...pathToParentObject, key]);
117
118
  return sibling;
118
119
  };
120
+
121
+ // src/regex.ts
122
+ var regex = (pattern, message = `\u201C{value}\u201D does not match the pattern {pattern}.`) => (value) => {
123
+ if (!value) {
124
+ return true;
125
+ }
126
+ const valueAsString = typeof value !== "string" ? value.toString() : value;
127
+ return pattern.test(valueAsString) ? true : message.replace("{value}", valueAsString).replace("{pattern}", pattern.toString());
128
+ };
119
129
  // Annotate the CommonJS export names for ESM import in node:
120
130
  0 && (module.exports = {
121
131
  fileExtension,
@@ -123,6 +133,7 @@ var getSibling = (key, context) => {
123
133
  maxDepth,
124
134
  minDimensions,
125
135
  referencedDocumentRequires,
136
+ regex,
126
137
  requiredIfSiblingEq,
127
138
  requiredIfSlugEq
128
139
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/referencedDocumentRequires.ts","../src/fileExtension.ts","../src/minDimensions.ts","../src/maxDepth.ts","../src/requiredIfSlugEq.ts","../src/requiredIfSiblingEq.ts","../src/lib/getSibling.ts"],"sourcesContent":["export * from \"./referencedDocumentRequires\"\nexport * from \"./fileExtension\"\nexport * from \"./minDimensions\"\nexport * from \"./maxDepth\"\nexport * from \"./requiredIfSlugEq\"\nexport * from \"./requiredIfSiblingEq\"\nexport * from \"./lib\"\n","import { ValidationContext } from \"sanity\"\n\nexport const referencedDocumentRequires = (\n documentType: string, \n field: string, \n message: string = `{documentType}’s {field} must be filled.`\n) => async (value: any | undefined, context: ValidationContext) => {\n if (!value?._ref) {\n return true\n }\n const client = context.getClient({ apiVersion: \"2022-08-12\" })\n // todo: use current API version, or test with no version at all\n\n // todo: if there's a value._type or value.referenced._type or something, we get rid of document.type from inputs\n const data = await client.fetch(`\n *[_type == \"${documentType}\" && _id == \"${value._ref}\"]{\n ${field}\n }[0]\n `) // TODO: why is typescript screaming about this? Fetch takes two parameters.\n if (!data[field]) {\n return message.replace(\"{documentType}\", documentType).replace(\"{field}\", field)\n }\n return true\n}\n","import { getExtension } from \"@sanity/asset-utils\"\nimport { FileValue } from \"sanity\"\n\nexport const fileExtension = (\n validFileExtension: string | Array<string>, \n message: string = `Image must be of type {validFileExtension}`\n) => (value: FileValue | undefined) => {\n if (!value || !value.asset) {\n return true\n }\n const validExtensions = typeof validFileExtension === \"string\" ? [validFileExtension] : validFileExtension\n const filetype = getExtension(value.asset._ref)\n if (!validExtensions.includes(filetype)) {\n return message.replace(\"{validFileExtension}\", validExtensions.join(\", or \"))\n }\n return true\n}\n","import { getImageDimensions } from \"@sanity/asset-utils\"\nimport { FileValue } from \"sanity\"\n\nexport const minDimensions =\n ({ x, y }: { x: number; y: number }, message?: string) =>\n (value: FileValue | undefined) => {\n if (!value || !value.asset) {\n return true\n }\n const { width, height } = getImageDimensions(value.asset._ref)\n if (!!x && width < x) {\n return message ? message.replace(\"{x}\", x.toString()).replace(\"{y}\", !y ? \"(any)\" : y.toString()) : `Image must be at least ${x} pixels wide.`\n }\n if (!!y && height < y) {\n return message ? message.replace(\"{x}\", !x ? \"(any)\" : x.toString()).replace(\"{y}\", y.toString()) : `Image must be at least ${y} pixels tall.`\n }\n return true\n }\n","import { ValidationContext } from \"sanity\"\n\nexport const maxDepth = (\n maxDepth: number, \n nestedValueName: string,\n message: string = `Error: You can only nest {nestedValueName} {maxDepth} levels deep.`\n) => (_: any, context: ValidationContext) => {\n let regex = new RegExp(String.raw`topLevelItems|${nestedValueName}`)\n const paths = (context.path as Array<any>).filter((e) => typeof e === \"string\" && e.match(regex))\n if (paths.length > maxDepth) {\n return message.replace(\"{nestedValueName}\", nestedValueName).replace(\"{maxDepth}\", maxDepth.toString())\n }\n return true\n}\n","import { ValidationContext } from \"sanity\"\n\n/*\nSanity has a funny idea of conditional fields. Every field is _always_ present, but it might be hidden.\nex. hidden: (node) => node.parent.slug === 'hideMe'\nThis works really well — unless a field marked as required gets hidden. \n\nThis validator conditionally marks a field as required only for specific slugs. It accepts a string or array of strings.\n```\nvalidation: (rule) => rule.custom(requiredIfSlugEq('alpha'))\nvalidation: (rule) => rule.custom(requiredIfSlugEq(['alpha', 'beta']))\nvalidation: (rule) => rule.custom(requiredIfSlugNotEq(['beta']))\n```\n\nIf the key of your slug is not simply \"slug\", fill that in the optional second parameter.\n```\nvalidation: (rule) => rule.custom(requiredIfSlugEq('alpha', 'id'))\n```\n\n\"Could this method be simpler if it just checked for the self.hidden state?\"\nNot possible, since the hidden state is not exposed to the context.\n\nBut even if it were, you wouldn't want to. There are valid reasons to make a component required but hidden.\nex. an admin- or developer-level identifier that you don't want civilians to see or edit.\n*/\n\nexport const requiredIfSlugEq = (\n slug: Array<string> | string, \n slugKey: string = \"slug\", \n message: string = `This is a required field.`\n) =>\n (value: unknown | undefined, context: ValidationContext) => {\n const slugs = typeof slug === \"string\" ? [slug] : slug\n const currentSlugValue = (context.parent as any)?.[slugKey]?.current\n \n // todo: does slugKey exist? If not, fail.\n // todo: deal with nested slugKey (ex. metadata.slug)\n \n if (!value && !!currentSlugValue && slugs.includes(currentSlugValue)) {\n return message.replace(\"{slugKey}\", slugKey).replace(\"{slug}\", slugs.join(', or '))\n }\n return true\n }","import {getSibling} from './'\nimport {ValidationContext} from 'sanity'\n\n/*\nFor a given object that has multiple fields, mark a field as `required` if a sibling has a particular value.\n\n```\ndefineType({\n name: 'ifAlphaAlsoBeta',\n type: 'object',\n fields: [\n defineField({\n name: 'alpha',\n type: 'string',\n options: {\n list: ['left', 'right'],\n layout: 'radio',\n direction: 'horizontal',\n },\n }),\n defineField({\n name: 'beta',\n type: 'string',\n placeholder: 'If alpha is “left”, I’m also required',\n validation: (rule) => rule.custom(requiredIfSiblingEq('alpha', 'left')),\n })\n ],\n})\n```\n\nIncidentally, context.path is technically Array<sanity.PathSegment>.\n\nThat shouldn't matter, but dealing with that and remapping siblingKey as a PathSegment could be a possible future enhancement.\n*/\n\nexport const requiredIfSiblingEq = (\n key: string, \n comparison: string | number | null | Array<string | number | null>, \n message: string = 'Required if {key} equals {value}.'\n) =>\n (value: unknown | undefined, context: ValidationContext) => {\n const sibling = getSibling(key, context)\n const comparisons = Array.isArray(comparison) ? comparison : [comparison]\n if (!value && comparisons.includes(sibling)) {\n return message.replace('{key}', key).replace('{value}', comparisons.join(', or ') ?? 'null')\n }\n return true\n }\n","import { get } from \"lodash-es\"\nimport { ValidationContext } from \"sanity\"\n\nexport const getSibling = (key: string | number, context: ValidationContext) => {\n const pathToParentObject = context.path!.slice(0, -1) as Array<string | number>\n const sibling = get(context.document, [...pathToParentObject, key])\n return sibling\n}\n\n/*\nTODO:\n There is an issue with finding a sibling when in an array element.\n If the context document looks something like this…\n {\n someArray: [\n {\n _key: 'abc123',\n targetSibling: 'herpderp'\n }\n ]\n }\n … we wind up with a path of…\n [ 'someArray', { _key: 'ab123' }, 'targetSibling' ]\n lodash.get() is trying to do an exact match, it doesn't know how to get object by _key.\n \n Will probably have to replace get() with a gnarly recursive lookup function.\n*/\n"],"mappings":"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","names":["import_asset_utils","maxDepth"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/referencedDocumentRequires.ts","../src/fileExtension.ts","../src/minDimensions.ts","../src/maxDepth.ts","../src/requiredIfSlugEq.ts","../src/requiredIfSiblingEq.ts","../src/lib/getSibling.ts","../src/regex.ts"],"sourcesContent":["export * from \"./referencedDocumentRequires\"\nexport * from \"./fileExtension\"\nexport * from \"./minDimensions\"\nexport * from \"./maxDepth\"\nexport * from \"./requiredIfSlugEq\"\nexport * from \"./requiredIfSiblingEq\"\nexport * from \"./lib\"\nexport * from \"./regex\"\n","import { ValidationContext } from \"sanity\"\n\nexport const referencedDocumentRequires = (\n documentType: string, \n field: string, \n message: string = `{documentType}’s {field} must be filled.`\n) => async (value: any | undefined, context: ValidationContext) => {\n if (!value?._ref) {\n return true\n }\n const client = context.getClient({ apiVersion: \"2022-08-12\" })\n // todo: use current API version, or test with no version at all\n\n // todo: if there's a value._type or value.referenced._type or something, we get rid of document.type from inputs\n const data = await client.fetch(`\n *[_type == \"${documentType}\" && _id == \"${value._ref}\"]{\n ${field}\n }[0]\n `) // TODO: why is typescript screaming about this? Fetch takes two parameters.\n if (!data[field]) {\n return message.replace(\"{documentType}\", documentType).replace(\"{field}\", field)\n }\n return true\n}\n","import { getExtension } from \"@sanity/asset-utils\"\nimport { FileValue } from \"sanity\"\n\nexport const fileExtension = (\n validFileExtension: string | Array<string>, \n message: string = `Image must be of type {validFileExtension}`\n) => (value: FileValue | undefined) => {\n if (!value || !value.asset) {\n return true\n }\n const validExtensions = typeof validFileExtension === \"string\" ? [validFileExtension] : validFileExtension\n const filetype = getExtension(value.asset._ref)\n if (!validExtensions.includes(filetype)) {\n return message.replace(\"{validFileExtension}\", validExtensions.join(\", or \"))\n }\n return true\n}\n\n// todo: this should fail if its attached to a field that is not of type \"file\"","import { getImageDimensions } from \"@sanity/asset-utils\"\nimport { FileValue } from \"sanity\"\n\nexport const minDimensions =\n ({ x, y }: { x: number; y: number }, message?: string) =>\n (value: FileValue | undefined) => {\n if (!value || !value.asset) {\n return true\n }\n const { width, height } = getImageDimensions(value.asset._ref)\n if (!!x && width < x) {\n return message \n ? message.replace(\"{width}\", width.toString())\n .replace(\"{height}\", height.toString())\n .replace(\"{x}\", x.toString())\n .replace(\"{y}\", !y ? \"(any)\" : y.toString()) \n : `Image must be at least ${x} pixels wide.`\n }\n if (!!y && height < y) {\n return message \n ? message.replace(\"{width}\", width.toString())\n .replace(\"{height}\", height.toString())\n .replace(\"{x}\", !x ? \"(any)\" : x.toString())\n .replace(\"{y}\", y.toString())\n : `Image must be at least ${y} pixels tall.`\n }\n return true\n }\n\n// todo: this should fail if its attached to a field that is not of type \"image\"","import { ValidationContext } from \"sanity\"\n\nexport const maxDepth = (\n maxDepth: number, \n key: string,\n message: string = `Error: You can only nest {key} {maxDepth} levels deep.`\n) => (_: any, context: ValidationContext) => {\n let regex = new RegExp(String.raw`topLevelItems|${key}`)\n const paths = (context.path as Array<any>).filter((e) => typeof e === \"string\" && e.match(regex))\n if (paths.length > maxDepth) {\n return message\n .replace(\"{key}\", key)\n .replace(\"{nestedValueName}\", key) // backward compatibility\n .replace(\"{maxDepth}\", maxDepth.toString())\n }\n return true\n}\n","import { ValidationContext } from \"sanity\"\n\n/*\nSanity has a funny idea of conditional fields. Every field is _always_ present, but it might be hidden.\nex. hidden: (node) => node.parent.slug === 'hideMe'\nThis works really well — unless a field marked as required gets hidden. \n\nThis validator conditionally marks a field as required only for specific slugs. It accepts a string or array of strings.\n```\nvalidation: (rule) => rule.custom(requiredIfSlugEq('alpha'))\nvalidation: (rule) => rule.custom(requiredIfSlugEq(['alpha', 'beta']))\nvalidation: (rule) => rule.custom(requiredIfSlugNotEq(['beta']))\n```\n\nIf the key of your slug is not simply \"slug\", fill that in the optional second parameter.\n```\nvalidation: (rule) => rule.custom(requiredIfSlugEq('alpha', 'id'))\n```\n\n\"Could this method be simpler if it just checked for the self.hidden state?\"\nNot possible, since the hidden state is not exposed to the context.\n\nBut even if it were, you wouldn't want to. There are valid reasons to make a component required but hidden.\nex. an admin- or developer-level identifier that you don't want civilians to see or edit.\n*/\n\nexport const requiredIfSlugEq = (\n slug: Array<string> | string, \n slugKey: string = \"slug\", \n message: string = `This is a required field.`\n) =>\n (value: unknown | undefined, context: ValidationContext) => {\n const slugs = typeof slug === \"string\" ? [slug] : slug\n const slugValue = (context.parent as any)?.[slugKey]?.current\n \n // todo: does slugKey exist? If not, fail.\n // todo: deal with nested slugKey (ex. metadata.slug)\n \n if (!value && !!slugValue && slugs.includes(slugValue)) {\n return message\n .replace(\"{slugKey}\", slugKey)\n .replace(\"{operand}\", slugs.join(', or '))\n .replace(\"{siblingSlugValue}\", slugValue)\n }\n return true\n }","import {getSibling} from './'\nimport {ValidationContext} from 'sanity'\n\n/*\nFor a given object that has multiple fields, mark a field as `required` if a sibling has a particular value.\n\n```\ndefineType({\n name: 'ifAlphaAlsoBeta',\n type: 'object',\n fields: [\n defineField({\n name: 'alpha',\n type: 'string',\n options: {\n list: ['left', 'right'],\n layout: 'radio',\n direction: 'horizontal',\n },\n }),\n defineField({\n name: 'beta',\n type: 'string',\n placeholder: 'If alpha is “left”, I’m also required',\n validation: (rule) => rule.custom(requiredIfSiblingEq('alpha', 'left')),\n })\n ],\n})\n```\n\nIncidentally, context.path is technically Array<sanity.PathSegment>.\n\nThat shouldn't matter, but dealing with that and remapping siblingKey as a PathSegment could be a possible future enhancement.\n*/\n\nexport const requiredIfSiblingEq = (\n key: string, \n operand: string | number | null | Array<string | number | null>, \n message: string = 'Required if {key} equals {operand}.'\n) =>\n (value: unknown | undefined, context: ValidationContext) => {\n const siblingValue = getSibling(key, context)\n const operands = Array.isArray(operand) ? operand : [operand]\n if (!value && operands.includes(siblingValue)) {\n return message\n .replace('{key}', key)\n .replace('{operand}', operands.join(', or ') ?? 'null')\n .replace('{value}', operands.join(', or ') ?? 'null') // backward compatibility\n .replace('{siblingValue}', siblingValue)\n }\n return true\n }\n","import { get } from \"lodash-es\"\nimport { ValidationContext } from \"sanity\"\n\nexport const getSibling = (key: string | number, context: ValidationContext) => {\n const pathToParentObject = context.path!.slice(0, -1) as Array<string | number>\n const sibling = get(context.document, [...pathToParentObject, key])\n return sibling\n}\n\n/*\nTODO:\n There is an issue with finding a sibling when in an array element.\n If the context document looks something like this…\n {\n someArray: [\n {\n _key: 'abc123',\n targetSibling: 'herpderp'\n }\n ]\n }\n … we wind up with a path of…\n [ 'someArray', { _key: 'ab123' }, 'targetSibling' ]\n lodash.get() is trying to do an exact match, it doesn't know how to get object by _key.\n \n Will probably have to replace get() with a gnarly recursive lookup function.\n*/\n","export const regex =\n (pattern: RegExp, message: string = `“{value}” does not match the pattern {pattern}.`) =>\n (value: unknown) => {\n if (!value) {\n return true\n }\n const valueAsString = typeof value !== \"string\" ? value.toString() : value\n return pattern.test(valueAsString) ? true : message.replace(\"{value}\", valueAsString).replace(\"{pattern}\", pattern.toString())\n }\n"],"mappings":"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","names":["import_asset_utils","maxDepth","regex"]}
package/dist/index.d.cts CHANGED
@@ -9,12 +9,14 @@ declare const minDimensions: ({ x, y }: {
9
9
  y: number;
10
10
  }, message?: string) => (value: FileValue | undefined) => string | true;
11
11
 
12
- declare const maxDepth: (maxDepth: number, nestedValueName: string, message?: string) => (_: any, context: ValidationContext) => string | true;
12
+ declare const maxDepth: (maxDepth: number, key: string, message?: string) => (_: any, context: ValidationContext) => string | true;
13
13
 
14
14
  declare const requiredIfSlugEq: (slug: Array<string> | string, slugKey?: string, message?: string) => (value: unknown | undefined, context: ValidationContext) => string | true;
15
15
 
16
- declare const requiredIfSiblingEq: (key: string, comparison: string | number | null | Array<string | number | null>, message?: string) => (value: unknown | undefined, context: ValidationContext) => string | true;
16
+ declare const requiredIfSiblingEq: (key: string, operand: string | number | null | Array<string | number | null>, message?: string) => (value: unknown | undefined, context: ValidationContext) => string | true;
17
17
 
18
18
  declare const getSibling: (key: string | number, context: ValidationContext) => any;
19
19
 
20
- export { fileExtension, getSibling, maxDepth, minDimensions, referencedDocumentRequires, requiredIfSiblingEq, requiredIfSlugEq };
20
+ declare const regex: (pattern: RegExp, message?: string) => (value: unknown) => string | true;
21
+
22
+ export { fileExtension, getSibling, maxDepth, minDimensions, referencedDocumentRequires, regex, requiredIfSiblingEq, requiredIfSlugEq };
package/dist/index.d.ts CHANGED
@@ -9,12 +9,14 @@ declare const minDimensions: ({ x, y }: {
9
9
  y: number;
10
10
  }, message?: string) => (value: FileValue | undefined) => string | true;
11
11
 
12
- declare const maxDepth: (maxDepth: number, nestedValueName: string, message?: string) => (_: any, context: ValidationContext) => string | true;
12
+ declare const maxDepth: (maxDepth: number, key: string, message?: string) => (_: any, context: ValidationContext) => string | true;
13
13
 
14
14
  declare const requiredIfSlugEq: (slug: Array<string> | string, slugKey?: string, message?: string) => (value: unknown | undefined, context: ValidationContext) => string | true;
15
15
 
16
- declare const requiredIfSiblingEq: (key: string, comparison: string | number | null | Array<string | number | null>, message?: string) => (value: unknown | undefined, context: ValidationContext) => string | true;
16
+ declare const requiredIfSiblingEq: (key: string, operand: string | number | null | Array<string | number | null>, message?: string) => (value: unknown | undefined, context: ValidationContext) => string | true;
17
17
 
18
18
  declare const getSibling: (key: string | number, context: ValidationContext) => any;
19
19
 
20
- export { fileExtension, getSibling, maxDepth, minDimensions, referencedDocumentRequires, requiredIfSiblingEq, requiredIfSlugEq };
20
+ declare const regex: (pattern: RegExp, message?: string) => (value: unknown) => string | true;
21
+
22
+ export { fileExtension, getSibling, maxDepth, minDimensions, referencedDocumentRequires, regex, requiredIfSiblingEq, requiredIfSlugEq };
package/dist/index.js CHANGED
@@ -37,20 +37,20 @@ var minDimensions = ({ x, y }, message) => (value) => {
37
37
  }
38
38
  const { width, height } = getImageDimensions(value.asset._ref);
39
39
  if (!!x && width < x) {
40
- return message ? message.replace("{x}", x.toString()).replace("{y}", !y ? "(any)" : y.toString()) : `Image must be at least ${x} pixels wide.`;
40
+ return message ? message.replace("{width}", width.toString()).replace("{height}", height.toString()).replace("{x}", x.toString()).replace("{y}", !y ? "(any)" : y.toString()) : `Image must be at least ${x} pixels wide.`;
41
41
  }
42
42
  if (!!y && height < y) {
43
- return message ? message.replace("{x}", !x ? "(any)" : x.toString()).replace("{y}", y.toString()) : `Image must be at least ${y} pixels tall.`;
43
+ return message ? message.replace("{width}", width.toString()).replace("{height}", height.toString()).replace("{x}", !x ? "(any)" : x.toString()).replace("{y}", y.toString()) : `Image must be at least ${y} pixels tall.`;
44
44
  }
45
45
  return true;
46
46
  };
47
47
 
48
48
  // src/maxDepth.ts
49
- var maxDepth = (maxDepth2, nestedValueName, message = `Error: You can only nest {nestedValueName} {maxDepth} levels deep.`) => (_, context) => {
50
- let regex = new RegExp(String.raw`topLevelItems|${nestedValueName}`);
51
- const paths = context.path.filter((e) => typeof e === "string" && e.match(regex));
49
+ var maxDepth = (maxDepth2, key, message = `Error: You can only nest {key} {maxDepth} levels deep.`) => (_, context) => {
50
+ let regex2 = new RegExp(String.raw`topLevelItems|${key}`);
51
+ const paths = context.path.filter((e) => typeof e === "string" && e.match(regex2));
52
52
  if (paths.length > maxDepth2) {
53
- return message.replace("{nestedValueName}", nestedValueName).replace("{maxDepth}", maxDepth2.toString());
53
+ return message.replace("{key}", key).replace("{nestedValueName}", key).replace("{maxDepth}", maxDepth2.toString());
54
54
  }
55
55
  return true;
56
56
  };
@@ -59,20 +59,20 @@ var maxDepth = (maxDepth2, nestedValueName, message = `Error: You can only nest
59
59
  var requiredIfSlugEq = (slug, slugKey = "slug", message = `This is a required field.`) => (value, context) => {
60
60
  var _a, _b;
61
61
  const slugs = typeof slug === "string" ? [slug] : slug;
62
- const currentSlugValue = (_b = (_a = context.parent) == null ? void 0 : _a[slugKey]) == null ? void 0 : _b.current;
63
- if (!value && !!currentSlugValue && slugs.includes(currentSlugValue)) {
64
- return message.replace("{slugKey}", slugKey).replace("{slug}", slugs.join(", or "));
62
+ const slugValue = (_b = (_a = context.parent) == null ? void 0 : _a[slugKey]) == null ? void 0 : _b.current;
63
+ if (!value && !!slugValue && slugs.includes(slugValue)) {
64
+ return message.replace("{slugKey}", slugKey).replace("{operand}", slugs.join(", or ")).replace("{siblingSlugValue}", slugValue);
65
65
  }
66
66
  return true;
67
67
  };
68
68
 
69
69
  // src/requiredIfSiblingEq.ts
70
- var requiredIfSiblingEq = (key, comparison, message = "Required if {key} equals {value}.") => (value, context) => {
71
- var _a;
72
- const sibling = getSibling(key, context);
73
- const comparisons = Array.isArray(comparison) ? comparison : [comparison];
74
- if (!value && comparisons.includes(sibling)) {
75
- return message.replace("{key}", key).replace("{value}", (_a = comparisons.join(", or ")) != null ? _a : "null");
70
+ var requiredIfSiblingEq = (key, operand, message = "Required if {key} equals {operand}.") => (value, context) => {
71
+ var _a, _b;
72
+ const siblingValue = getSibling(key, context);
73
+ const operands = Array.isArray(operand) ? operand : [operand];
74
+ if (!value && operands.includes(siblingValue)) {
75
+ return message.replace("{key}", key).replace("{operand}", (_a = operands.join(", or ")) != null ? _a : "null").replace("{value}", (_b = operands.join(", or ")) != null ? _b : "null").replace("{siblingValue}", siblingValue);
76
76
  }
77
77
  return true;
78
78
  };
@@ -84,12 +84,22 @@ var getSibling = (key, context) => {
84
84
  const sibling = get(context.document, [...pathToParentObject, key]);
85
85
  return sibling;
86
86
  };
87
+
88
+ // src/regex.ts
89
+ var regex = (pattern, message = `\u201C{value}\u201D does not match the pattern {pattern}.`) => (value) => {
90
+ if (!value) {
91
+ return true;
92
+ }
93
+ const valueAsString = typeof value !== "string" ? value.toString() : value;
94
+ return pattern.test(valueAsString) ? true : message.replace("{value}", valueAsString).replace("{pattern}", pattern.toString());
95
+ };
87
96
  export {
88
97
  fileExtension,
89
98
  getSibling,
90
99
  maxDepth,
91
100
  minDimensions,
92
101
  referencedDocumentRequires,
102
+ regex,
93
103
  requiredIfSiblingEq,
94
104
  requiredIfSlugEq
95
105
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/referencedDocumentRequires.ts","../src/fileExtension.ts","../src/minDimensions.ts","../src/maxDepth.ts","../src/requiredIfSlugEq.ts","../src/requiredIfSiblingEq.ts","../src/lib/getSibling.ts"],"sourcesContent":["import { ValidationContext } from \"sanity\"\n\nexport const referencedDocumentRequires = (\n documentType: string, \n field: string, \n message: string = `{documentType}’s {field} must be filled.`\n) => async (value: any | undefined, context: ValidationContext) => {\n if (!value?._ref) {\n return true\n }\n const client = context.getClient({ apiVersion: \"2022-08-12\" })\n // todo: use current API version, or test with no version at all\n\n // todo: if there's a value._type or value.referenced._type or something, we get rid of document.type from inputs\n const data = await client.fetch(`\n *[_type == \"${documentType}\" && _id == \"${value._ref}\"]{\n ${field}\n }[0]\n `) // TODO: why is typescript screaming about this? Fetch takes two parameters.\n if (!data[field]) {\n return message.replace(\"{documentType}\", documentType).replace(\"{field}\", field)\n }\n return true\n}\n","import { getExtension } from \"@sanity/asset-utils\"\nimport { FileValue } from \"sanity\"\n\nexport const fileExtension = (\n validFileExtension: string | Array<string>, \n message: string = `Image must be of type {validFileExtension}`\n) => (value: FileValue | undefined) => {\n if (!value || !value.asset) {\n return true\n }\n const validExtensions = typeof validFileExtension === \"string\" ? [validFileExtension] : validFileExtension\n const filetype = getExtension(value.asset._ref)\n if (!validExtensions.includes(filetype)) {\n return message.replace(\"{validFileExtension}\", validExtensions.join(\", or \"))\n }\n return true\n}\n","import { getImageDimensions } from \"@sanity/asset-utils\"\nimport { FileValue } from \"sanity\"\n\nexport const minDimensions =\n ({ x, y }: { x: number; y: number }, message?: string) =>\n (value: FileValue | undefined) => {\n if (!value || !value.asset) {\n return true\n }\n const { width, height } = getImageDimensions(value.asset._ref)\n if (!!x && width < x) {\n return message ? message.replace(\"{x}\", x.toString()).replace(\"{y}\", !y ? \"(any)\" : y.toString()) : `Image must be at least ${x} pixels wide.`\n }\n if (!!y && height < y) {\n return message ? message.replace(\"{x}\", !x ? \"(any)\" : x.toString()).replace(\"{y}\", y.toString()) : `Image must be at least ${y} pixels tall.`\n }\n return true\n }\n","import { ValidationContext } from \"sanity\"\n\nexport const maxDepth = (\n maxDepth: number, \n nestedValueName: string,\n message: string = `Error: You can only nest {nestedValueName} {maxDepth} levels deep.`\n) => (_: any, context: ValidationContext) => {\n let regex = new RegExp(String.raw`topLevelItems|${nestedValueName}`)\n const paths = (context.path as Array<any>).filter((e) => typeof e === \"string\" && e.match(regex))\n if (paths.length > maxDepth) {\n return message.replace(\"{nestedValueName}\", nestedValueName).replace(\"{maxDepth}\", maxDepth.toString())\n }\n return true\n}\n","import { ValidationContext } from \"sanity\"\n\n/*\nSanity has a funny idea of conditional fields. Every field is _always_ present, but it might be hidden.\nex. hidden: (node) => node.parent.slug === 'hideMe'\nThis works really well — unless a field marked as required gets hidden. \n\nThis validator conditionally marks a field as required only for specific slugs. It accepts a string or array of strings.\n```\nvalidation: (rule) => rule.custom(requiredIfSlugEq('alpha'))\nvalidation: (rule) => rule.custom(requiredIfSlugEq(['alpha', 'beta']))\nvalidation: (rule) => rule.custom(requiredIfSlugNotEq(['beta']))\n```\n\nIf the key of your slug is not simply \"slug\", fill that in the optional second parameter.\n```\nvalidation: (rule) => rule.custom(requiredIfSlugEq('alpha', 'id'))\n```\n\n\"Could this method be simpler if it just checked for the self.hidden state?\"\nNot possible, since the hidden state is not exposed to the context.\n\nBut even if it were, you wouldn't want to. There are valid reasons to make a component required but hidden.\nex. an admin- or developer-level identifier that you don't want civilians to see or edit.\n*/\n\nexport const requiredIfSlugEq = (\n slug: Array<string> | string, \n slugKey: string = \"slug\", \n message: string = `This is a required field.`\n) =>\n (value: unknown | undefined, context: ValidationContext) => {\n const slugs = typeof slug === \"string\" ? [slug] : slug\n const currentSlugValue = (context.parent as any)?.[slugKey]?.current\n \n // todo: does slugKey exist? If not, fail.\n // todo: deal with nested slugKey (ex. metadata.slug)\n \n if (!value && !!currentSlugValue && slugs.includes(currentSlugValue)) {\n return message.replace(\"{slugKey}\", slugKey).replace(\"{slug}\", slugs.join(', or '))\n }\n return true\n }","import {getSibling} from './'\nimport {ValidationContext} from 'sanity'\n\n/*\nFor a given object that has multiple fields, mark a field as `required` if a sibling has a particular value.\n\n```\ndefineType({\n name: 'ifAlphaAlsoBeta',\n type: 'object',\n fields: [\n defineField({\n name: 'alpha',\n type: 'string',\n options: {\n list: ['left', 'right'],\n layout: 'radio',\n direction: 'horizontal',\n },\n }),\n defineField({\n name: 'beta',\n type: 'string',\n placeholder: 'If alpha is “left”, I’m also required',\n validation: (rule) => rule.custom(requiredIfSiblingEq('alpha', 'left')),\n })\n ],\n})\n```\n\nIncidentally, context.path is technically Array<sanity.PathSegment>.\n\nThat shouldn't matter, but dealing with that and remapping siblingKey as a PathSegment could be a possible future enhancement.\n*/\n\nexport const requiredIfSiblingEq = (\n key: string, \n comparison: string | number | null | Array<string | number | null>, \n message: string = 'Required if {key} equals {value}.'\n) =>\n (value: unknown | undefined, context: ValidationContext) => {\n const sibling = getSibling(key, context)\n const comparisons = Array.isArray(comparison) ? comparison : [comparison]\n if (!value && comparisons.includes(sibling)) {\n return message.replace('{key}', key).replace('{value}', comparisons.join(', or ') ?? 'null')\n }\n return true\n }\n","import { get } from \"lodash-es\"\nimport { ValidationContext } from \"sanity\"\n\nexport const getSibling = (key: string | number, context: ValidationContext) => {\n const pathToParentObject = context.path!.slice(0, -1) as Array<string | number>\n const sibling = get(context.document, [...pathToParentObject, key])\n return sibling\n}\n\n/*\nTODO:\n There is an issue with finding a sibling when in an array element.\n If the context document looks something like this…\n {\n someArray: [\n {\n _key: 'abc123',\n targetSibling: 'herpderp'\n }\n ]\n }\n … we wind up with a path of…\n [ 'someArray', { _key: 'ab123' }, 'targetSibling' ]\n lodash.get() is trying to do an exact match, it doesn't know how to get object by _key.\n \n Will probably have to replace get() with a gnarly recursive lookup function.\n*/\n"],"mappings":"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","names":["maxDepth"]}
1
+ {"version":3,"sources":["../src/referencedDocumentRequires.ts","../src/fileExtension.ts","../src/minDimensions.ts","../src/maxDepth.ts","../src/requiredIfSlugEq.ts","../src/requiredIfSiblingEq.ts","../src/lib/getSibling.ts","../src/regex.ts"],"sourcesContent":["import { ValidationContext } from \"sanity\"\n\nexport const referencedDocumentRequires = (\n documentType: string, \n field: string, \n message: string = `{documentType}’s {field} must be filled.`\n) => async (value: any | undefined, context: ValidationContext) => {\n if (!value?._ref) {\n return true\n }\n const client = context.getClient({ apiVersion: \"2022-08-12\" })\n // todo: use current API version, or test with no version at all\n\n // todo: if there's a value._type or value.referenced._type or something, we get rid of document.type from inputs\n const data = await client.fetch(`\n *[_type == \"${documentType}\" && _id == \"${value._ref}\"]{\n ${field}\n }[0]\n `) // TODO: why is typescript screaming about this? Fetch takes two parameters.\n if (!data[field]) {\n return message.replace(\"{documentType}\", documentType).replace(\"{field}\", field)\n }\n return true\n}\n","import { getExtension } from \"@sanity/asset-utils\"\nimport { FileValue } from \"sanity\"\n\nexport const fileExtension = (\n validFileExtension: string | Array<string>, \n message: string = `Image must be of type {validFileExtension}`\n) => (value: FileValue | undefined) => {\n if (!value || !value.asset) {\n return true\n }\n const validExtensions = typeof validFileExtension === \"string\" ? [validFileExtension] : validFileExtension\n const filetype = getExtension(value.asset._ref)\n if (!validExtensions.includes(filetype)) {\n return message.replace(\"{validFileExtension}\", validExtensions.join(\", or \"))\n }\n return true\n}\n\n// todo: this should fail if its attached to a field that is not of type \"file\"","import { getImageDimensions } from \"@sanity/asset-utils\"\nimport { FileValue } from \"sanity\"\n\nexport const minDimensions =\n ({ x, y }: { x: number; y: number }, message?: string) =>\n (value: FileValue | undefined) => {\n if (!value || !value.asset) {\n return true\n }\n const { width, height } = getImageDimensions(value.asset._ref)\n if (!!x && width < x) {\n return message \n ? message.replace(\"{width}\", width.toString())\n .replace(\"{height}\", height.toString())\n .replace(\"{x}\", x.toString())\n .replace(\"{y}\", !y ? \"(any)\" : y.toString()) \n : `Image must be at least ${x} pixels wide.`\n }\n if (!!y && height < y) {\n return message \n ? message.replace(\"{width}\", width.toString())\n .replace(\"{height}\", height.toString())\n .replace(\"{x}\", !x ? \"(any)\" : x.toString())\n .replace(\"{y}\", y.toString())\n : `Image must be at least ${y} pixels tall.`\n }\n return true\n }\n\n// todo: this should fail if its attached to a field that is not of type \"image\"","import { ValidationContext } from \"sanity\"\n\nexport const maxDepth = (\n maxDepth: number, \n key: string,\n message: string = `Error: You can only nest {key} {maxDepth} levels deep.`\n) => (_: any, context: ValidationContext) => {\n let regex = new RegExp(String.raw`topLevelItems|${key}`)\n const paths = (context.path as Array<any>).filter((e) => typeof e === \"string\" && e.match(regex))\n if (paths.length > maxDepth) {\n return message\n .replace(\"{key}\", key)\n .replace(\"{nestedValueName}\", key) // backward compatibility\n .replace(\"{maxDepth}\", maxDepth.toString())\n }\n return true\n}\n","import { ValidationContext } from \"sanity\"\n\n/*\nSanity has a funny idea of conditional fields. Every field is _always_ present, but it might be hidden.\nex. hidden: (node) => node.parent.slug === 'hideMe'\nThis works really well — unless a field marked as required gets hidden. \n\nThis validator conditionally marks a field as required only for specific slugs. It accepts a string or array of strings.\n```\nvalidation: (rule) => rule.custom(requiredIfSlugEq('alpha'))\nvalidation: (rule) => rule.custom(requiredIfSlugEq(['alpha', 'beta']))\nvalidation: (rule) => rule.custom(requiredIfSlugNotEq(['beta']))\n```\n\nIf the key of your slug is not simply \"slug\", fill that in the optional second parameter.\n```\nvalidation: (rule) => rule.custom(requiredIfSlugEq('alpha', 'id'))\n```\n\n\"Could this method be simpler if it just checked for the self.hidden state?\"\nNot possible, since the hidden state is not exposed to the context.\n\nBut even if it were, you wouldn't want to. There are valid reasons to make a component required but hidden.\nex. an admin- or developer-level identifier that you don't want civilians to see or edit.\n*/\n\nexport const requiredIfSlugEq = (\n slug: Array<string> | string, \n slugKey: string = \"slug\", \n message: string = `This is a required field.`\n) =>\n (value: unknown | undefined, context: ValidationContext) => {\n const slugs = typeof slug === \"string\" ? [slug] : slug\n const slugValue = (context.parent as any)?.[slugKey]?.current\n \n // todo: does slugKey exist? If not, fail.\n // todo: deal with nested slugKey (ex. metadata.slug)\n \n if (!value && !!slugValue && slugs.includes(slugValue)) {\n return message\n .replace(\"{slugKey}\", slugKey)\n .replace(\"{operand}\", slugs.join(', or '))\n .replace(\"{siblingSlugValue}\", slugValue)\n }\n return true\n }","import {getSibling} from './'\nimport {ValidationContext} from 'sanity'\n\n/*\nFor a given object that has multiple fields, mark a field as `required` if a sibling has a particular value.\n\n```\ndefineType({\n name: 'ifAlphaAlsoBeta',\n type: 'object',\n fields: [\n defineField({\n name: 'alpha',\n type: 'string',\n options: {\n list: ['left', 'right'],\n layout: 'radio',\n direction: 'horizontal',\n },\n }),\n defineField({\n name: 'beta',\n type: 'string',\n placeholder: 'If alpha is “left”, I’m also required',\n validation: (rule) => rule.custom(requiredIfSiblingEq('alpha', 'left')),\n })\n ],\n})\n```\n\nIncidentally, context.path is technically Array<sanity.PathSegment>.\n\nThat shouldn't matter, but dealing with that and remapping siblingKey as a PathSegment could be a possible future enhancement.\n*/\n\nexport const requiredIfSiblingEq = (\n key: string, \n operand: string | number | null | Array<string | number | null>, \n message: string = 'Required if {key} equals {operand}.'\n) =>\n (value: unknown | undefined, context: ValidationContext) => {\n const siblingValue = getSibling(key, context)\n const operands = Array.isArray(operand) ? operand : [operand]\n if (!value && operands.includes(siblingValue)) {\n return message\n .replace('{key}', key)\n .replace('{operand}', operands.join(', or ') ?? 'null')\n .replace('{value}', operands.join(', or ') ?? 'null') // backward compatibility\n .replace('{siblingValue}', siblingValue)\n }\n return true\n }\n","import { get } from \"lodash-es\"\nimport { ValidationContext } from \"sanity\"\n\nexport const getSibling = (key: string | number, context: ValidationContext) => {\n const pathToParentObject = context.path!.slice(0, -1) as Array<string | number>\n const sibling = get(context.document, [...pathToParentObject, key])\n return sibling\n}\n\n/*\nTODO:\n There is an issue with finding a sibling when in an array element.\n If the context document looks something like this…\n {\n someArray: [\n {\n _key: 'abc123',\n targetSibling: 'herpderp'\n }\n ]\n }\n … we wind up with a path of…\n [ 'someArray', { _key: 'ab123' }, 'targetSibling' ]\n lodash.get() is trying to do an exact match, it doesn't know how to get object by _key.\n \n Will probably have to replace get() with a gnarly recursive lookup function.\n*/\n","export const regex =\n (pattern: RegExp, message: string = `“{value}” does not match the pattern {pattern}.`) =>\n (value: unknown) => {\n if (!value) {\n return true\n }\n const valueAsString = typeof value !== \"string\" ? value.toString() : value\n return pattern.test(valueAsString) ? true : message.replace(\"{value}\", valueAsString).replace(\"{pattern}\", pattern.toString())\n }\n"],"mappings":";AAEO,IAAM,6BAA6B,CACxC,cACA,OACA,UAAkB,oDACf,OAAO,OAAwB,YAA+B;AACjE,MAAI,EAAC,+BAAO,OAAM;AAChB,WAAO;AAAA,EACT;AACA,QAAM,SAAS,QAAQ,UAAU,EAAE,YAAY,aAAa,CAAC;AAI7D,QAAM,OAAO,MAAM,OAAO,MAAM;AAAA,kBAChB,YAAY,gBAAgB,MAAM,IAAI;AAAA,QAChD,KAAK;AAAA;AAAA,GAEV;AACD,MAAI,CAAC,KAAK,KAAK,GAAG;AAChB,WAAO,QAAQ,QAAQ,kBAAkB,YAAY,EAAE,QAAQ,WAAW,KAAK;AAAA,EACjF;AACA,SAAO;AACT;;;ACvBA,SAAS,oBAAoB;AAGtB,IAAM,gBAAgB,CAC3B,oBACA,UAAkB,iDACf,CAAC,UAAiC;AACrC,MAAI,CAAC,SAAS,CAAC,MAAM,OAAO;AAC1B,WAAO;AAAA,EACT;AACA,QAAM,kBAAkB,OAAO,uBAAuB,WAAW,CAAC,kBAAkB,IAAI;AACxF,QAAM,WAAW,aAAa,MAAM,MAAM,IAAI;AAC9C,MAAI,CAAC,gBAAgB,SAAS,QAAQ,GAAG;AACvC,WAAO,QAAQ,QAAQ,wBAAwB,gBAAgB,KAAK,OAAO,CAAC;AAAA,EAC9E;AACA,SAAO;AACT;;;AChBA,SAAS,0BAA0B;AAG5B,IAAM,gBACX,CAAC,EAAE,GAAG,EAAE,GAA6B,YACrC,CAAC,UAAiC;AAChC,MAAI,CAAC,SAAS,CAAC,MAAM,OAAO;AAC1B,WAAO;AAAA,EACT;AACA,QAAM,EAAE,OAAO,OAAO,IAAI,mBAAmB,MAAM,MAAM,IAAI;AAC7D,MAAI,CAAC,CAAC,KAAK,QAAQ,GAAG;AACpB,WAAO,UACH,QAAQ,QAAQ,WAAW,MAAM,SAAS,CAAC,EAC1C,QAAQ,YAAY,OAAO,SAAS,CAAC,EACrC,QAAQ,OAAO,EAAE,SAAS,CAAC,EAC3B,QAAQ,OAAO,CAAC,IAAI,UAAU,EAAE,SAAS,CAAC,IAC3C,0BAA0B,CAAC;AAAA,EACjC;AACA,MAAI,CAAC,CAAC,KAAK,SAAS,GAAG;AACrB,WAAO,UACH,QAAQ,QAAQ,WAAW,MAAM,SAAS,CAAC,EAC1C,QAAQ,YAAY,OAAO,SAAS,CAAC,EACrC,QAAQ,OAAO,CAAC,IAAI,UAAU,EAAE,SAAS,CAAC,EAC1C,QAAQ,OAAO,EAAE,SAAS,CAAC,IAC5B,0BAA0B,CAAC;AAAA,EACjC;AACA,SAAO;AACT;;;ACzBK,IAAM,WAAW,CACtBA,WACA,KACA,UAAkB,6DACf,CAAC,GAAQ,YAA+B;AAC3C,MAAIC,SAAQ,IAAI,OAAO,OAAO,oBAAoB,GAAG,EAAE;AACvD,QAAM,QAAS,QAAQ,KAAoB,OAAO,CAAC,MAAM,OAAO,MAAM,YAAY,EAAE,MAAMA,MAAK,CAAC;AAChG,MAAI,MAAM,SAASD,WAAU;AAC3B,WAAO,QACJ,QAAQ,SAAS,GAAG,EACpB,QAAQ,qBAAqB,GAAG,EAChC,QAAQ,cAAcA,UAAS,SAAS,CAAC;AAAA,EAC9C;AACA,SAAO;AACT;;;ACUO,IAAM,mBAAmB,CAC9B,MACA,UAAkB,QAClB,UAAkB,gCAElB,CAAC,OAA4B,YAA+B;AA/B9D;AAgCI,QAAM,QAAQ,OAAO,SAAS,WAAW,CAAC,IAAI,IAAI;AAClD,QAAM,aAAa,mBAAQ,WAAR,mBAAyB,aAAzB,mBAAmC;AAKtD,MAAI,CAAC,SAAS,CAAC,CAAC,aAAa,MAAM,SAAS,SAAS,GAAG;AACtD,WAAO,QACJ,QAAQ,aAAa,OAAO,EAC5B,QAAQ,aAAa,MAAM,KAAK,OAAO,CAAC,EACxC,QAAQ,sBAAsB,SAAS;AAAA,EAC5C;AACA,SAAO;AACT;;;ACVK,IAAM,sBAAsB,CACjC,KACA,SACA,UAAkB,0CAElB,CAAC,OAA4B,YAA+B;AAxC9D;AAyCI,QAAM,eAAe,WAAW,KAAK,OAAO;AAC5C,QAAM,WAAW,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO;AAC5D,MAAI,CAAC,SAAS,SAAS,SAAS,YAAY,GAAG;AAC7C,WAAO,QACJ,QAAQ,SAAS,GAAG,EACpB,QAAQ,cAAa,cAAS,KAAK,OAAO,MAArB,YAA0B,MAAM,EACrD,QAAQ,YAAW,cAAS,KAAK,OAAO,MAArB,YAA0B,MAAM,EACnD,QAAQ,kBAAkB,YAAY;AAAA,EAC3C;AACA,SAAO;AACT;;;ACnDF,SAAS,WAAW;AAGb,IAAM,aAAa,CAAC,KAAsB,YAA+B;AAC9E,QAAM,qBAAqB,QAAQ,KAAM,MAAM,GAAG,EAAE;AACpD,QAAM,UAAU,IAAI,QAAQ,UAAU,CAAC,GAAG,oBAAoB,GAAG,CAAC;AAClE,SAAO;AACT;;;ACPO,IAAM,QACX,CAAC,SAAiB,UAAkB,gEACpC,CAAC,UAAmB;AAClB,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,QAAM,gBAAgB,OAAO,UAAU,WAAW,MAAM,SAAS,IAAI;AACrE,SAAO,QAAQ,KAAK,aAAa,IAAI,OAAO,QAAQ,QAAQ,WAAW,aAAa,EAAE,QAAQ,aAAa,QAAQ,SAAS,CAAC;AAC/H;","names":["maxDepth","regex"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package-name": "sanity-advanced-validators",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Advanced input validation tools for Sanity CMS.",
5
5
  "author": "Eric_WVGG",
6
6
  "license": "MIT",
@@ -22,13 +22,13 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "@sanity/asset-utils": "^2.2.1",
25
- "lodash-es": "^4.0.0",
26
- "sanity": "^4.0.0"
25
+ "lodash-es": "^4.17.0",
26
+ "sanity": "^3.9.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@types/lodash-es": "^4.0.0",
29
+ "@types/lodash-es": "^4.17.0",
30
30
  "tsup": "^8.5.0",
31
- "typescript": "^5.9.2",
31
+ "typescript": "^5.8.0",
32
32
  "vitest": "^3.2.4"
33
33
  },
34
34
  "name": "sanity-advanced-validators",