@sanity/validation 2.33.4-shopify.8 → 2.34.1-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -687,81 +687,4 @@ describe('validateItem', () => {
687
687
  },
688
688
  ])
689
689
  })
690
-
691
- it('runs all nested validation checks concurrently', async () => {
692
- type Resolver = (value: true) => void
693
- const resolvers = new Set<Resolver>()
694
- const customValidationFn = () => new Promise<true>((resolve) => resolvers.add(resolve))
695
-
696
- const schema: Schema = createSchema({
697
- types: [
698
- {
699
- name: 'root',
700
- type: 'object',
701
- fields: [
702
- {
703
- name: 'level1Object',
704
- type: 'object',
705
- validation: (rule: Rule) => [
706
- rule.custom(customValidationFn),
707
- rule.fields({
708
- level2String: (r) => r.custom(customValidationFn),
709
- }),
710
- ],
711
- fields: [
712
- {
713
- name: 'level2String',
714
- type: 'string',
715
- validation: (rule: Rule) => rule.custom(customValidationFn),
716
- },
717
- ],
718
- },
719
- {
720
- name: 'level1Array',
721
- type: 'array',
722
- validation: (rule: Rule) => rule.custom(customValidationFn),
723
- of: [
724
- {
725
- type: 'object',
726
- fields: [
727
- {
728
- name: 'level2Number',
729
- type: 'number',
730
- validation: (rule: Rule) => rule.custom(customValidationFn),
731
- },
732
- ],
733
- },
734
- ],
735
- },
736
- ],
737
- validation: (rule: Rule) => rule.custom(customValidationFn),
738
- },
739
- ],
740
- })
741
-
742
- const value = {
743
- level1Object: {level3String: 'a string'},
744
- level1Array: [{level2Number: 5}],
745
- }
746
-
747
- const resultPromise = validateItem({
748
- value,
749
- document: undefined,
750
- parent: undefined,
751
- path: undefined,
752
- type: schema.get('root'),
753
- getDocumentExists: undefined,
754
- })
755
-
756
- // after `validateItem(...)` has been initiated, all of the custom
757
- // validation calls should have fired so the resolver size should equal the
758
- // amount of total fields
759
- expect(resolvers.size).toBe(6)
760
-
761
- for (const resolver of resolvers) {
762
- resolver(true)
763
- }
764
-
765
- await resultPromise
766
- })
767
690
  })
@@ -1,17 +1,20 @@
1
1
  import {
2
+ isBlock,
3
+ isBlockSchemaType,
4
+ isKeyedObject,
5
+ isSpanSchemaType,
6
+ isTypedObject,
2
7
  SanityDocument,
3
8
  Schema,
4
9
  SchemaType,
5
10
  ValidationContext,
6
11
  ValidationMarker,
7
- isKeyedObject,
8
- isTypedObject,
9
- isBlock,
10
- isBlockSchemaType,
11
- isSpanSchemaType,
12
12
  } from '@sanity/types'
13
- import {uniqBy} from 'lodash'
13
+ import {concat, defer, merge, Observable, of} from 'rxjs'
14
+ import {catchError, map, mergeAll, mergeMap, toArray} from 'rxjs/operators'
15
+ import {flatten, uniqBy} from 'lodash'
14
16
  import typeString from './util/typeString'
17
+ import {requestIdleCallback, cancelIdleCallback} from './util/requestIdleCallback'
15
18
  import ValidationErrorClass from './ValidationError'
16
19
  import normalizeValidationRules from './util/normalizeValidationRules'
17
20
 
@@ -45,38 +48,47 @@ export function resolveTypeForArrayItem(
45
48
  candidates.find((candidate) => candidate.name === 'object' && primitive === 'object')
46
49
  )
47
50
  }
51
+ const EMPTY_MARKERS: ValidationMarker[] = []
48
52
 
