@sanity/validation 3.14.3 → 6.12.0-next.112
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 +1 -1
- package/README.md +52 -0
- package/lib/_internal.d.ts +96 -0
- package/lib/_internal.js +39 -0
- package/lib/_internal.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -1306
- package/lib/validateDocument-6pLTIHIn.d.ts +277 -0
- package/lib/validateDocument-Cq33kUmN.js +1127 -0
- package/lib/validateDocument-Cq33kUmN.js.map +1 -0
- package/package.json +53 -45
- package/lib/dts/src/index.d.ts +0 -50
- package/lib/index.cjs.mjs +0 -9
- package/lib/index.esm.js +0 -1286
- package/lib/index.esm.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/src/Rule.ts +0 -424
- package/src/ValidationError.ts +0 -32
- package/src/index.ts +0 -9
- package/src/inferFromSchema.ts +0 -19
- package/src/inferFromSchemaType.ts +0 -50
- package/src/util/convertToValidationMarker.ts +0 -84
- package/src/util/deepEquals.ts +0 -77
- package/src/util/escapeRegex.ts +0 -5
- package/src/util/normalizeValidationRules.test.ts +0 -170
- package/src/util/normalizeValidationRules.ts +0 -118
- package/src/util/pathToString.ts +0 -21
- package/src/util/requestIdleCallback.ts +0 -31
- package/src/util/typeString.test.ts +0 -27
- package/src/util/typeString.ts +0 -23
- package/src/validateDocument.test.ts +0 -703
- package/src/validateDocument.ts +0 -240
- package/src/validators/arrayValidator.ts +0 -100
- package/src/validators/booleanValidator.ts +0 -16
- package/src/validators/dateValidator.ts +0 -113
- package/src/validators/genericValidator.ts +0 -117
- package/src/validators/numberValidator.ts +0 -66
- package/src/validators/objectValidator.ts +0 -64
- package/src/validators/slugValidator.ts +0 -117
- package/src/validators/stringValidator.ts +0 -120
package/src/validateDocument.ts
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SanityDocument,
|
|
3
|
-
Schema,
|
|
4
|
-
SchemaType,
|
|
5
|
-
ValidationContext,
|
|
6
|
-
ValidationMarker,
|
|
7
|
-
isKeyedObject,
|
|
8
|
-
isTypedObject,
|
|
9
|
-
isBlockSchemaType,
|
|
10
|
-
isSpanSchemaType,
|
|
11
|
-
isPortableTextTextBlock,
|
|
12
|
-
} from '@sanity/types'
|
|
13
|
-
import {concat, defer, lastValueFrom, merge, Observable, of} from 'rxjs'
|
|
14
|
-
import {catchError, map, mergeAll, mergeMap, toArray} from 'rxjs/operators'
|
|
15
|
-
import {flatten, uniqBy} from 'lodash'
|
|
16
|
-
import typeString from './util/typeString'
|
|
17
|
-
import {cancelIdleCallback, requestIdleCallback} from './util/requestIdleCallback'
|
|
18
|
-
import ValidationErrorClass from './ValidationError'
|
|
19
|
-
import normalizeValidationRules from './util/normalizeValidationRules'
|
|
20
|
-
|
|
21
|
-
const isRecord = (maybeRecord: unknown): maybeRecord is Record<string, unknown> =>
|
|
22
|
-
typeof maybeRecord === 'object' && maybeRecord !== null && !Array.isArray(maybeRecord)
|
|
23
|
-
|
|
24
|
-
const isNonNullable = <T>(value: T): value is NonNullable<T> =>
|
|
25
|
-
value !== null && value !== undefined
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @internal
|
|
29
|
-
*/
|
|
30
|
-
export function resolveTypeForArrayItem(
|
|
31
|
-
item: unknown,
|
|
32
|
-
candidates: SchemaType[]
|
|
33
|
-
): SchemaType | undefined {
|
|
34
|
-
// if there is only one type available, assume that it's the correct one
|
|
35
|
-
if (candidates.length === 1) return candidates[0]
|
|
36
|
-
|
|
37
|
-
const itemType = isTypedObject(item) && item._type
|
|
38
|
-
const primitive =
|
|
39
|
-
item === undefined || item === null || (!itemType && typeString(item).toLowerCase())
|
|
40
|
-
|
|
41
|
-
if (primitive && primitive !== 'object') {
|
|
42
|
-
return candidates.find((candidate) => candidate.jsonType === primitive)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
candidates.find((candidate) => candidate.type?.name === itemType) ||
|
|
47
|
-
candidates.find((candidate) => candidate.name === itemType) ||
|
|
48
|
-
candidates.find((candidate) => candidate.name === 'object' && primitive === 'object')
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
const EMPTY_MARKERS: ValidationMarker[] = []
|
|
52
|
-
|
|
53
|
-
export default async function validateDocument(
|
|
54
|
-
getClient: ValidateItemOptions['getClient'],
|
|
55
|
-
doc: SanityDocument,
|
|
56
|
-
schema: Schema,
|
|
57
|
-
context?: Pick<ValidationContext, 'getDocumentExists'>
|
|
58
|
-
): Promise<ValidationMarker[]> {
|
|
59
|
-
return lastValueFrom(validateDocumentObservable(getClient, doc, schema, context))
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function validateDocumentObservable(
|
|
63
|
-
getClient: ValidateItemOptions['getClient'],
|
|
64
|
-
doc: SanityDocument,
|
|
65
|
-
schema: Schema,
|
|
66
|
-
context?: Pick<ValidationContext, 'getDocumentExists'>
|
|
67
|
-
): Observable<ValidationMarker[]> {
|
|
68
|
-
const documentType = schema.get(doc._type)
|
|
69
|
-
if (!documentType) {
|
|
70
|
-
console.warn('Schema type for object type "%s" not found, skipping validation', doc._type)
|
|
71
|
-
return of(EMPTY_MARKERS)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const validationOptions: ValidateItemOptions = {
|
|
75
|
-
getClient,
|
|
76
|
-
schema,
|
|
77
|
-
parent: undefined,
|
|
78
|
-
value: doc,
|
|
79
|
-
path: [],
|
|
80
|
-
document: doc,
|
|
81
|
-
type: documentType,
|
|
82
|
-
getDocumentExists: context?.getDocumentExists,
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return validateItemObservable(validationOptions).pipe(
|
|
86
|
-
catchError((err) => {
|
|
87
|
-
console.error(err)
|
|
88
|
-
return of([
|
|
89
|
-
{
|
|
90
|
-
type: 'validation' as const,
|
|
91
|
-
level: 'error' as const,
|
|
92
|
-
path: [],
|
|
93
|
-
item: new ValidationErrorClass(err?.message),
|
|
94
|
-
},
|
|
95
|
-
])
|
|
96
|
-
})
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* this is used make optional properties required by replacing optionals with
|
|
102
|
-
* `T[P] | undefined`. this is used to prevent errors in `validateItem` where
|
|
103
|
-
* an option from a previous invocation would be incorrectly passed down.
|
|
104
|
-
*
|
|
105
|
-
* https://medium.com/terria/typescript-transforming-optional-properties-to-required-properties-that-may-be-undefined-7482cb4e1585
|
|
106
|
-
*/
|
|
107
|
-
type ExplicitUndefined<T> = {
|
|
108
|
-
[P in keyof Required<T>]: Pick<T, P> extends Required<Pick<T, P>> ? T[P] : T[P] | undefined
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
type ValidateItemOptions = {
|
|
112
|
-
value: unknown
|
|
113
|
-
} & ExplicitUndefined<ValidationContext>
|
|
114
|
-
|
|
115
|
-
export function validateItem(opts: ValidateItemOptions): Promise<ValidationMarker[]> {
|
|
116
|
-
return lastValueFrom(validateItemObservable(opts))
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function validateItemObservable({
|
|
120
|
-
value,
|
|
121
|
-
type,
|
|
122
|
-
path = [],
|
|
123
|
-
parent,
|
|
124
|
-
...restOfContext
|
|
125
|
-
}: ValidateItemOptions): Observable<ValidationMarker[]> {
|
|
126
|
-
const rules = normalizeValidationRules(type)
|
|
127
|
-
// run validation for the current value
|
|
128
|
-
const selfChecks = rules.map((rule) =>
|
|
129
|
-
defer(() =>
|
|
130
|
-
rule.validate(value, {
|
|
131
|
-
...restOfContext,
|
|
132
|
-
parent,
|
|
133
|
-
path,
|
|
134
|
-
type,
|
|
135
|
-
})
|
|
136
|
-
)
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
// run validation for nested values (conditionally)
|
|
140
|
-
let nestedChecks: Array<Observable<ValidationMarker[]>> = []
|
|
141
|
-
|
|
142
|
-
const selfIsRequired = rules.some((rule) => rule.isRequired())
|
|
143
|
-
const shouldRunNestedObjectValidation =
|
|
144
|
-
// run nested validation for objects
|
|
145
|
-
type?.jsonType === 'object' &&
|
|
146
|
-
// if the value is truthy
|
|
147
|
-
(!!value || // or
|
|
148
|
-
// (the value is null or undefined) and the top-level value is required
|
|
149
|
-
((value === null || value === undefined) && selfIsRequired))
|
|
150
|
-
|
|
151
|
-
if (shouldRunNestedObjectValidation) {
|
|
152
|
-
const fieldTypes = type.fields.reduce<Record<string, SchemaType>>((acc, field) => {
|
|
153
|
-
acc[field.name] = field.type
|
|
154
|
-
return acc
|
|
155
|
-
}, {})
|
|
156
|
-
|
|
157
|
-
// Validation for rules set at the object level with `Rule.fields({/* ... */})`
|
|
158
|
-
nestedChecks = nestedChecks.concat(
|
|
159
|
-
rules
|
|
160
|
-
.map((rule) => rule._fieldRules)
|
|
161
|
-
.filter(isNonNullable)
|
|
162
|
-
.flatMap((fieldResults) => Object.entries(fieldResults))
|
|
163
|
-
.flatMap(([name, validation]) => {
|
|
164
|
-
const fieldType = fieldTypes[name]
|
|
165
|
-
return normalizeValidationRules({...fieldType, validation}).map((subRule) => {
|
|
166
|
-
const nestedValue = isRecord(value) ? value[name] : undefined
|
|
167
|
-
return defer(() =>
|
|
168
|
-
subRule.validate(nestedValue, {
|
|
169
|
-
...restOfContext,
|
|
170
|
-
parent: value,
|
|
171
|
-
path: path.concat(name),
|
|
172
|
-
type: fieldType,
|
|
173
|
-
})
|
|
174
|
-
)
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
// Validation from each field's schema `validation: Rule => {/* ... */}` function
|
|
180
|
-
nestedChecks = nestedChecks.concat(
|
|
181
|
-
type.fields.map((field) =>
|
|
182
|
-
validateItemObservable({
|
|
183
|
-
...restOfContext,
|
|
184
|
-
parent: value,
|
|
185
|
-
value: isRecord(value) ? value[field.name] : undefined,
|
|
186
|
-
path: path.concat(field.name),
|
|
187
|
-
type: field.type,
|
|
188
|
-
})
|
|
189
|
-
)
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// note: unlike objects, arrays should not run nested validation for undefined
|
|
194
|
-
// values because we won't have a valid path to put a marker (i.e. missing the
|
|
195
|
-
// key or index in the path) and the downstream form builder won't have a
|
|
196
|
-
// valid target component
|
|
197
|
-
const shouldRunNestedValidationForArrays = type?.jsonType === 'array' && Array.isArray(value)
|
|
198
|
-
|
|
199
|
-
if (shouldRunNestedValidationForArrays) {
|
|
200
|
-
nestedChecks = nestedChecks.concat(
|
|
201
|
-
value.map((item, index) =>
|
|
202
|
-
validateItemObservable({
|
|
203
|
-
...restOfContext,
|
|
204
|
-
parent: value,
|
|
205
|
-
value: item,
|
|
206
|
-
path: path.concat(isKeyedObject(item) ? {_key: item._key} : index),
|
|
207
|
-
type: resolveTypeForArrayItem(item, type.of),
|
|
208
|
-
})
|
|
209
|
-
)
|
|
210
|
-
)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return defer(() => merge([...selfChecks, ...nestedChecks])).pipe(
|
|
214
|
-
mergeMap((validateNode) => concat(idle(), validateNode), 40),
|
|
215
|
-
mergeAll(),
|
|
216
|
-
toArray(),
|
|
217
|
-
map(flatten),
|
|
218
|
-
map((results) => {
|
|
219
|
-
// run `uniqBy` if `_fieldRules` are present because they can
|
|
220
|
-
// cause repeat markers
|
|
221
|
-
if (rules.some((rule) => rule._fieldRules)) {
|
|
222
|
-
return uniqBy(results, (rule) => JSON.stringify(rule))
|
|
223
|
-
}
|
|
224
|
-
return results
|
|
225
|
-
})
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function idle(timeout?: number): Observable<never> {
|
|
230
|
-
return new Observable<never>((observer) => {
|
|
231
|
-
const handle = requestIdleCallback(
|
|
232
|
-
() => {
|
|
233
|
-
observer.complete()
|
|
234
|
-
},
|
|
235
|
-
timeout ? {timeout} : undefined
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
return () => cancelIdleCallback(handle)
|
|
239
|
-
})
|
|
240
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import {Path, PathSegment, Validators} from '@sanity/types'
|
|
2
|
-
import deepEquals from '../util/deepEquals'
|
|
3
|
-
import ValidationErrorClass from '../ValidationError'
|
|
4
|
-
import genericValidator from './genericValidator'
|
|
5
|
-
|
|
6
|
-
const arrayValidators: Validators = {
|
|
7
|
-
...genericValidator,
|
|
8
|
-
|
|
9
|
-
min: (minLength, value, message) => {
|
|
10
|
-
if (!value || value.length >= minLength) {
|
|
11
|
-
return true
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return message || `Must have at least ${minLength} items`
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
max: (maxLength, value, message) => {
|
|
18
|
-
if (!value || value.length <= maxLength) {
|
|
19
|
-
return true
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return message || `Must have at most ${maxLength} items`
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
length: (wantedLength, value, message) => {
|
|
26
|
-
if (!value || value.length === wantedLength) {
|
|
27
|
-
return true
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return message || `Must have exactly ${wantedLength} items`
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
presence: (flag, value, message) => {
|
|
34
|
-
if (flag === 'required' && !value) {
|
|
35
|
-
return message || 'Required'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return true
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
valid: (allowedValues, values, message) => {
|
|
42
|
-
const valueType = typeof values
|
|
43
|
-
if (valueType === 'undefined') {
|
|
44
|
-
return true
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const paths: Path[] = []
|
|
48
|
-
for (let i = 0; i < values.length; i++) {
|
|
49
|
-
const value = values[i]
|
|
50
|
-
if (allowedValues.some((expected) => deepEquals(expected, value))) {
|
|
51
|
-
continue
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const pathSegment: PathSegment = value && value._key ? {_key: value._key} : i
|
|
55
|
-
paths.push([pathSegment])
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return paths.length === 0
|
|
59
|
-
? true
|
|
60
|
-
: new ValidationErrorClass(message || 'Value did not match any allowed values', {paths})
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
unique: (_unused, value, message) => {
|
|
64
|
-
const dupeIndices = []
|
|
65
|
-
if (!value) {
|
|
66
|
-
return true
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (let x = 0; x < value.length; x++) {
|
|
70
|
-
for (let y = x + 1; y < value.length; y++) {
|
|
71
|
-
const itemA = value[x]
|
|
72
|
-
const itemB = value[y]
|
|
73
|
-
|
|
74
|
-
if (!deepEquals(itemA, itemB)) {
|
|
75
|
-
continue
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (dupeIndices.indexOf(x) === -1) {
|
|
79
|
-
dupeIndices.push(x)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (dupeIndices.indexOf(y) === -1) {
|
|
83
|
-
dupeIndices.push(y)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const paths = dupeIndices.map((idx) => {
|
|
89
|
-
const item = value[idx]
|
|
90
|
-
const pathSegment = item && item._key ? {_key: item._key} : idx
|
|
91
|
-
return [pathSegment]
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
return dupeIndices.length > 0
|
|
95
|
-
? new ValidationErrorClass(message || `Can't be a duplicate`, {paths})
|
|
96
|
-
: true
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export default arrayValidators
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import {Validators} from '@sanity/types'
|
|
2
|
-
import genericValidator from './genericValidator'
|
|
3
|
-
|
|
4
|
-
const booleanValidators: Validators = {
|
|
5
|
-
...genericValidator,
|
|
6
|
-
|
|
7
|
-
presence: (flag, value, message) => {
|
|
8
|
-
if (flag === 'required' && typeof value !== 'boolean') {
|
|
9
|
-
return message || 'Required'
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return true
|
|
13
|
-
},
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default booleanValidators
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import {Validators} from '@sanity/types'
|
|
2
|
-
import formatDate from 'date-fns/format'
|
|
3
|
-
import genericValidator from './genericValidator'
|
|
4
|
-
|
|
5
|
-
export function isRecord(obj: unknown): obj is Record<string, unknown> {
|
|
6
|
-
return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const isoDate =
|
|
10
|
-
/^(?:[-+]\d{2})?(?:\d{4}(?!\d{2}\b))(?:(-?)(?:(?:0[1-9]|1[0-2])(?:\1(?:[12]\d|0[1-9]|3[01]))?|W(?:[0-4]\d|5[0-2])(?:-?[1-7])?|(?:00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[1-6])))(?![T]$|[T][\d]+Z$)(?:[T\s](?:(?:(?:[01]\d|2[0-3])(?:(:?)[0-5]\d)?|24:?00)(?:[.,]\d+(?!:))?)(?:\2[0-5]\d(?:[.,]\d+)?)?(?:[Z]|(?:[+-])(?:[01]\d|2[0-3])(?::?[0-5]\d)?)?)?)?$/
|
|
11
|
-
|
|
12
|
-
// eslint-disable-next-line no-warning-comments
|
|
13
|
-
// TODO (eventually): move these to schema type package
|
|
14
|
-
interface DateTimeOptions {
|
|
15
|
-
dateFormat?: string
|
|
16
|
-
timeFormat?: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const getFormattedDate = (type = '', value: string | number | Date, options?: DateTimeOptions) => {
|
|
20
|
-
let format = 'yyyy-MM-dd'
|
|
21
|
-
if (options && options.dateFormat) {
|
|
22
|
-
format = options.dateFormat
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (type === 'date') {
|
|
26
|
-
// If the type is date only
|
|
27
|
-
return formatDate(new Date(value), format)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// If the type is datetime
|
|
31
|
-
if (options && options.timeFormat) {
|
|
32
|
-
format += ` ${options.timeFormat}`
|
|
33
|
-
} else {
|
|
34
|
-
format += ' HH:mm'
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return formatDate(new Date(value), format)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseDate(date: unknown): Date | null
|
|
41
|
-
function parseDate(date: unknown, throwOnError: true): Date
|
|
42
|
-
function parseDate(date: unknown, throwOnError = false): Date | null {
|
|
43
|
-
if (!date) return null
|
|
44
|
-
if (date === 'now') return new Date()
|
|
45
|
-
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
-
const parsed = new Date(date as any)
|
|
48
|
-
const isInvalid = isNaN(parsed.getTime())
|
|
49
|
-
if (isInvalid && throwOnError) {
|
|
50
|
-
throw new Error(`Unable to parse "${date}" to a date`)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return isInvalid ? null : parsed
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const dateValidators: Validators = {
|
|
57
|
-
...genericValidator,
|
|
58
|
-
|
|
59
|
-
type: (_unused, value, message) => {
|
|
60
|
-
const strVal = `${value}`
|
|
61
|
-
if (!strVal || isoDate.test(value)) {
|
|
62
|
-
return true
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return message || 'Must be a valid ISO-8601 formatted date string'
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
min: (minDate, value, message, context) => {
|
|
69
|
-
const dateVal = parseDate(value)
|
|
70
|
-
if (!dateVal) {
|
|
71
|
-
return true // `type()` should catch parse errors
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!value || dateVal >= parseDate(minDate, true)) {
|
|
75
|
-
return true
|
|
76
|
-
}
|
|
77
|
-
if (!context.type) {
|
|
78
|
-
throw new Error(`\`type\` was not provided in validation context.`)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const dateTimeOptions: DateTimeOptions = isRecord(context.type.options)
|
|
82
|
-
? (context.type.options as DateTimeOptions)
|
|
83
|
-
: {}
|
|
84
|
-
|
|
85
|
-
const date = getFormattedDate(context.type.name, minDate, dateTimeOptions)
|
|
86
|
-
|
|
87
|
-
return message || `Must be at or after ${date}`
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
max: (maxDate, value, message, context) => {
|
|
91
|
-
const dateVal = parseDate(value)
|
|
92
|
-
if (!dateVal) {
|
|
93
|
-
return true // `type()` should catch parse errors
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!value || dateVal <= parseDate(maxDate, true)) {
|
|
97
|
-
return true
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!context.type) {
|
|
101
|
-
throw new Error(`\`type\` was not provided in validation context.`)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const dateTimeOptions: DateTimeOptions = isRecord(context.type.options)
|
|
105
|
-
? (context.type.options as DateTimeOptions)
|
|
106
|
-
: {}
|
|
107
|
-
|
|
108
|
-
const date = getFormattedDate(context.type.name, maxDate, dateTimeOptions)
|
|
109
|
-
return message || `Must be at or before ${date}`
|
|
110
|
-
},
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export default dateValidators
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import {ValidationMarker, Validators} from '@sanity/types'
|
|
2
|
-
import typeString from '../util/typeString'
|
|
3
|
-
import deepEquals from '../util/deepEquals'
|
|
4
|
-
import pathToString from '../util/pathToString'
|
|
5
|
-
import ValidationErrorClass from '../ValidationError'
|
|
6
|
-
|
|
7
|
-
const SLOW_VALIDATOR_TIMEOUT = 5000
|
|
8
|
-
|
|
9
|
-
const formatValidationErrors = (options: {
|
|
10
|
-
message: string | undefined
|
|
11
|
-
results: ValidationMarker[]
|
|
12
|
-
operation: 'AND' | 'OR'
|
|
13
|
-
}) => {
|
|
14
|
-
let message
|
|
15
|
-
|
|
16
|
-
if (options.message) {
|
|
17
|
-
message = options.message
|
|
18
|
-
} else if (options.results.length === 1) {
|
|
19
|
-
message = options.results[0]?.item.message
|
|
20
|
-
} else {
|
|
21
|
-
message = `[${options.results
|
|
22
|
-
.map((err) => err.item.message)
|
|
23
|
-
.join(` - ${options.operation} - `)}]`
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return new ValidationErrorClass(message, {
|
|
27
|
-
children: options.results.length > 1 ? options.results : undefined,
|
|
28
|
-
operation: options.operation,
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const genericValidators: Validators = {
|
|
33
|
-
type: (expected, value, message) => {
|
|
34
|
-
const actualType = typeString(value)
|
|
35
|
-
if (actualType !== expected && actualType !== 'undefined') {
|
|
36
|
-
return message || `Expected type "${expected}", got "${actualType}"`
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return true
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
presence: (expected, value, message) => {
|
|
43
|
-
if (value === undefined && expected === 'required') {
|
|
44
|
-
return message || 'Value is required'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return true
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
all: async (children, value, message, context) => {
|
|
51
|
-
const resolved = await Promise.all(children.map((child) => child.validate(value, context)))
|
|
52
|
-
const results = resolved.flat()
|
|
53
|
-
|
|
54
|
-
if (!results.length) return true
|
|
55
|
-
|
|
56
|
-
return formatValidationErrors({
|
|
57
|
-
message,
|
|
58
|
-
results,
|
|
59
|
-
operation: 'AND',
|
|
60
|
-
})
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
either: async (children, value, message, context) => {
|
|
64
|
-
const resolved = await Promise.all(children.map((child) => child.validate(value, context)))
|
|
65
|
-
const results = resolved.flat()
|
|
66
|
-
|
|
67
|
-
// Read: There is at least one rule that matched
|
|
68
|
-
if (results.length < children.length) return true
|
|
69
|
-
|
|
70
|
-
return formatValidationErrors({
|
|
71
|
-
message,
|
|
72
|
-
results,
|
|
73
|
-
operation: 'OR',
|
|
74
|
-
})
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
valid: (allowedValues, actual, message) => {
|
|
78
|
-
const valueType = typeof actual
|
|
79
|
-
if (valueType === 'undefined') {
|
|
80
|
-
return true
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const value = (valueType === 'number' || valueType === 'string') && `${actual}`
|
|
84
|
-
const strValue = value && value.length > 30 ? `${value.slice(0, 30)}…` : value
|
|
85
|
-
|
|
86
|
-
const defaultMessage = value
|
|
87
|
-
? `Value "${strValue}" did not match any allowed values`
|
|
88
|
-
: 'Value did not match any allowed values'
|
|
89
|
-
|
|
90
|
-
return allowedValues.some((expected) => deepEquals(expected, actual))
|
|
91
|
-
? true
|
|
92
|
-
: message || defaultMessage
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
custom: async (fn, value, message, context) => {
|
|
96
|
-
const slowTimer = setTimeout(() => {
|
|
97
|
-
// eslint-disable-next-line no-console
|
|
98
|
-
console.warn(
|
|
99
|
-
`Custom validator at ${pathToString(
|
|
100
|
-
context.path
|
|
101
|
-
)} has taken more than ${SLOW_VALIDATOR_TIMEOUT}ms to respond`
|
|
102
|
-
)
|
|
103
|
-
}, SLOW_VALIDATOR_TIMEOUT)
|
|
104
|
-
|
|
105
|
-
let result
|
|
106
|
-
try {
|
|
107
|
-
result = await fn(value, context)
|
|
108
|
-
} finally {
|
|
109
|
-
clearTimeout(slowTimer)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (typeof result === 'string') return message || result
|
|
113
|
-
return result
|
|
114
|
-
},
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export default genericValidators
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import {Validators} from '@sanity/types'
|
|
2
|
-
import genericValidator from './genericValidator'
|
|
3
|
-
|
|
4
|
-
const precisionRx = /(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/
|
|
5
|
-
|
|
6
|
-
const numberValidators: Validators = {
|
|
7
|
-
...genericValidator,
|
|
8
|
-
|
|
9
|
-
integer: (_unused, value, message) => {
|
|
10
|
-
if (!Number.isInteger(value)) {
|
|
11
|
-
return message || 'Must be an integer'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return true
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
precision: (limit, value, message) => {
|
|
18
|
-
if (value === undefined) return true
|
|
19
|
-
|
|
20
|
-
const places = value.toString().match(precisionRx)
|
|
21
|
-
const decimals = Math.max(
|
|
22
|
-
(places[1] ? places[1].length : 0) - (places[2] ? parseInt(places[2], 10) : 0),
|
|
23
|
-
0
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
if (decimals > limit) {
|
|
27
|
-
return message || `Max precision is ${limit}`
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return true
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
min: (minNum, value, message) => {
|
|
34
|
-
if (value >= minNum) {
|
|
35
|
-
return true
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return message || `Must be greater than or equal ${minNum}`
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
max: (maxNum, value, message) => {
|
|
42
|
-
if (value <= maxNum) {
|
|
43
|
-
return true
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return message || `Must be less than or equal ${maxNum}`
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
greaterThan: (num, value, message) => {
|
|
50
|
-
if (value > num) {
|
|
51
|
-
return true
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return message || `Must be greater than ${num}`
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
lessThan: (maxNum, value, message) => {
|
|
58
|
-
if (value < maxNum) {
|
|
59
|
-
return true
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return message || `Must be less than ${maxNum}`
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export default numberValidators
|