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 +149 -160
- package/dist/index.cjs +26 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +25 -15
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
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.
|
|
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
|
-
|
|
117
|
+
---
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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.
|
|
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
|
-
'
|
|
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
|
-
|
|
200
|
+
And it also works for arrays.
|
|
216
201
|
|
|
217
202
|
```typescript
|
|
218
203
|
defineType({
|
|
219
|
-
name:
|
|
220
|
-
type:
|
|
204
|
+
name: "person",
|
|
205
|
+
type: "object",
|
|
221
206
|
fields: [
|
|
207
|
+
// ...
|
|
222
208
|
defineField({
|
|
223
|
-
name:
|
|
224
|
-
type:
|
|
209
|
+
name: "occupation",
|
|
210
|
+
type: "string",
|
|
211
|
+
options: {
|
|
212
|
+
list: ["doctor", "lawyer", "software engineer", "linguist"],
|
|
213
|
+
},
|
|
225
214
|
}),
|
|
226
215
|
defineField({
|
|
227
|
-
name:
|
|
228
|
-
type:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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: '
|
|
256
|
-
type: 'string'
|
|
257
|
-
}),
|
|
258
|
-
defineField({
|
|
259
|
-
name: 'occupation',
|
|
240
|
+
name: 'email',
|
|
260
241
|
type: 'string',
|
|
261
|
-
|
|
262
|
-
list: ['doctor', 'lawyer', 'software engineer']
|
|
263
|
-
}
|
|
264
|
-
}),
|
|
242
|
+
})
|
|
265
243
|
defineField({
|
|
266
|
-
name: '
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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:
|
|
312
|
-
description:
|
|
313
|
-
type:
|
|
314
|
-
validation: rule => rule.custom(requiredIfSiblingNeq(
|
|
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
|
-
|
|
302
|
+
Mark a field as `required` for documents with matching slugs.
|
|
333
303
|
|
|
334
304
|
```typescript
|
|
335
|
-
|
|
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:
|
|
339
|
-
type:
|
|
314
|
+
name: "page",
|
|
315
|
+
type: "document",
|
|
340
316
|
fields: [
|
|
341
317
|
defineField({
|
|
342
|
-
name:
|
|
343
|
-
type:
|
|
318
|
+
name: "slug",
|
|
319
|
+
type: "slug",
|
|
344
320
|
}),
|
|
345
321
|
defineField({
|
|
346
|
-
name:
|
|
347
|
-
type:
|
|
348
|
-
of: [
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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:
|
|
386
|
-
type:
|
|
357
|
+
name: "page",
|
|
358
|
+
type: "document",
|
|
387
359
|
fields: [
|
|
388
360
|
defineField({
|
|
389
|
-
name:
|
|
390
|
-
type:
|
|
361
|
+
name: "slug",
|
|
362
|
+
type: "slug",
|
|
391
363
|
}),
|
|
392
364
|
defineField({
|
|
393
|
-
name:
|
|
365
|
+
name: "subnav",
|
|
394
366
|
description: `Subnav is required on documents that aren’t '/home'`,
|
|
395
|
-
type:
|
|
396
|
-
of: [
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
##
|
|
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,
|
|
82
|
-
let
|
|
83
|
-
const paths = context.path.filter((e) => typeof e === "string" && e.match(
|
|
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}",
|
|
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
|
|
95
|
-
if (!value && !!
|
|
96
|
-
return message.replace("{slugKey}", slugKey).replace("{
|
|
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,
|
|
103
|
-
var _a;
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
if (!value &&
|
|
107
|
-
return message.replace("{key}", key).replace("{
|
|
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
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEO,IAAM,6BAA6B,CACxC,cACA,OACA,UAAkB,oDACf,OAAO,OAAwB,YAA+B;AACjE,MAAI,EAAC,+BAAO,OAAM;AAChB,WAAO;AAAA,EACT;AACA,QAAM,SAAS,QAAQ,UAAU,EAAE,YAAY,aAAa,CAAC;AAI7D,QAAM,OAAO,MAAM,OAAO,MAAM;AAAA,kBAChB,YAAY,gBAAgB,MAAM,IAAI;AAAA,QAChD,KAAK;AAAA;AAAA,GAEV;AACD,MAAI,CAAC,KAAK,KAAK,GAAG;AAChB,WAAO,QAAQ,QAAQ,kBAAkB,YAAY,EAAE,QAAQ,WAAW,KAAK;AAAA,EACjF;AACA,SAAO;AACT;;;ACvBA,yBAA6B;AAGtB,IAAM,gBAAgB,CAC3B,oBACA,UAAkB,iDACf,CAAC,UAAiC;AACrC,MAAI,CAAC,SAAS,CAAC,MAAM,OAAO;AAC1B,WAAO;AAAA,EACT;AACA,QAAM,kBAAkB,OAAO,uBAAuB,WAAW,CAAC,kBAAkB,IAAI;AACxF,QAAM,eAAW,iCAAa,MAAM,MAAM,IAAI;AAC9C,MAAI,CAAC,gBAAgB,SAAS,QAAQ,GAAG;AACvC,WAAO,QAAQ,QAAQ,wBAAwB,gBAAgB,KAAK,OAAO,CAAC;AAAA,EAC9E;AACA,SAAO;AACT;;;AChBA,IAAAA,sBAAmC;AAG5B,IAAM,gBACX,CAAC,EAAE,GAAG,EAAE,GAA6B,YACrC,CAAC,UAAiC;AAChC,MAAI,CAAC,SAAS,CAAC,MAAM,OAAO;AAC1B,WAAO;AAAA,EACT;AACA,QAAM,EAAE,OAAO,OAAO,QAAI,wCAAmB,MAAM,MAAM,IAAI;AAC7D,MAAI,CAAC,CAAC,KAAK,QAAQ,GAAG;AACpB,WAAO,UAAU,QAAQ,QAAQ,OAAO,EAAE,SAAS,CAAC,EAAE,QAAQ,OAAO,CAAC,IAAI,UAAU,EAAE,SAAS,CAAC,IAAI,0BAA0B,CAAC;AAAA,EACjI;AACA,MAAI,CAAC,CAAC,KAAK,SAAS,GAAG;AACrB,WAAO,UAAU,QAAQ,QAAQ,OAAO,CAAC,IAAI,UAAU,EAAE,SAAS,CAAC,EAAE,QAAQ,OAAO,EAAE,SAAS,CAAC,IAAI,0BAA0B,CAAC;AAAA,EACjI;AACA,SAAO;AACT;;;ACfK,IAAM,WAAW,CACtBC,WACA,iBACA,UAAkB,yEACf,CAAC,GAAQ,YAA+B;AAC3C,MAAI,QAAQ,IAAI,OAAO,OAAO,oBAAoB,eAAe,EAAE;AACnE,QAAM,QAAS,QAAQ,KAAoB,OAAO,CAAC,MAAM,OAAO,MAAM,YAAY,EAAE,MAAM,KAAK,CAAC;AAChG,MAAI,MAAM,SAASA,WAAU;AAC3B,WAAO,QAAQ,QAAQ,qBAAqB,eAAe,EAAE,QAAQ,cAAcA,UAAS,SAAS,CAAC;AAAA,EACxG;AACA,SAAO;AACT;;;ACaO,IAAM,mBAAmB,CAC9B,MACA,UAAkB,QAClB,UAAkB,gCAElB,CAAC,OAA4B,YAA+B;AA/B9D;AAgCI,QAAM,QAAQ,OAAO,SAAS,WAAW,CAAC,IAAI,IAAI;AAClD,QAAM,oBAAoB,mBAAQ,WAAR,mBAAyB,aAAzB,mBAAmC;AAK7D,MAAI,CAAC,SAAS,CAAC,CAAC,oBAAoB,MAAM,SAAS,gBAAgB,GAAG;AACpE,WAAO,QAAQ,QAAQ,aAAa,OAAO,EAAE,QAAQ,UAAU,MAAM,KAAK,OAAO,CAAC;AAAA,EACpF;AACA,SAAO;AACT;;;ACPK,IAAM,sBAAsB,CACjC,KACA,YACA,UAAkB,wCAElB,CAAC,OAA4B,YAA+B;AAxC9D;AAyCI,QAAM,UAAU,WAAW,KAAK,OAAO;AACvC,QAAM,cAAc,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC,UAAU;AACxE,MAAI,CAAC,SAAS,YAAY,SAAS,OAAO,GAAG;AAC3C,WAAO,QAAQ,QAAQ,SAAS,GAAG,EAAE,QAAQ,YAAW,iBAAY,KAAK,OAAO,MAAxB,YAA6B,MAAM;AAAA,EAC7F;AACA,SAAO;AACT;;;AC/CF,uBAAoB;AAGb,IAAM,aAAa,CAAC,KAAsB,YAA+B;AAC9E,QAAM,qBAAqB,QAAQ,KAAM,MAAM,GAAG,EAAE;AACpD,QAAM,cAAU,sBAAI,QAAQ,UAAU,CAAC,GAAG,oBAAoB,GAAG,CAAC;AAClE,SAAO;AACT;","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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEO,IAAM,6BAA6B,CACxC,cACA,OACA,UAAkB,oDACf,OAAO,OAAwB,YAA+B;AACjE,MAAI,EAAC,+BAAO,OAAM;AAChB,WAAO;AAAA,EACT;AACA,QAAM,SAAS,QAAQ,UAAU,EAAE,YAAY,aAAa,CAAC;AAI7D,QAAM,OAAO,MAAM,OAAO,MAAM;AAAA,kBAChB,YAAY,gBAAgB,MAAM,IAAI;AAAA,QAChD,KAAK;AAAA;AAAA,GAEV;AACD,MAAI,CAAC,KAAK,KAAK,GAAG;AAChB,WAAO,QAAQ,QAAQ,kBAAkB,YAAY,EAAE,QAAQ,WAAW,KAAK;AAAA,EACjF;AACA,SAAO;AACT;;;ACvBA,yBAA6B;AAGtB,IAAM,gBAAgB,CAC3B,oBACA,UAAkB,iDACf,CAAC,UAAiC;AACrC,MAAI,CAAC,SAAS,CAAC,MAAM,OAAO;AAC1B,WAAO;AAAA,EACT;AACA,QAAM,kBAAkB,OAAO,uBAAuB,WAAW,CAAC,kBAAkB,IAAI;AACxF,QAAM,eAAW,iCAAa,MAAM,MAAM,IAAI;AAC9C,MAAI,CAAC,gBAAgB,SAAS,QAAQ,GAAG;AACvC,WAAO,QAAQ,QAAQ,wBAAwB,gBAAgB,KAAK,OAAO,CAAC;AAAA,EAC9E;AACA,SAAO;AACT;;;AChBA,IAAAA,sBAAmC;AAG5B,IAAM,gBACX,CAAC,EAAE,GAAG,EAAE,GAA6B,YACrC,CAAC,UAAiC;AAChC,MAAI,CAAC,SAAS,CAAC,MAAM,OAAO;AAC1B,WAAO;AAAA,EACT;AACA,QAAM,EAAE,OAAO,OAAO,QAAI,wCAAmB,MAAM,MAAM,IAAI;AAC7D,MAAI,CAAC,CAAC,KAAK,QAAQ,GAAG;AACpB,WAAO,UACH,QAAQ,QAAQ,WAAW,MAAM,SAAS,CAAC,EAC1C,QAAQ,YAAY,OAAO,SAAS,CAAC,EACrC,QAAQ,OAAO,EAAE,SAAS,CAAC,EAC3B,QAAQ,OAAO,CAAC,IAAI,UAAU,EAAE,SAAS,CAAC,IAC3C,0BAA0B,CAAC;AAAA,EACjC;AACA,MAAI,CAAC,CAAC,KAAK,SAAS,GAAG;AACrB,WAAO,UACH,QAAQ,QAAQ,WAAW,MAAM,SAAS,CAAC,EAC1C,QAAQ,YAAY,OAAO,SAAS,CAAC,EACrC,QAAQ,OAAO,CAAC,IAAI,UAAU,EAAE,SAAS,CAAC,EAC1C,QAAQ,OAAO,EAAE,SAAS,CAAC,IAC5B,0BAA0B,CAAC;AAAA,EACjC;AACA,SAAO;AACT;;;ACzBK,IAAM,WAAW,CACtBC,WACA,KACA,UAAkB,6DACf,CAAC,GAAQ,YAA+B;AAC3C,MAAIC,SAAQ,IAAI,OAAO,OAAO,oBAAoB,GAAG,EAAE;AACvD,QAAM,QAAS,QAAQ,KAAoB,OAAO,CAAC,MAAM,OAAO,MAAM,YAAY,EAAE,MAAMA,MAAK,CAAC;AAChG,MAAI,MAAM,SAASD,WAAU;AAC3B,WAAO,QACJ,QAAQ,SAAS,GAAG,EACpB,QAAQ,qBAAqB,GAAG,EAChC,QAAQ,cAAcA,UAAS,SAAS,CAAC;AAAA,EAC9C;AACA,SAAO;AACT;;;ACUO,IAAM,mBAAmB,CAC9B,MACA,UAAkB,QAClB,UAAkB,gCAElB,CAAC,OAA4B,YAA+B;AA/B9D;AAgCI,QAAM,QAAQ,OAAO,SAAS,WAAW,CAAC,IAAI,IAAI;AAClD,QAAM,aAAa,mBAAQ,WAAR,mBAAyB,aAAzB,mBAAmC;AAKtD,MAAI,CAAC,SAAS,CAAC,CAAC,aAAa,MAAM,SAAS,SAAS,GAAG;AACtD,WAAO,QACJ,QAAQ,aAAa,OAAO,EAC5B,QAAQ,aAAa,MAAM,KAAK,OAAO,CAAC,EACxC,QAAQ,sBAAsB,SAAS;AAAA,EAC5C;AACA,SAAO;AACT;;;ACVK,IAAM,sBAAsB,CACjC,KACA,SACA,UAAkB,0CAElB,CAAC,OAA4B,YAA+B;AAxC9D;AAyCI,QAAM,eAAe,WAAW,KAAK,OAAO;AAC5C,QAAM,WAAW,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO;AAC5D,MAAI,CAAC,SAAS,SAAS,SAAS,YAAY,GAAG;AAC7C,WAAO,QACJ,QAAQ,SAAS,GAAG,EACpB,QAAQ,cAAa,cAAS,KAAK,OAAO,MAArB,YAA0B,MAAM,EACrD,QAAQ,YAAW,cAAS,KAAK,OAAO,MAArB,YAA0B,MAAM,EACnD,QAAQ,kBAAkB,YAAY;AAAA,EAC3C;AACA,SAAO;AACT;;;ACnDF,uBAAoB;AAGb,IAAM,aAAa,CAAC,KAAsB,YAA+B;AAC9E,QAAM,qBAAqB,QAAQ,KAAM,MAAM,GAAG,EAAE;AACpD,QAAM,cAAU,sBAAI,QAAQ,UAAU,CAAC,GAAG,oBAAoB,GAAG,CAAC;AAClE,SAAO;AACT;;;ACPO,IAAM,QACX,CAAC,SAAiB,UAAkB,gEACpC,CAAC,UAAmB;AAClB,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,QAAM,gBAAgB,OAAO,UAAU,WAAW,MAAM,SAAS,IAAI;AACrE,SAAO,QAAQ,KAAK,aAAa,IAAI,OAAO,QAAQ,QAAQ,WAAW,aAAa,EAAE,QAAQ,aAAa,QAAQ,SAAS,CAAC;AAC/H;","names":["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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
50
|
-
let
|
|
51
|
-
const paths = context.path.filter((e) => typeof e === "string" && e.match(
|
|
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}",
|
|
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
|
|
63
|
-
if (!value && !!
|
|
64
|
-
return message.replace("{slugKey}", slugKey).replace("{
|
|
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,
|
|
71
|
-
var _a;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
if (!value &&
|
|
75
|
-
return message.replace("{key}", key).replace("{
|
|
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
|
+
"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
|
-
|
|
22
|
-
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@sanity/asset-utils": "^2.2.1",
|
|
25
|
-
"lodash-es": "^4.
|
|
26
|
-
"sanity": "^
|
|
25
|
+
"lodash-es": "^4.17.0",
|
|
26
|
+
"sanity": "^3.9.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@types/lodash-es": "^4.
|
|
29
|
+
"@types/lodash-es": "^4.17.0",
|
|
30
30
|
"tsup": "^8.5.0",
|
|
31
|
-
"typescript": "^5.
|
|
31
|
+
"typescript": "^5.8.0",
|
|
32
32
|
"vitest": "^3.2.4"
|
|
33
33
|
},
|
|
34
34
|
"name": "sanity-advanced-validators",
|