@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.
Files changed (40) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +52 -0
  3. package/lib/_internal.d.ts +96 -0
  4. package/lib/_internal.js +39 -0
  5. package/lib/_internal.js.map +1 -0
  6. package/lib/index.d.ts +2 -0
  7. package/lib/index.js +2 -1306
  8. package/lib/validateDocument-6pLTIHIn.d.ts +277 -0
  9. package/lib/validateDocument-Cq33kUmN.js +1127 -0
  10. package/lib/validateDocument-Cq33kUmN.js.map +1 -0
  11. package/package.json +53 -45
  12. package/lib/dts/src/index.d.ts +0 -50
  13. package/lib/index.cjs.mjs +0 -9
  14. package/lib/index.esm.js +0 -1286
  15. package/lib/index.esm.js.map +0 -1
  16. package/lib/index.js.map +0 -1
  17. package/src/Rule.ts +0 -424
  18. package/src/ValidationError.ts +0 -32
  19. package/src/index.ts +0 -9
  20. package/src/inferFromSchema.ts +0 -19
  21. package/src/inferFromSchemaType.ts +0 -50
  22. package/src/util/convertToValidationMarker.ts +0 -84
  23. package/src/util/deepEquals.ts +0 -77
  24. package/src/util/escapeRegex.ts +0 -5
  25. package/src/util/normalizeValidationRules.test.ts +0 -170
  26. package/src/util/normalizeValidationRules.ts +0 -118
  27. package/src/util/pathToString.ts +0 -21
  28. package/src/util/requestIdleCallback.ts +0 -31
  29. package/src/util/typeString.test.ts +0 -27
  30. package/src/util/typeString.ts +0 -23
  31. package/src/validateDocument.test.ts +0 -703
  32. package/src/validateDocument.ts +0 -240
  33. package/src/validators/arrayValidator.ts +0 -100
  34. package/src/validators/booleanValidator.ts +0 -16
  35. package/src/validators/dateValidator.ts +0 -113
  36. package/src/validators/genericValidator.ts +0 -117
  37. package/src/validators/numberValidator.ts +0 -66
  38. package/src/validators/objectValidator.ts +0 -64
  39. package/src/validators/slugValidator.ts +0 -117
  40. package/src/validators/stringValidator.ts +0 -120
@@ -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