sanity-advanced-validators 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,8 @@
1
- *🚓 Never trust a user! 👮*
1
+ _🚓 Never trust a user! 👮_
2
2
 
3
3
  # Sanity Advanced Validators
4
4
 
5
- This package includes a set of Sanity validators for aggressive and weird edge cases. *Maintain sanity with micro-managed validation.*
6
-
7
- Note: Every validator can accept an optional custom error message as its last parameter. See [minDimensions](#minDimensions) for an example.
5
+ This package includes a set of Sanity validators for aggressive and weird edge cases. _Maintain sanity with micro-managed validation._
8
6
 
9
7
  ## Tools
10
8
 
@@ -15,12 +13,14 @@ Note: Every validator can accept an optional custom error message as its last pa
15
13
  - [requiredIfSiblingNeq](#requiredIfSiblingNeq)
16
14
  - [requiredIfSlugEq](#requiredIfSlugEq)
17
15
  - [requiredIfSlugNeq](#requiredIfSlugNeq)
16
+ - [regex](#regex) 🆕
18
17
  - [referencedDocumentRequires](#referencedDocumentRequires)
19
18
  - [maxDepth](#maxDepth)
20
19
 
21
20
  ## Mega-example
22
21
 
23
22
  Imagine that you’ve got a document that has an optional video file, but…
23
+
24
24
  - it’s required on the `/about` page
25
25
  - if the video exists, it must be either **MP4** or **MOV**
26
26
  - and there must be a poster image that's between **1250x800** and **2500x1600** pixels in size
@@ -38,7 +38,7 @@ const Page = defineType({
38
38
  name: "someVideoFile",
39
39
  type: "file",
40
40
  validation: (rule) =>
41
- rule.custom(requiredIfSlugEq('about'))
41
+ rule.custom(requiredIfSlugEq('about', 'A video is required if {slugKey} is {operand}.'))
42
42
  .custom(fileExtension(['mp4', 'mov']))
43
43
  })
44
44
  defineField({
@@ -60,6 +60,11 @@ const Page = defineType({
60
60
 
61
61
  Enforces that an uploaded file asset is of a certain format.
62
62
 
63
+ ```typescript
64
+ fileType: string | Array<string>,
65
+ message?: string // optional custom error message; replaces {validFileExtension} with fileType (flattened)
66
+ ```
67
+
63
68
  ```typescript
64
69
  import { fileExtension } from "sanity-advanced-validation"
65
70
 
@@ -81,17 +86,16 @@ const Page = defineType({
81
86
  })
82
87
  ```
83
88
 
84
- #### parameters
85
- ```typescript
86
- fileType: string | Array<string>,
87
- message?: string // optional custom error message; replaces {validFileExtension} with fileType (flattened)
88
- ```
89
-
90
89
  ---
91
90
 
92
91
  ### minDimensions
93
92
 
94
- Enforces that an uploaded image asset is at minimum certain dimensions.
93
+ Enforces that an uploaded image asset is at minimum certain dimensions. You can test on both, just x, or just y.
94
+
95
+ ```typescript
96
+ dimensions: {x?: number, y?: number},
97
+ message?: string // optional custom error message; replaces {x} and {y} with your dimension requirements, and {width} and {height} with submitted image dimensions
98
+ ```
95
99
 
96
100
  ```typescript
97
101
  import { minDimensions } from "sanity-advanced-validation"
@@ -110,35 +114,17 @@ const ImageWithCaption = defineType({
110
114
  })
111
115
  ```
112
116
 
113
- You can also enforce on only one dimension, or feed a custom error message:
117
+ ---
114
118
 
115
- ```typescript
116
- defineField({
117
- name: "heroImage",
118
- type: "image",
119
- description: "At least 1200px wide; as tall as you like.",
120
- validation: (rule) => rule.custom(
121
- minDimensions(
122
- { x: 1200 },
123
- "Uh oh, your image is {width} pixels wide. That’s less than {x}!"
124
- )
125
- ),
126
- })
127
- ```
119
+ ### maxDimensions
120
+
121
+ Enforces that an uploaded image asset is at most certain dimensions. You can test on both, just x, or just y.
128
122
 
129
- #### parameters
130
123
  ```typescript
131
- dimensions: {x?: number, y?: number},
124
+ dimensions: {x?: number, y?: number},
132
125
  message?: string // optional custom error message; replaces {x} and {y} with your dimension requirements, and {width} and {height} with submitted image dimensions
133
126
  ```
134
127
 
135
- ---
136
-
137
-
138
- ### maxDimensions
139
-
140
- Enforces that an uploaded image asset is at most certain dimensions.
141
-
142
128
  ```typescript
143
129
  defineField({
144
130
  name: "heroImage",
@@ -162,23 +148,22 @@ defineField({
162
148
  })
163
149
  ```
164
150
 
165
- #### parameters
166
- ```typescript
167
- dimensions: {x?: number, y?: number},
168
- message?: string // optional custom error message; replaces {x} and {y} with your dimension requirements, and {width} and {height} with submitted image dimensions
169
- ```
170
-
171
151
  ---
172
152
 
173
-
174
153
  ### requiredIfSiblingEq
175
154
 
176
- Mark a field as `required` if a sibling field has a particular value. This is the validator we use most. *It’s super effective!*
155
+ Mark a field as `required` if a sibling field has a particular value. This is the validator we use most. _It’s super effective!_
177
156
 
178
- This is handy if you have a field that is hidden under some circumstances, but is `required()` when it’s visible.
157
+ This is handy if you have a field that is hidden under some circumstances, but is `required()` when it’s visible.
179
158
 
180
159
  _note:_ This does not work for slugs, because they have to match a nested `.current` value. Use the [requiredIfSlugEq validator](#requiredIfSlugEq) instead.
181
160
 
161
+ ```typescript
162
+ key: string, // name of sibling
163
+ operand: string | number | boolean | null | Array<string, number> // value that you’re testing for (i.e. if 'name' === operand)
164
+ 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
+ ```
166
+
182
167
  ```typescript
183
168
  import {requiredIfSiblingEq} from 'sanity-advanced-validation'
184
169
 
@@ -202,7 +187,7 @@ defineType({
202
187
  type: 'string',
203
188
  options: {
204
189
  list: [
205
- 'javascript', 'rust', 'python', 'swift'
190
+ 'typescript', 'rust', 'python', 'swift'
206
191
  ]
207
192
  },
208
193
  validation: rule => rule.custom(requiredIfSiblingEq('occupation', 'software engineer')),
@@ -212,35 +197,35 @@ defineType({
212
197
  })
213
198
  ```
214
199
 
215
- “If not that, then this.” This also works for null.
200
+ And it also works for arrays.
216
201
 
217
202
  ```typescript
218
203
  defineType({
219
- name: 'person',
220
- type: 'object',
204
+ name: "person",
205
+ type: "object",
221
206
  fields: [
207
+ // ...
222
208
  defineField({
223
- name: 'name',
224
- type: 'string'
209
+ name: "occupation",
210
+ type: "string",
211
+ options: {
212
+ list: ["doctor", "lawyer", "software engineer", "linguist"],
213
+ },
225
214
  }),
226
215
  defineField({
227
- name: 'email',
228
- type: 'string',
229
- })
230
- defineField({
231
- name: 'phone',
232
- type: 'string',
233
- validation: rule => rule.custom(requiredIfSiblingEq(
234
- 'email',
235
- null,
236
- "If you don’t have an email address, a phone number is required."
237
- ))
238
- })
216
+ name: "favoriteLanguage",
217
+ type: "string",
218
+ options: {
219
+ list: ["typescript", "rust", "python", "swift", "latin", "urdu", "klingon"],
220
+ },
221
+ validation: (rule) => rule.custom(requiredIfSiblingEq("occupation", ["software engineer", "linguist"])),
222
+ hidden: ({ parent }) => !["software engineer", "linguist"].includes(parent.occupation),
223
+ }),
239
224
  ],
240
225
  })
241
226
  ```
242
227
 
243
- And it even works for arrays.
228
+ “If not that, then this.” It even works for null.
244
229
 
245
230
  ```typescript
246
231
  defineType({
@@ -252,43 +237,36 @@ defineType({
252
237
  type: 'string'
253
238
  }),
254
239
  defineField({
255
- name: 'name',
256
- type: 'string'
257
- }),
258
- defineField({
259
- name: 'occupation',
240
+ name: 'email',
260
241
  type: 'string',
261
- options: {
262
- list: ['doctor', 'lawyer', 'software engineer']
263
- }
264
- }),
242
+ })
265
243
  defineField({
266
- name: 'explanation',
267
- description: 'Why are you wasting your life this way?',
268
- type: 'text',
269
- validation: rule => rule.custom(requiredIfSiblingEq('occupation', ['doctor', 'lawyer'])),
270
- hidden: ({parent}) => parent.occuption === 'software engineer',
244
+ name: 'phone',
245
+ type: 'string',
246
+ validation: rule => rule.custom(requiredIfSiblingEq(
247
+ 'email',
248
+ null,
249
+ "If you don’t have an email address, a phone number is required."
250
+ ))
271
251
  })
272
252
  ],
273
253
  })
274
254
  ```
275
255
 
276
- #### parameters
277
- ```typescript
278
- key: string, // name of sibling
279
- operand: string | number | null | Array<string, number, null> // value that you’re testing for (i.e. if 'name' === operand)
280
- 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.
281
- ```
282
-
283
256
  ---
284
257
 
285
-
286
258
  ### requiredIfSiblingNeq
287
259
 
288
- For a given object that has multiple fields, mark a field as `required` if a sibling does _not_ have a particular value.
260
+ 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).
289
261
 
290
262
  _note:_ This does not work for slugs, because they have to match a nested `.current` value. Use the [requiredIfSlugNeq validator](#requiredIfSlugNeq) instead.
291
263
 
264
+ ```typescript
265
+ key: string, // name of sibling
266
+ operand: string | number | boolean | null | Array<string, number> // value that you’re testing for (i.e. if 'name' === operand)
267
+ 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.
268
+ ```
269
+
292
270
  ```typescript
293
271
  import {requiredIfSiblingNeq} from 'sanity-advanced-validation'
294
272
 
@@ -308,50 +286,46 @@ defineType({
308
286
  }
309
287
  })
310
288
  defineField({
311
- name: 'why',
312
- description: 'How many years will you spend paying off your degree?',
313
- type: 'number',
314
- validation: rule => rule.custom(requiredIfSiblingNeq('occupation', 'software engineer'))
315
- }),
316
- ],
289
+ name: "explanation",
290
+ description: "Why are you wasting your life this way?",
291
+ type: "text",
292
+ validation: (rule) => rule.custom(requiredIfSiblingNeq("occupation", "software engineer")),
293
+ hidden: ({ parent }) => parent.occuption === "software engineer",
294
+ }), ],
317
295
  })
318
296
  ```
319
297
 
320
- #### parameters
321
- ```typescript
322
- key: string, // name of sibling
323
- operand: string | number | null | Array<string, number, null> // value that you’re testing for (i.e. if 'name' === operand)
324
- 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.
325
- ```
326
-
327
298
  ---
328
299
 
329
-
330
300
  ### requiredIfSlugEq
331
301
 
332
- Require for matching slugs.
302
+ Mark a field as `required` for documents with matching slugs.
333
303
 
334
304
  ```typescript
335
- import {requiredIfSlugEq} from 'sanity-advanced-validation'
305
+ operand: string | number | null | Array<string, number> // possible slug or slugs you’re testing
306
+ key?: string, // name of sibling if not "slug"
307
+ 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.
308
+ ```
309
+
310
+ ```typescript
311
+ import { requiredIfSlugEq } from "sanity-advanced-validation"
336
312
 
337
313
  defineType({
338
- name: 'page',
339
- type: 'document',
314
+ name: "page",
315
+ type: "document",
340
316
  fields: [
341
317
  defineField({
342
- name: 'slug',
343
- type: 'slug'
318
+ name: "slug",
319
+ type: "slug",
344
320
  }),
345
321
  defineField({
346
- name: 'questionsAndAnswers',
347
- type: 'array',
348
- of: [
349
- {type: 'qaItem'}
350
- ],
351
- validation: rule => rule.custom(requiredIfSlugEq('faq')),
352
- hidden: ({parent}) => parent.slug.current !== 'faq'
353
- })
354
- ]
322
+ name: "questionsAndAnswers",
323
+ type: "array",
324
+ of: [{ type: "qaItem" }],
325
+ validation: (rule) => rule.custom(requiredIfSlugEq("faq")),
326
+ hidden: ({ parent }) => parent.slug.current !== "faq",
327
+ }),
328
+ ],
355
329
  })
356
330
  ```
357
331
 
@@ -364,59 +338,85 @@ defineField({
364
338
  }),
365
339
  ```
366
340
 
367
- #### parameters
368
- ```typescript
369
- operand: string | number | null | Array<string, number, null> // possible slug or slugs you’re testing
370
- key?: string, // name of sibling if not "slug"
371
- 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.
372
- ```
373
-
374
341
  ---
375
342
 
376
-
377
343
  ### requiredIfSlugNeq
378
344
 
379
345
  Require fields on pages that don't match one or more slugs.
380
346
 
381
347
  ```typescript
382
- import {requiredIfSlugNeq} from 'sanity-advanced-validation'
348
+ operand: string | number | null | Array<string, number> // possible slug or slugs you’re testing
349
+ key?: string, // name of sibling if not "slug"
350
+ 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.
351
+ ```
352
+
353
+ ```typescript
354
+ import { requiredIfSlugNeq } from "sanity-advanced-validation"
383
355
 
384
356
  defineType({
385
- name: 'page',
386
- type: 'document',
357
+ name: "page",
358
+ type: "document",
387
359
  fields: [
388
360
  defineField({
389
- name: 'slug',
390
- type: 'slug'
361
+ name: "slug",
362
+ type: "slug",
391
363
  }),
392
364
  defineField({
393
- name: 'subnav',
365
+ name: "subnav",
394
366
  description: `Subnav is required on documents that aren’t '/home'`,
395
- type: 'array',
396
- of: [
397
- {type: 'navLink'}
398
- ],
399
- validation: rule => rule.custom(requiredIfSlugNeq('home')),
400
- hidden: ({parent}) => parent.slug.current !== 'home'
401
- })
402
- ]
367
+ type: "array",
368
+ of: [{ type: "navLink" }],
369
+ validation: (rule) => rule.custom(requiredIfSlugNeq("home")),
370
+ hidden: ({ parent }) => parent.slug.current !== "home",
371
+ }),
372
+ ],
403
373
  })
404
374
  ```
405
375
 
406
- #### parameters
376
+ ---
377
+
378
+ ### regex
379
+
380
+ Easily test any value against a regular expression.
381
+
382
+ Values can be of type string, number, boolean… even objects!
383
+
384
+ ```typescript
385
+ pattern: RegExp // regular expression
386
+ message?: string // optional custom error message; replaces {pattern} with your input and {value} as submitted field value
387
+ ```
388
+
407
389
  ```typescript
408
- operand: string | number | null | Array<string, number, null> // possible slug or slugs you’re testing
409
- key?: string, // name of sibling if not "slug"
410
- 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.
390
+ defineField({
391
+ name: 'email',
392
+ type: 'string',
393
+ validation: (rule) => rule.custom(
394
+ regex(
395
+ /^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})*$/,
396
+ "“{value}” is not a valid email address."
397
+ )
398
+ ),
399
+ }),
411
400
  ```
412
401
 
413
- ---
402
+ **Custom error messages are highly recommended here.** Without the custom message above, the default response would be:
403
+
404
+ ```
405
+ “me@googlecom” does not match the pattern /^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})*$/.
406
+ ```
414
407
 
408
+ ---
415
409
 
416
410
  ### referencedDocumentRequires
417
411
 
418
412
  You might want to enforce some validation on a referenced document. This validator enforces that a given value is not null in the referenced document.
419
413
 
414
+ ```typescript
415
+ documentType: string // type of document you’re referring to
416
+ field: string, // name of document field that is required
417
+ message?: string // optional custom error message; replaces {documentType} and {field} with your input
418
+ ```
419
+
420
420
  ```typescript
421
421
  defineField({
422
422
  name: 'referredArticle',
@@ -427,16 +427,8 @@ defineField({
427
427
  }),
428
428
  ```
429
429
 
430
- #### parameters
431
- ```typescript
432
- documentType: string // type of document you’re referring to
433
- field: string, // name of document field that is required
434
- message?: string // optional custom error message; replaces {documentType} and {field} with your input
435
- ```
436
-
437
430
  ---
438
431
 
439
-
440
432
  ### maxDepth
441
433
 
442
434
  It can be useful to have a nested type. This often comes up when making some kind of navigation tree, like…
@@ -490,6 +482,12 @@ const navLink = defineType({
490
482
 
491
483
  … but your users might get a little stupid with this, and you may want to enforce navigations only going _n_ layers deep.
492
484
 
485
+ ```typescript
486
+ maxDepth: number // maximum "depth" of embedding (including parent)
487
+ key: string, // name of the field that includes the cursive value (i.e. the field’s own name)
488
+ message?: string // optional custom error message; replaces {maxDepth} and {key} with your input
489
+ ```
490
+
493
491
  ```typescript
494
492
  import { maxDepth } from "sanity-advanced-validation"
495
493
 
@@ -509,17 +507,8 @@ const navLink = defineType({
509
507
 
510
508
  This will enforce that a subnav list can embed in a subnav, which can also be embedded in a subnav — but no further.
511
509
 
512
-
513
- #### parameters
514
- ```typescript
515
- maxDepth: number // maximum "depth" of embedding (including parent)
516
- key: string, // name of the field that includes the cursive value (i.e. the field’s own name)
517
- message?: string // optional custom error message; replaces {maxDepth} and {key} with your input
518
- ```
519
-
520
510
  ---
521
511
 
522
-
523
512
  #### Note to any Sanity dev who looks at this
524
513
 
525
514
  I’d love to include similar logic on my `hidden:` attribute, but I don’t think that’t possible without a `path` array in `hidden`’s `ConditionalPropertyCallbackContext` that’s similar to the one fed to the `ValidationContext` (todo: type this correctly). Wouldn’t this be cool?
@@ -543,7 +532,7 @@ defineField({
543
532
 
544
533
  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.
545
534
 
546
- ## Upcoming
535
+ ## Roadmap
547
536
 
548
537
  ### Nested pathfinders
549
538
 
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":"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","names":["maxDepth","regex"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package-name": "sanity-advanced-validators",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Advanced input validation tools for Sanity CMS.",
5
5
  "author": "Eric_WVGG",
6
6
  "license": "MIT",
@@ -18,17 +18,17 @@
18
18
  "module": "./dist/index.cjs",
19
19
  "types": "./dist/index.d.ts",
20
20
  "files": [
21
- "dist"
22
- ],
21
+ "dist"
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",