49
- export default async function validateDocument(
53
+ export default function validateDocument(
50
54
  doc: SanityDocument,
51
55
  schema: Schema,
52
56
  context?: Pick<ValidationContext, 'getDocumentExists'>
53
57
  ): Promise<ValidationMarker[]> {
58
+ return validateDocumentObservable(doc, schema, context).toPromise()
59
+ }
60
+
61
+ export function validateDocumentObservable(
62
+ doc: SanityDocument,
63
+ schema: Schema,
64
+ context?: Pick<ValidationContext, 'getDocumentExists'>
65
+ ): Observable<ValidationMarker[]> {
54
66
  const documentType = schema.get(doc._type)
55
67
  if (!documentType) {
56
68
  console.warn('Schema type for object type "%s" not found, skipping validation', doc._type)
57
- return []
69
+ return of(EMPTY_MARKERS)
58
70
  }
59
71
 
60
- try {
61
- return await validateItem({
62
- parent: undefined,
63
- value: doc,
64
- path: [],
65
- document: doc,
66
- type: documentType,
67
- getDocumentExists: context?.getDocumentExists,
72
+ return validateItemObservable({
73
+ parent: undefined,
74
+ value: doc,
75
+ path: [],
76
+ document: doc,
77
+ type: documentType,
78
+ getDocumentExists: context?.getDocumentExists,
79
+ }).pipe(
80
+ catchError((err) => {
81
+ console.error(err)
82
+ return of([
83
+ {
84
+ type: 'validation' as const,
85
+ level: 'error' as const,
86
+ path: [],
87
+ item: new ValidationErrorClass(err?.message),
88
+ },
89
+ ])
68
90
  })
69
- } catch (err) {
70
- console.error(err)
71
- return [
72
- {
73
- type: 'validation',
74
- level: 'error',
75
- path: [],
76
- item: new ValidationErrorClass(err?.message),
77
- },
78
- ]
79
- }
91
+ )
80
92
  }
81
93
 
82
94
  /**
@@ -94,27 +106,32 @@ type ValidateItemOptions = {
94
106
  value: unknown
95
107
  } & ExplicitUndefined<ValidationContext>
96
108
 
97
- export async function validateItem({
109
+ export function validateItem(opts: ValidateItemOptions): Promise<ValidationMarker[]> {
110
+ return validateItemObservable(opts).toPromise()
111
+ }
112
+
113
+ function validateItemObservable({
98
114
  value,
99
115
  type,
100
116
  path = [],
101
117
  parent,
102
118
  ...restOfContext
103
- }: ValidateItemOptions): Promise<ValidationMarker[]> {
119
+ }: ValidateItemOptions): Observable<ValidationMarker[]> {
104
120
  const rules = normalizeValidationRules(type)
105
-
106
121
  // run validation for the current value
107
122
  const selfChecks = rules.map((rule) =>
108
- rule.validate(value, {
109
- ...restOfContext,
110
- parent,
111
- path,
112
- type,
113
- })
123
+ defer(() =>
124
+ rule.validate(value, {
125
+ ...restOfContext,
126
+ parent,
127
+ path,
128
+ type,
129
+ })
130
+ )
114
131
  )
115
132
 
116
133
  // run validation for nested values (conditionally)
117
- let nestedChecks: Array<Promise<ValidationMarker[]>> = []
134
+ let fieldChecks: Array<Observable<ValidationMarker[]>> = []
118
135
 
119
136
  const selfIsRequired = rules.some((rule) => rule.isRequired())
120
137
  const shouldRunNestedObjectValidation =
@@ -132,7 +149,7 @@ export async function validateItem({
132
149
  }, {})
133
150
 
134
151
  // Validation for rules set at the object level with `Rule.fields({/* ... */})`
135
- nestedChecks = nestedChecks.concat(
152
+ fieldChecks = fieldChecks.concat(
136
153
  rules
137
154
  .map((rule) => rule._fieldRules)
138
155
  .filter(isNonNullable)
@@ -141,20 +158,22 @@ export async function validateItem({
141
158
  const fieldType = fieldTypes[name]
142
159
  return normalizeValidationRules({...fieldType, validation}).map((subRule) => {
143
160
  const nestedValue = isRecord(value) ? value[name] : undefined
144
- return subRule.validate(nestedValue, {
145
- ...restOfContext,
146
- parent: value,
147
- path: path.concat(name),
148
- type: fieldType,
149
- })
161
+ return defer(() =>
162
+ subRule.validate(nestedValue, {
163
+ ...restOfContext,
164
+ parent: value,
165
+ path: path.concat(name),
166
+ type: fieldType,
167
+ })
168
+ )
150
169
  })
151
170
  })
152
171
  )
153
172
 
154
173
  // Validation from each field's schema `validation: Rule => {/* ... */}` function
155
- nestedChecks = nestedChecks.concat(
174
+ fieldChecks = fieldChecks.concat(
156
175
  type.fields.map((field) =>
157
- validateItem({
176
+ validateItemObservable({
158
177
  ...restOfContext,
159
178
  parent: value,
160
179
  value: isRecord(value) ? value[field.name] : undefined,
@@ -172,9 +191,9 @@ export async function validateItem({
172
191
  const shouldRunNestedValidationForArrays = type?.jsonType === 'array' && Array.isArray(value)
173
192
 
174
193
  if (shouldRunNestedValidationForArrays) {
175
- nestedChecks = nestedChecks.concat(
194
+ fieldChecks = fieldChecks.concat(
176
195
  value.map((item) =>
177
- validateItem({
196
+ validateItemObservable({
178
197
  ...restOfContext,
179
198
  parent: value,
180
199
  value: item,
@@ -195,16 +214,16 @@ export async function validateItem({
195
214
  const spanType = spanChildrenField.type.of.find(isSpanSchemaType)
196
215
 
197
216
  const annotations = (spanType?.annotations || []).reduce<Map<string, SchemaType>>(
198
- (map, annotationType) => {
199
- map.set(annotationType.name, annotationType)
200
- return map
217
+ (acc, annotationType) => {
218
+ acc.set(annotationType.name, annotationType)
219
+ return acc
201
220
  },
202
221
  new Map()
203
222
  )
204
223
 
205
- nestedChecks = nestedChecks.concat(
224
+ fieldChecks = fieldChecks.concat(
206
225
  value.markDefs.map((markDef) =>
207
- validateItem({
226
+ validateItemObservable({
208
227
  ...restOfContext,
209
228
  parent: value,
210
229
  value: markDef,
@@ -215,13 +234,31 @@ export async function validateItem({
215
234
  )
216
235
  }
217
236
 
218
- const results = (await Promise.all([...selfChecks, ...nestedChecks])).flat()
237
+ return defer(() => merge([...selfChecks, ...fieldChecks])).pipe(
238
+ mergeMap((validateNode) => concat(idle(), validateNode), 40),
239
+ mergeAll(),
240
+ toArray(),
241
+ map(flatten),
242
+ map((results) => {
243
+ // run `uniqBy` if `_fieldRules` are present because they can
244
+ // cause repeat markers
245
+ if (rules.some((rule) => rule._fieldRules)) {
246
+ return uniqBy(results, (rule) => JSON.stringify(rule))
247
+ }
248
+ return results
249
+ })
250
+ )
251
+ }
219
252
 
220
- // run `uniqBy` if `_fieldRules` are present because they can
221
- // cause repeat markers
222
- if (rules.some((rule) => rule._fieldRules)) {
223
- return uniqBy(results, (rule) => JSON.stringify(rule))
224
- }
253
+ function idle(timeout?: number): Observable<never> {
254
+ return new Observable<never>((observer) => {
255
+ const handle = requestIdleCallback(
256
+ () => {
257
+ observer.complete()
258
+ },
259
+ timeout ? {timeout} : undefined
260
+ )
225
261
 
226
- return results
262
+ return () => cancelIdleCallback(handle)
263
+ })
227
264
  }