sanity-advanced-validators 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Eric Jacobsen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,474 @@
1
+ # Sanity Advanced Validators
2
+
3
+ (note: this is a WIP, I don’t know a lot about publishing NPM packages, hopefully coming soon)
4
+
5
+ This package includes a set of Sanity validators for aggressive and weird edge cases. Please let me know if you find these helpful!
6
+
7
+ Note that every validator can accept an optional custom error message as its last parameter. `minDimensions` lists one example; all the others work the same way.
8
+
9
+ ## Tools
10
+
11
+ - [fileExtension](#fileExtension)
12
+ - [minDimensions](#minDimensions)
13
+ - [maxDimensions](#maxDimensions)
14
+ - [requiredIfPeerEq](#requiredIfPeerEq)
15
+ - [requiredIfPeerNeq](#requiredIfPeerNeq)
16
+ - [requiredIfSlugEq](#requiredIfSlugEq)
17
+ - [requiredIfSlugNeq](#requiredIfSlugNeq)
18
+ - [referencedDocumentRequires](#referencedDocumentRequires)
19
+ - [maxDepth](#maxDepth)
20
+
21
+ ## Mega-example
22
+
23
+ Imagine that you’ve got a document that has an optional video file — but it's required on the `/about` page. If the video exists, it must either MP4 or MOV, and have a poster image that's between 1250x800 and 2500x1600 in size.
24
+
25
+ ```typescript
26
+ const Page = defineType({
27
+ name: "page",
28
+ type: "document",
29
+ fields: [
30
+ defineField({
31
+ name: 'slug',
32
+ type: 'slug'
33
+ }),
34
+ defineField({
35
+ name: "someVideoFile",
36
+ type: "file",
37
+ validation: (rule) =>
38
+ rule.requiredIfSlugEq('about')
39
+ .custom(fileExtension(['mp4', 'mov']))
40
+ })
41
+ defineField({
42
+ name: "posterImage",
43
+ type: "image",
44
+ hidden: ({ parent }) => parent.someVideoFile === null,
45
+ validation: (rule) =>
46
+ rule.requiredIfPeerNeq('someVideoFile', null)
47
+ .custom(minDimensions({ x: 1250, y: 800 }))
48
+ .custom(maxDimensions({ x: 2500, y: 1600 })),
49
+ })
50
+ ]
51
+ })
52
+ ```
53
+
54
+ ## Examples
55
+
56
+ ### fileType
57
+
58
+ Enforces that an uploaded file asset is of a certain format.
59
+
60
+ ```typescript
61
+ import { fileExtension } from "sanity-advanced-validation"
62
+
63
+ const Page = defineType({
64
+ name: "page",
65
+ type: "document",
66
+ fields: [
67
+ defineField({
68
+ name: "catalog",
69
+ type: "file",
70
+ validation: (rule) => rule.custom(fileType("pdf")),
71
+ }),
72
+ defineField({
73
+ name: "video",
74
+ type: "file",
75
+ validation: (rule) => rule.custom(fileExtension(["mp4", "mov", "webm"])),
76
+ }),
77
+ ],
78
+ })
79
+ ```
80
+
81
+ ### minDimensions
82
+
83
+ Enforces that an uploaded image asset is at minimum certain dimensions.
84
+
85
+ ```typescript
86
+ import { minDimensions } from "sanity-advanced-validation"
87
+
88
+ const ImageWithCaption = defineType({
89
+ name: "imageWithCaption",
90
+ type: "object",
91
+ fields: [
92
+ defineField({
93
+ name: "caption",
94
+ type: "string",
95
+ }),
96
+ defineField({
97
+ name: "image",
98
+ type: "image",
99
+ validation: (rule) => rule.custom(minDimensions({ x: 100, y: 100 })),
100
+ }),
101
+ ],
102
+ })
103
+ ```
104
+
105
+ You can also enforce on only one dimension, or feed a custom error message:
106
+
107
+ ```typescript
108
+ defineField({
109
+ name: "image",
110
+ type: "image",
111
+ description: "At least 100px wide; as tall as you like.",
112
+ validation: (rule) => rule.custom(
113
+ minDimensions(
114
+ { x: 100 },
115
+ "Uh oh, your image is {width} pixels wide. That’s less than {x}!"
116
+ )
117
+ ),
118
+ })
119
+ ```
120
+
121
+ ### maxDimensions
122
+
123
+ Enforces that an uploaded image asset is at most certain dimensions.
124
+
125
+ ```typescript
126
+ import { maxDimensions } from "sanity-advanced-validation"
127
+
128
+ const ImageWithCaption = defineType({
129
+ name: "imageWithCaption",
130
+ type: "object",
131
+ fields: [
132
+ defineField({
133
+ name: "caption",
134
+ type: "string",
135
+ }),
136
+ defineField({
137
+ name: "image",
138
+ type: "image",
139
+ validation: (rule) => rule.custom(maxDimensions({ x: 2000, y: 2000 })),
140
+ }),
141
+ ],
142
+ })
143
+ ```
144
+
145
+ Chain for min and max dimensions:
146
+
147
+ ```typescript
148
+ defineField({
149
+ name: "image",
150
+ type: "image",
151
+ description: "Min: 100x100, max: 2000x2000.",
152
+ validation: (rule) =>
153
+ rule
154
+ .required()
155
+ .custom(minDimensions({ x: 1000, y: 1000 }))
156
+ .custom(maxDimensions({ x: 2000, y: 2000 })),
157
+ })
158
+ ```
159
+
160
+ ### requiredIfPeerEq
161
+
162
+ For a given object that has multiple fields, mark a field as `required` if a peer has a particular value.
163
+
164
+ _note:_ This does not work for slugs, because they have to match a nested `.current` value. Use the [requiredIfSlugEq validator](#requiredIfSlugEq) instead.
165
+
166
+ ```typescript
167
+ import {requiredIfPeerEq} from 'sanity-advanced-validation'
168
+
169
+ defineType({
170
+ name: 'person',
171
+ type: 'object',
172
+ fields: [
173
+ defineField({
174
+ name: 'name',
175
+ type: 'string'
176
+ }),
177
+ defineField({
178
+ name: 'occupation',
179
+ type: 'string',
180
+ options: {
181
+ list: ['doctor', 'lawyer', 'software engineer']
182
+ }
183
+ })
184
+ defineField({
185
+ name: 'favoriteLanguage',
186
+ type: 'string',
187
+ options: {
188
+ list: [
189
+ 'javascript', 'rust', 'python', 'swift'
190
+ ]
191
+ }
192
+ hidden: ({parent}) => parent.occuption !== 'software engineer',
193
+ validation: rule => rule.custom(requiredIfPeerEq('occupation', 'software engineer'))
194
+ }),
195
+ ],
196
+ })
197
+ ```
198
+
199
+ This also works for null. It’s very effective!
200
+
201
+ ```typescript
202
+ defineType({
203
+ name: 'person',
204
+ type: 'object',
205
+ fields: [
206
+ defineField({
207
+ name: 'name',
208
+ type: 'string'
209
+ }),
210
+ defineField({
211
+ name: 'email',
212
+ type: 'string',
213
+ })
214
+ defineField({
215
+ name: 'phone',
216
+ type: 'string',
217
+ validation: rule => rule.custom(requiredIfPeerEq(
218
+ 'email',
219
+ null,
220
+ "If you don’t have an email address, please provide a phone number."
221
+ ))
222
+ })
223
+ ],
224
+ })
225
+ ```
226
+
227
+ And it even works for arrays.
228
+
229
+ ```typescript
230
+ defineType({
231
+ name: 'person',
232
+ type: 'object',
233
+ fields: [
234
+ defineField({
235
+ name: 'name',
236
+ type: 'string'
237
+ }),
238
+ defineType({
239
+ name: 'person',
240
+ type: 'object',
241
+ fields: [
242
+ defineField({
243
+ name: 'name',
244
+ type: 'string'
245
+ }),
246
+ defineField({
247
+ name: 'occupation',
248
+ type: 'string',
249
+ options: {
250
+ list: ['doctor', 'lawyer', 'software engineer']
251
+ }
252
+ }),
253
+ defineField({
254
+ name: 'explanation',
255
+ description: 'Why are you wasting your life this way?',
256
+ type: 'text',
257
+ hidden: ({parent}) => parent.occuption === 'software engineer',
258
+ validation: rule => rule.custom(requiredIfPeerEq('occupation', ['doctor', 'lawyer']))
259
+ })
260
+ ],
261
+ })
262
+ ],
263
+ })
264
+ ```
265
+
266
+ ### requiredIfPeerNeq
267
+
268
+ For a given object that has multiple fields, mark a field as `required` if a peer does _not_ have a particular value.
269
+
270
+ _note:_ This does not work for slugs, because they have to match a nested `.current` value. Use the [requiredIfSlugNeq validator](#requiredIfSlugNeq) instead.
271
+
272
+ ```typescript
273
+ import {requiredIfPeerNeq} from 'sanity-advanced-validation'
274
+
275
+ defineType({
276
+ name: 'person',
277
+ type: 'object',
278
+ fields: [
279
+ defineField({
280
+ name: 'name',
281
+ type: 'string'
282
+ }),
283
+ defineField({
284
+ name: 'occupation',
285
+ type: 'string',
286
+ options: {
287
+ list: ['doctor', 'lawyer', 'software engineer']
288
+ }
289
+ })
290
+ defineField({
291
+ name: 'why',
292
+ description: 'Why are you wasting your life this way?',
293
+ type: 'text',
294
+ hidden: ({parent}) => parent.occuption === 'software engineer',
295
+ validation: rule => rule.custom(requiredIfPeerNeq('occupation', 'software engineer'))
296
+ }),
297
+ ],
298
+ })
299
+ ```
300
+
301
+ ### requiredIfSlugEq
302
+
303
+ Require for matching slugs.
304
+
305
+ ```typescript
306
+ import {requiredIfSlugEq} from 'sanity-advanced-validation'
307
+
308
+ defineType({
309
+ name: 'page',
310
+ type: 'document',
311
+ fields: [
312
+ defineField({
313
+ name: 'slug',
314
+ type: 'slug'
315
+ }),
316
+ defineField({
317
+ name: 'questionsAndAnswers',
318
+ type: 'array',
319
+ of: [
320
+ {type: 'qaItem'}
321
+ ],
322
+ validation: rule => rule.custom(requiredIfSlugEq('faq'))
323
+ hidden: ({parent}) => parent.slug.current !== 'faq'
324
+ })
325
+ ]
326
+ })
327
+ ```
328
+
329
+ And this can apply to multiple slugs…
330
+
331
+ ```typescript
332
+ defineField({
333
+ name: "questionsAndAnswers",
334
+ validation: (rule) => rule.custom(requiredIfSlugEq(["faq", "about"])),
335
+ }),
336
+ ```
337
+
338
+ ### requiredIfSlugNeq
339
+
340
+ Require fields on pages that don't match one or more slugs.
341
+
342
+ ```typescript
343
+ import {requiredIfSlugNeq} from 'sanity-advanced-validation'
344
+
345
+ defineType({
346
+ name: 'page',
347
+ type: 'document',
348
+ fields: [
349
+ defineField({
350
+ name: 'slug',
351
+ type: 'slug'
352
+ }),
353
+ defineField({
354
+ name: 'subnav',
355
+ description: 'Subnav is required on documents that aren't '/home'`,
356
+ type: 'array',
357
+ of: [
358
+ {type: 'navLink'}
359
+ ],
360
+ validation: rule => rule.custom(requiredIfSlugNeq('home'))
361
+ hidden: ({parent}) => parent.slug.current !== 'home'
362
+ })
363
+ ]
364
+ })
365
+ ```
366
+
367
+ ### referencedDocumentRequires
368
+
369
+ You might want to enforce some validation on a referred document. This validator enforces that a given value is not null in the referenced document.
370
+
371
+ ```typescript
372
+ defineField({
373
+ name: 'refferedArticle',
374
+ description: 'An article (must include a valid poster image)',
375
+ type: 'reference',
376
+ to: [{type: 'article'}],
377
+ validation: (rule) => rule.custom(referencedDocumentRequires('article', 'poster')),
378
+ }),
379
+ ```
380
+
381
+ ### maxDepth
382
+
383
+ It can be useful to have a nested type. This often comes up when making some kind of navigation tree, like…
384
+
385
+ ```
386
+ - Home
387
+ - About
388
+ - Articles
389
+ - First Article
390
+ - Second Article
391
+ - Articles about Trees
392
+ - Article about Elm Trees
393
+ - Article about Willow Trees
394
+ ```
395
+
396
+ Sanity can handle this without breaking a sweat:
397
+
398
+ ```typescript
399
+ const navigation = defineType({
400
+ name: "navigation",
401
+ type: "document",
402
+ fields: [
403
+ defineField({
404
+ name: "links",
405
+ type: "array",
406
+ of: [{ type: navLink }],
407
+ }),
408
+ ],
409
+ })
410
+
411
+ const navLink = defineType({
412
+ name: "navLink",
413
+ type: "object",
414
+ fields: [
415
+ defineField({
416
+ name: "link",
417
+ type: "url",
418
+ }),
419
+ defineField({
420
+ name: "label",
421
+ type: "string",
422
+ }),
423
+ defineField({
424
+ name: "subnav",
425
+ type: "array",
426
+ of: [{ type: navigation }], // < circular reference
427
+ }),
428
+ ],
429
+ })
430
+ ```
431
+
432
+ … but your users might get a little stupid with this, and you may want to enforce navigations only going _n_ layers deep.
433
+
434
+ ```typescript
435
+ import { maxDepth } from "sanity-advanced-validation"
436
+
437
+ const navLink = defineType({
438
+ // …
439
+ fields: [
440
+ // …
441
+ defineField({
442
+ name: "subnav",
443
+ type: "array",
444
+ of: [{ type: navigation }],
445
+ validation: (rule) => rule.custom(maxDepth(3, "subnav")),
446
+ }),
447
+ ],
448
+ })
449
+ ```
450
+
451
+ This will enforce that a subnav list can embed in a subnav, which can also be embedded in a subnav — but no further.
452
+
453
+ _Note to any Sanity dev who looks at this_: I’d love to include similar logic on my `hidden:` attribute, but I don’t think that’t possible without a `path` array in the `hidden` context that’s similar to the one fed to the `ValidationContext` (todo: type this correctly). Wouldn’t this be cool?
454
+
455
+ ```typescript
456
+ defineField({
457
+ name: "subnav",
458
+ type: "array",
459
+ of: [{ type: navigation }],
460
+ hidden: ({ path }) => {
461
+ let regex = new RegExp(String.raw`topLevelItems|subNav`)
462
+ const paths = context.path.filter((e) => e.match(/topLevelItems|subnav/))
463
+ return paths.length > 3
464
+ },
465
+ })
466
+ ```
467
+
468
+ ## Extending these and writing your own
469
+
470
+ Most of these validators rely on a function called `getPeer()`. If you’re thinking about picking this apart and writing your own custom validator, take a close look at how these validators use it.
471
+
472
+ ## MOAR
473
+
474
+ Do you have any ideas or edge cases that these validators don’t cover? Leave an issue, maybe I can hack it out.
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/fileExtension.ts
21
+ var fileExtension_exports = {};
22
+ __export(fileExtension_exports, {
23
+ fileExtension: () => fileExtension
24
+ });
25
+ module.exports = __toCommonJS(fileExtension_exports);
26
+ var import_asset_utils = require("@sanity/asset-utils");
27
+ var fileExtension = (validFileExtension, message = `Image must be of type {validFileExtension}`) => (value) => {
28
+ if (!value || !value.asset) {
29
+ return true;
30
+ }
31
+ const validExtensions = typeof validFileExtension === "string" ? [validFileExtension] : validFileExtension;
32
+ const filetype = (0, import_asset_utils.getExtension)(value.asset._ref);
33
+ if (!validExtensions.includes(filetype)) {
34
+ return message.replace("{validFileExtension}", validExtensions.join(", or "));
35
+ }
36
+ return true;
37
+ };
38
+ // Annotate the CommonJS export names for ESM import in node:
39
+ 0 && (module.exports = {
40
+ fileExtension
41
+ });