sanity-advanced-validators 0.2.0 → 0.3.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 +166 -73
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
*🚓 Never trust a user! 👮*
|
|
2
|
+
|
|
1
3
|
# Sanity Advanced Validators
|
|
2
4
|
|
|
3
|
-
This package includes a set of Sanity validators for aggressive and weird edge cases.
|
|
5
|
+
This package includes a set of Sanity validators for aggressive and weird edge cases. *Maintain sanity with micro-managed validation.*
|
|
4
6
|
|
|
5
|
-
Note
|
|
7
|
+
Note: Every validator can accept an optional custom error message as its last parameter. See [minDimensions](#minDimensions) for an example.
|
|
6
8
|
|
|
7
9
|
## Tools
|
|
8
10
|
|
|
@@ -18,7 +20,10 @@ Note that every validator can accept an optional custom error message as its las
|
|
|
18
20
|
|
|
19
21
|
## Mega-example
|
|
20
22
|
|
|
21
|
-
Imagine that you’ve got a document that has an optional video file
|
|
23
|
+
Imagine that you’ve got a document that has an optional video file, but…
|
|
24
|
+
- it’s required on the `/about` page
|
|
25
|
+
- if the video exists, it must be either **MP4** or **MOV**
|
|
26
|
+
- and there must be a poster image that's between **1250x800** and **2500x1600** pixels in size
|
|
22
27
|
|
|
23
28
|
```typescript
|
|
24
29
|
const Page = defineType({
|
|
@@ -51,7 +56,7 @@ const Page = defineType({
|
|
|
51
56
|
|
|
52
57
|
## Examples
|
|
53
58
|
|
|
54
|
-
###
|
|
59
|
+
### fileExtension
|
|
55
60
|
|
|
56
61
|
Enforces that an uploaded file asset is of a certain format.
|
|
57
62
|
|
|
@@ -65,7 +70,7 @@ const Page = defineType({
|
|
|
65
70
|
defineField({
|
|
66
71
|
name: "catalog",
|
|
67
72
|
type: "file",
|
|
68
|
-
validation: (rule) => rule.custom(
|
|
73
|
+
validation: (rule) => rule.custom(fileExtension("pdf")),
|
|
69
74
|
}),
|
|
70
75
|
defineField({
|
|
71
76
|
name: "video",
|
|
@@ -76,6 +81,14 @@ const Page = defineType({
|
|
|
76
81
|
})
|
|
77
82
|
```
|
|
78
83
|
|
|
84
|
+
#### parameters
|
|
85
|
+
```typescript
|
|
86
|
+
fileType: string | Array<string>,
|
|
87
|
+
message?: string // optional custom error message; replaces {validFileExtension} with fileType (flattened)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
79
92
|
### minDimensions
|
|
80
93
|
|
|
81
94
|
Enforces that an uploaded image asset is at minimum certain dimensions.
|
|
@@ -84,17 +97,14 @@ Enforces that an uploaded image asset is at minimum certain dimensions.
|
|
|
84
97
|
import { minDimensions } from "sanity-advanced-validation"
|
|
85
98
|
|
|
86
99
|
const ImageWithCaption = defineType({
|
|
87
|
-
name: "
|
|
100
|
+
name: "article",
|
|
88
101
|
type: "object",
|
|
89
102
|
fields: [
|
|
103
|
+
// …
|
|
90
104
|
defineField({
|
|
91
|
-
name: "
|
|
92
|
-
type: "string",
|
|
93
|
-
}),
|
|
94
|
-
defineField({
|
|
95
|
-
name: "image",
|
|
105
|
+
name: "heroImage",
|
|
96
106
|
type: "image",
|
|
97
|
-
validation: (rule) => rule.custom(minDimensions({ x:
|
|
107
|
+
validation: (rule) => rule.custom(minDimensions({ x: 1200, y: 800 })),
|
|
98
108
|
}),
|
|
99
109
|
],
|
|
100
110
|
})
|
|
@@ -104,60 +114,68 @@ You can also enforce on only one dimension, or feed a custom error message:
|
|
|
104
114
|
|
|
105
115
|
```typescript
|
|
106
116
|
defineField({
|
|
107
|
-
name: "
|
|
117
|
+
name: "heroImage",
|
|
108
118
|
type: "image",
|
|
109
|
-
description: "At least
|
|
119
|
+
description: "At least 1200px wide; as tall as you like.",
|
|
110
120
|
validation: (rule) => rule.custom(
|
|
111
121
|
minDimensions(
|
|
112
|
-
{ x:
|
|
122
|
+
{ x: 1200 },
|
|
113
123
|
"Uh oh, your image is {width} pixels wide. That’s less than {x}!"
|
|
114
124
|
)
|
|
115
125
|
),
|
|
116
126
|
})
|
|
117
127
|
```
|
|
118
128
|
|
|
129
|
+
#### parameters
|
|
130
|
+
```typescript
|
|
131
|
+
dimensions: {x?: number, y?: number},
|
|
132
|
+
message?: string // optional custom error message; replaces {x} and {y} with your dimension requirements, and {width} and {height} with submitted image dimensions
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
|
|
119
138
|
### maxDimensions
|
|
120
139
|
|
|
121
140
|
Enforces that an uploaded image asset is at most certain dimensions.
|
|
122
141
|
|
|
123
142
|
```typescript
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
fields: [
|
|
130
|
-
defineField({
|
|
131
|
-
name: "caption",
|
|
132
|
-
type: "string",
|
|
133
|
-
}),
|
|
134
|
-
defineField({
|
|
135
|
-
name: "image",
|
|
136
|
-
type: "image",
|
|
137
|
-
validation: (rule) => rule.custom(maxDimensions({ x: 2000, y: 2000 })),
|
|
138
|
-
}),
|
|
139
|
-
],
|
|
140
|
-
})
|
|
143
|
+
defineField({
|
|
144
|
+
name: "heroImage",
|
|
145
|
+
type: "image",
|
|
146
|
+
validation: (rule) => rule.custom(maxDimensions({ x: 2400, y: 1600 })),
|
|
147
|
+
}),
|
|
141
148
|
```
|
|
142
149
|
|
|
143
150
|
Chain for min and max dimensions:
|
|
144
151
|
|
|
145
152
|
```typescript
|
|
146
153
|
defineField({
|
|
147
|
-
name: "
|
|
154
|
+
name: "heroImage",
|
|
148
155
|
type: "image",
|
|
149
|
-
description: "Min:
|
|
156
|
+
description: "Min: 1200x800, max: 2400x1600.",
|
|
150
157
|
validation: (rule) =>
|
|
151
158
|
rule
|
|
152
159
|
.required()
|
|
153
|
-
.custom(minDimensions({ x:
|
|
154
|
-
.custom(maxDimensions({ x:
|
|
160
|
+
.custom(minDimensions({ x: 1200, y: 800 }))
|
|
161
|
+
.custom(maxDimensions({ x: 2400, y: 1600 })),
|
|
155
162
|
})
|
|
156
163
|
```
|
|
157
164
|
|
|
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
|
+
---
|
|
172
|
+
|
|
173
|
+
|
|
158
174
|
### requiredIfSiblingEq
|
|
159
175
|
|
|
160
|
-
|
|
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!*
|
|
177
|
+
|
|
178
|
+
This is handy if you have a field that is hidden under some circumstances, but is `required()` when it’s visible.
|
|
161
179
|
|
|
162
180
|
_note:_ This does not work for slugs, because they have to match a nested `.current` value. Use the [requiredIfSlugEq validator](#requiredIfSlugEq) instead.
|
|
163
181
|
|
|
@@ -186,15 +204,15 @@ defineType({
|
|
|
186
204
|
list: [
|
|
187
205
|
'javascript', 'rust', 'python', 'swift'
|
|
188
206
|
]
|
|
189
|
-
}
|
|
207
|
+
},
|
|
208
|
+
validation: rule => rule.custom(requiredIfSiblingEq('occupation', 'software engineer')),
|
|
190
209
|
hidden: ({parent}) => parent.occuption !== 'software engineer',
|
|
191
|
-
validation: rule => rule.custom(requiredIfSiblingEq('occupation', 'software engineer'))
|
|
192
210
|
}),
|
|
193
211
|
],
|
|
194
212
|
})
|
|
195
213
|
```
|
|
196
214
|
|
|
197
|
-
This also works for null.
|
|
215
|
+
“If not that, then this.” This also works for null.
|
|
198
216
|
|
|
199
217
|
```typescript
|
|
200
218
|
defineType({
|
|
@@ -215,7 +233,7 @@ defineType({
|
|
|
215
233
|
validation: rule => rule.custom(requiredIfSiblingEq(
|
|
216
234
|
'email',
|
|
217
235
|
null,
|
|
218
|
-
"If you don’t have an email address,
|
|
236
|
+
"If you don’t have an email address, a phone number is required."
|
|
219
237
|
))
|
|
220
238
|
})
|
|
221
239
|
],
|
|
@@ -233,34 +251,38 @@ defineType({
|
|
|
233
251
|
name: 'name',
|
|
234
252
|
type: 'string'
|
|
235
253
|
}),
|
|
236
|
-
|
|
237
|
-
name: '
|
|
238
|
-
type: '
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
description: 'Why are you wasting your life this way?',
|
|
254
|
-
type: 'text',
|
|
255
|
-
hidden: ({parent}) => parent.occuption === 'software engineer',
|
|
256
|
-
validation: rule => rule.custom(requiredIfSiblingEq('occupation', ['doctor', 'lawyer']))
|
|
257
|
-
})
|
|
258
|
-
],
|
|
254
|
+
defineField({
|
|
255
|
+
name: 'name',
|
|
256
|
+
type: 'string'
|
|
257
|
+
}),
|
|
258
|
+
defineField({
|
|
259
|
+
name: 'occupation',
|
|
260
|
+
type: 'string',
|
|
261
|
+
options: {
|
|
262
|
+
list: ['doctor', 'lawyer', 'software engineer']
|
|
263
|
+
}
|
|
264
|
+
}),
|
|
265
|
+
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',
|
|
259
271
|
})
|
|
260
272
|
],
|
|
261
273
|
})
|
|
262
274
|
```
|
|
263
275
|
|
|
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
|
+
---
|
|
284
|
+
|
|
285
|
+
|
|
264
286
|
### requiredIfSiblingNeq
|
|
265
287
|
|
|
266
288
|
For a given object that has multiple fields, mark a field as `required` if a sibling does _not_ have a particular value.
|
|
@@ -287,15 +309,24 @@ defineType({
|
|
|
287
309
|
})
|
|
288
310
|
defineField({
|
|
289
311
|
name: 'why',
|
|
290
|
-
description: '
|
|
291
|
-
type: '
|
|
292
|
-
hidden: ({parent}) => parent.occuption === 'software engineer',
|
|
312
|
+
description: 'How many years will you spend paying off your degree?',
|
|
313
|
+
type: 'number',
|
|
293
314
|
validation: rule => rule.custom(requiredIfSiblingNeq('occupation', 'software engineer'))
|
|
294
315
|
}),
|
|
295
316
|
],
|
|
296
317
|
})
|
|
297
318
|
```
|
|
298
319
|
|
|
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
|
+
---
|
|
328
|
+
|
|
329
|
+
|
|
299
330
|
### requiredIfSlugEq
|
|
300
331
|
|
|
301
332
|
Require for matching slugs.
|
|
@@ -317,7 +348,7 @@ defineType({
|
|
|
317
348
|
of: [
|
|
318
349
|
{type: 'qaItem'}
|
|
319
350
|
],
|
|
320
|
-
validation: rule => rule.custom(requiredIfSlugEq('faq'))
|
|
351
|
+
validation: rule => rule.custom(requiredIfSlugEq('faq')),
|
|
321
352
|
hidden: ({parent}) => parent.slug.current !== 'faq'
|
|
322
353
|
})
|
|
323
354
|
]
|
|
@@ -333,6 +364,16 @@ defineField({
|
|
|
333
364
|
}),
|
|
334
365
|
```
|
|
335
366
|
|
|
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
|
+
---
|
|
375
|
+
|
|
376
|
+
|
|
336
377
|
### requiredIfSlugNeq
|
|
337
378
|
|
|
338
379
|
Require fields on pages that don't match one or more slugs.
|
|
@@ -350,25 +391,35 @@ defineType({
|
|
|
350
391
|
}),
|
|
351
392
|
defineField({
|
|
352
393
|
name: 'subnav',
|
|
353
|
-
description:
|
|
394
|
+
description: `Subnav is required on documents that aren’t '/home'`,
|
|
354
395
|
type: 'array',
|
|
355
396
|
of: [
|
|
356
397
|
{type: 'navLink'}
|
|
357
398
|
],
|
|
358
|
-
validation: rule => rule.custom(requiredIfSlugNeq('home'))
|
|
399
|
+
validation: rule => rule.custom(requiredIfSlugNeq('home')),
|
|
359
400
|
hidden: ({parent}) => parent.slug.current !== 'home'
|
|
360
401
|
})
|
|
361
402
|
]
|
|
362
403
|
})
|
|
363
404
|
```
|
|
364
405
|
|
|
406
|
+
#### parameters
|
|
407
|
+
```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.
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
|
|
365
416
|
### referencedDocumentRequires
|
|
366
417
|
|
|
367
|
-
You might want to enforce some validation on a
|
|
418
|
+
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.
|
|
368
419
|
|
|
369
420
|
```typescript
|
|
370
421
|
defineField({
|
|
371
|
-
name: '
|
|
422
|
+
name: 'referredArticle',
|
|
372
423
|
description: 'An article (must include a valid poster image)',
|
|
373
424
|
type: 'reference',
|
|
374
425
|
to: [{type: 'article'}],
|
|
@@ -376,6 +427,16 @@ defineField({
|
|
|
376
427
|
}),
|
|
377
428
|
```
|
|
378
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
|
+
---
|
|
438
|
+
|
|
439
|
+
|
|
379
440
|
### maxDepth
|
|
380
441
|
|
|
381
442
|
It can be useful to have a nested type. This often comes up when making some kind of navigation tree, like…
|
|
@@ -448,7 +509,20 @@ const navLink = defineType({
|
|
|
448
509
|
|
|
449
510
|
This will enforce that a subnav list can embed in a subnav, which can also be embedded in a subnav — but no further.
|
|
450
511
|
|
|
451
|
-
|
|
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
|
+
---
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
#### Note to any Sanity dev who looks at this
|
|
524
|
+
|
|
525
|
+
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?
|
|
452
526
|
|
|
453
527
|
```typescript
|
|
454
528
|
defineField({
|
|
@@ -463,18 +537,37 @@ defineField({
|
|
|
463
537
|
})
|
|
464
538
|
```
|
|
465
539
|
|
|
540
|
+
---
|
|
541
|
+
|
|
466
542
|
## Extending these and writing your own
|
|
467
543
|
|
|
468
544
|
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.
|
|
469
545
|
|
|
470
546
|
## Upcoming
|
|
471
547
|
|
|
548
|
+
### Nested pathfinders
|
|
549
|
+
|
|
472
550
|
Since building these validator, I took to putting my slugs in a metadata object. I need to update `requiredIfSlugEq` to accept a path, like `requiredIfSlugEq('metadata.slug', 'some-values')`.
|
|
473
551
|
|
|
474
552
|
This pathfinding should be added to any validator that takes a sibling, like `requiredIfSiblingEq`. It can probably be snapped into `getSibling`.
|
|
475
553
|
|
|
476
554
|
While I’m at it, there’s a possibility that `getSibling` could detect the target type. If that type is `slug`, then it could add `current` to the path, and then I can deprecate `requiredIfSlugEq` altogether.
|
|
477
555
|
|
|
556
|
+
### Image and File checks
|
|
557
|
+
|
|
558
|
+
`minDimensions`, `maxDimensions`, and `fileExtension` should check to see if the field is of type `image` or `file`.
|
|
559
|
+
|
|
560
|
+
Some of the other checks should probably make sure the field is _not_ `image` or `file`.
|
|
561
|
+
|
|
562
|
+
### new referencedDocumentFieldEq validator
|
|
563
|
+
|
|
564
|
+
```
|
|
565
|
+
// only articles by Jimmy Olsen
|
|
566
|
+
rule => rule.custom(referencedDocumentFieldEq('article', 'author', 'Jimmy Olsen'))
|
|
567
|
+
// only articles whose authors are not null. replaces `referencedDocumentRequires`.
|
|
568
|
+
rule => rule.custom(referencedDocumentFieldNeq('article', 'author', null))
|
|
569
|
+
```
|
|
570
|
+
|
|
478
571
|
## MOAR
|
|
479
572
|
|
|
480
573
|
Do you have any ideas or edge cases that these validators don’t cover? Leave an issue, maybe I can hack it out.
|