@sanity/validation 3.0.0-v3-pkg-utils.59 → 3.0.0-v3-pkg-utils.79

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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@sanity/validation",
3
- "version": "3.0.0-v3-pkg-utils.59+a412550301",
3
+ "version": "3.0.0-v3-pkg-utils.79+73676e30fc",
4
4
  "description": "Validation and warning infrastructure for Sanity projects",
5
- "source": "./src/index.ts",
6
- "main": "./lib/index.cjs",
7
- "module": "./lib/index.js",
8
5
  "types": "./lib/dts/src/index.d.ts",
6
+ "source": "./src/index.ts",
7
+ "module": "./lib/index.mjs",
8
+ "main": "./lib/index.js",
9
9
  "files": [
10
10
  "lib",
11
11
  "src"
@@ -43,17 +43,18 @@
43
43
  "directory": "packages/@sanity/validation"
44
44
  },
45
45
  "dependencies": {
46
- "@sanity/types": "3.0.0-v3-pkg-utils.59+a412550301",
46
+ "@sanity/types": "3.0.0-v3-pkg-utils.79+73676e30fc",
47
47
  "date-fns": "^2.26.1",
48
- "lodash": "^4.17.21"
48
+ "lodash": "^4.17.21",
49
+ "rxjs": "^6.5.3"
49
50
  },
50
51
  "devDependencies": {
51
- "@sanity/client": "^3.3.3",
52
- "@sanity/schema": "3.0.0-v3-pkg-utils.59+a412550301"
52
+ "@sanity/client": "^3.4.1",
53
+ "@sanity/schema": "3.0.0-v3-pkg-utils.79+73676e30fc"
53
54
  },
54
55
  "peerDependencies": {
55
- "@sanity/client": "^3.3.3",
56
+ "@sanity/client": "^3.4.1",
56
57
  "@sanity/schema": "dev-preview"
57
58
  },
58
- "gitHead": "a412550301a93eba1dea643753e5b54f12b6fdae"
59
+ "gitHead": "73676e30fcc7d17f60ca574f45b632eb6823b60f"
59
60
  }
package/src/index.ts CHANGED
@@ -5,3 +5,5 @@ import inferFromSchemaType from './inferFromSchemaType'
5
5
 
6
6
  // export default {Rule: RuleClass, validateDocument, inferFromSchema, inferFromSchemaType}
7
7
  export {RuleClass as Rule, validateDocument, inferFromSchema, inferFromSchemaType}
8
+
9
+ export {validateDocumentObservable} from './validateDocument'
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Simple requestIdleCallback polyfill
3
+ * Can be removed when all browsers support requestIdleCallback: https://caniuse.com/requestidlecallback
4
+ * @param callback
5
+ * @param options
6
+ */
7
+ const requestIdleCallbackShim: typeof window.requestIdleCallback = function requestIdleCallbackShim(
8
+ callback,
9
+ options?
10
+ ): number {
11
+ const start = Date.now()
12
+ return window.setTimeout(() => {
13
+ callback({
14
+ didTimeout: false,
15
+ timeRemaining() {
16
+ return Math.max(0, Date.now() - start)
17
+ },
18
+ })
19
+ }, 0)
20
+ }
21
+
22
+ const cancelIdleCallbackShim: typeof window.cancelIdleCallback = function cancelIdleCallbackShim(
23
+ handle: number
24
+ ): void {
25
+ return window.clearTimeout(handle)
26
+ }
27
+
28
+ const win = typeof window === 'undefined' ? undefined : window
29
+
30
+ export const requestIdleCallback = win?.requestIdleCallback || requestIdleCallbackShim
31
+ export const cancelIdleCallback = win?.cancelIdleCallback || cancelIdleCallbackShim
@@ -1,3 +1,4 @@
1
+ import type {SanityClient} from '@sanity/client'
1
2
  import {Rule, SchemaType, SanityDocument, Schema, ArraySchemaType, Block} from '@sanity/types'
2
3
  import {createSchema} from '../test/createSchema'
3
4
  import {createMockSanityClient} from '../test/mocks/mockSanityClient'
@@ -17,7 +18,8 @@ beforeEach(() => {
17
18
  })
18
19
 
19
20
  // mock client
20
- const client = createMockSanityClient()
21
+ const client = createMockSanityClient() as any as SanityClient
22
+ const getClient = (options: {apiVersion: string}) => client
21
23
 
22
24
  describe('resolveTypeForArrayItem', () => {
23
25
  const schema: Schema = createSchema({
@@ -93,7 +95,7 @@ describe('validateDocument', () => {
93
95
  title: null,
94
96
  }
95
97
 
96
- const result = await validateDocument(client as any, document, schema)
98
+ const result = await validateDocument(getClient, document, schema)
97
99
  expect(result).toMatchObject([
98
100
  {
99
101
  level: 'error',
@@ -167,7 +169,8 @@ describe('validateItem', () => {
167
169
 
168
170
  await expect(
169
171
  validateItem({
170
- client: client as any,
172
+ client,
173
+ getClient,
171
174
  schema,
172
175
  value: {},
173
176
  document: undefined,
@@ -237,7 +240,8 @@ describe('validateItem', () => {
237
240
 
238
241
  await expect(
239
242
  validateItem({
240
- client: client as any,
243
+ client,
244
+ getClient,
241
245
  schema,
242
246
  document: undefined,
243
247
  parent: undefined,
@@ -404,7 +408,8 @@ describe('validateItem', () => {
404
408
  }
405
409
 
406
410
  const result = await validateItem({
407
- client: client as any,
411
+ client,
412
+ getClient,
408
413
  schema,
409
414
  document: document,
410
415
  parent: undefined,
@@ -507,7 +512,8 @@ describe('validateItem', () => {
507
512
 
508
513
  await expect(
509
514
  validateItem({
510
- client: client as any,
515
+ client,
516
+ getClient,
511
517
  schema,
512
518
  document: undefined,
513
519
  parent: undefined,
@@ -606,7 +612,8 @@ describe('validateItem', () => {
606
612
 
607
613
  await expect(
608
614
  validateItem({
609
- client: client as any,
615
+ client,
616
+ getClient,
610
617
  schema,
611
618
  value,
612
619
  type: rootType,
@@ -691,83 +698,4 @@ describe('validateItem', () => {
691
698
  },
692
699
  ])
693
700
  })
694
-
695
- it('runs all nested validation checks concurrently', async () => {
696
- type Resolver = (value: true) => void
697
- const resolvers = new Set<Resolver>()
698
- const customValidationFn = () => new Promise<true>((resolve) => resolvers.add(resolve))
699
-
700
- const schema: Schema = createSchema({
701
- types: [
702
- {
703
- name: 'root',
704
- type: 'object',
705
- fields: [
706
- {
707
- name: 'level1Object',
708
- type: 'object',
709
- validation: (rule: Rule) => [
710
- rule.custom(customValidationFn),
711
- rule.fields({
712
- level2String: (r) => r.custom(customValidationFn),
713
- }),
714
- ],
715
- fields: [
716
- {
717
- name: 'level2String',
718
- type: 'string',
719
- validation: (rule: Rule) => rule.custom(customValidationFn),
720
- },
721
- ],
722
- },
723
- {
724
- name: 'level1Array',
725
- type: 'array',
726
- validation: (rule: Rule) => rule.custom(customValidationFn),
727
- of: [
728
- {
729
- type: 'object',
730
- fields: [
731
- {
732
- name: 'level2Number',
733
- type: 'number',
734
- validation: (rule: Rule) => rule.custom(customValidationFn),
735
- },
736
- ],
737
- },
738
- ],
739
- },
740
- ],
741
- validation: (rule: Rule) => rule.custom(customValidationFn),
742
- },
743
- ],
744
- })
745
-
746
- const value = {
747
- level1Object: {level3String: 'a string'},
748
- level1Array: [{level2Number: 5}],
749
- }
750
-
751
- const resultPromise = validateItem({
752
- client: client as any,
753
- schema,
754
- value,
755
- document: undefined,
756
- parent: undefined,
757
- path: undefined,
758
- type: schema.get('root'),
759
- getDocumentExists: undefined,
760
- })
761
-
762
- // after `validateItem(...)` has been initiated, all of the custom
763
- // validation calls should have fired so the resolver size should equal the
764
- // amount of total fields
765
- expect(resolvers.size).toBe(6)
766
-
767
- for (const resolver of resolvers) {
768
- resolver(true)
769
- }
770
-
771
- await resultPromise
772
- })
773
701
  })
@@ -10,9 +10,12 @@ import {
10
10
  isBlockSchemaType,
11
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 type {SanityClient} from '@sanity/client'
15
17
  import typeString from './util/typeString'
18
+ import {cancelIdleCallback, requestIdleCallback} from './util/requestIdleCallback'
16
19
  import ValidationErrorClass from './ValidationError'
17
20
  import normalizeValidationRules from './util/normalizeValidationRules'
18
21
 
@@ -46,40 +49,78 @@ export function resolveTypeForArrayItem(
46
49
  candidates.find((candidate) => candidate.name === 'object' && primitive === 'object')
47
50
  )
48
51
  }
52
+ const EMPTY_MARKERS: ValidationMarker[] = []
49
53
 
50
54
  export default async function validateDocument(
51
- client: SanityClient,
55
+ getClient: (options: {apiVersion: string}) => SanityClient,
52
56
  doc: SanityDocument,
53
57
  schema: Schema,
54
58
  context?: Pick<ValidationContext, 'getDocumentExists'>
55
59
  ): Promise<ValidationMarker[]> {
60
+ return validateDocumentObservable(getClient, doc, schema, context).toPromise()
61
+ }
62
+
63
+ export function validateDocumentObservable(
64
+ getClient: (options: {apiVersion: string}) => SanityClient,
65
+ doc: SanityDocument,
66
+ schema: Schema,
67
+ context?: Pick<ValidationContext, 'getDocumentExists'>
68
+ ): Observable<ValidationMarker[]> {
56
69
  const documentType = schema.get(doc._type)
57
70
  if (!documentType) {
58
71
  console.warn('Schema type for object type "%s" not found, skipping validation', doc._type)
59
- return []
72
+ return of(EMPTY_MARKERS)
60
73
  }
61
74
 
62
- try {
63
- return await validateItem({
64
- client,
65
- schema,
66
- parent: undefined,
67
- value: doc,
68
- path: [],
69
- document: doc,
70
- type: documentType,
71
- getDocumentExists: context?.getDocumentExists,
72
- })
73
- } catch (err) {
74
- console.error(err)
75
- return [
76
- {
77
- level: 'error',
78
- path: [],
79
- item: new ValidationErrorClass(err?.message),
80
- },
81
- ]
75
+ const client = getClient({apiVersion: '2021-06-07'})
76
+ const validationOptions: ValidateItemOptions = {
77
+ client,
78
+ getClient,
79
+ schema,
80
+ parent: undefined,
81
+ value: doc,
82
+ path: [],
83
+ document: doc,
84
+ type: documentType,
85
+ getDocumentExists: context?.getDocumentExists,
82
86
  }
87
+
88
+ // <TEMPORARY UGLY HACK TO PRINT DEPRECATION WARNINGS ON USE>
89
+ /* eslint-disable no-proto */
90
+ const wrappedClient = client as any
91
+ validationOptions.client = [
92
+ ...Object.keys(client),
93
+ ...Object.keys(wrappedClient.__proto__),
94
+ ].reduce((acc, key) => {
95
+ const original = Object.hasOwnProperty.call(client, key)
96
+ ? wrappedClient[key]
97
+ : wrappedClient.__proto__[key]
98
+
99
+ return Object.defineProperty(acc, key, {
100
+ get() {
101
+ console.warn(
102
+ '`configContext.client` is deprecated and will be removed in the next release! Use `context.getClient({apiVersion: "2021-06-07"})` instead'
103
+ )
104
+ return original
105
+ },
106
+ })
107
+ }, {}) as any as SanityClient
108
+ /* eslint-enable no-proto */
109
+ // </TEMPORARY UGLY HACK TO PRINT DEPRECATION WARNINGS ON USE>
110
+
111
+ return validateItemObservable(validationOptions).pipe(
112
+ catchError((err) => {
113
+ console.error(err)
114
+ return of([
115
+ {
116
+ type: 'validation' as const,
117
+ level: 'error' as const,
118
+ path: [],
119
+ item: new ValidationErrorClass(err?.message),
120
+ },
121
+ ])
122
+ })
123
+ )
83
124
  }
84
125
 
85
126
  /**
@@ -97,27 +138,32 @@ type ValidateItemOptions = {
97
138
  value: unknown
98
139
  } & ExplicitUndefined<ValidationContext>
99
140
 
100
- export async function validateItem({
141
+ export function validateItem(opts: ValidateItemOptions): Promise<ValidationMarker[]> {
142
+ return validateItemObservable(opts).toPromise()
143
+ }
144
+
145
+ function validateItemObservable({
101
146
  value,
102
147
  type,
103
148
  path = [],
104
149
  parent,
105
150
  ...restOfContext
106
- }: ValidateItemOptions): Promise<ValidationMarker[]> {
151
+ }: ValidateItemOptions): Observable<ValidationMarker[]> {
107
152
  const rules = normalizeValidationRules(type)
108
-
109
153
  // run validation for the current value
110
154
  const selfChecks = rules.map((rule) =>
111
- rule.validate(value, {
112
- ...restOfContext,
113
- parent,
114
- path,
115
- type,
116
- })
155
+ defer(() =>
156
+ rule.validate(value, {
157
+ ...restOfContext,
158
+ parent,
159
+ path,
160
+ type,
161
+ })
162
+ )
117
163
  )
118
164
 
119
165
  // run validation for nested values (conditionally)
120
- let nestedChecks: Array<Promise<ValidationMarker[]>> = []
166
+ let nestedChecks: Array<Observable<ValidationMarker[]>> = []
121
167
 
122
168
  const selfIsRequired = rules.some((rule) => rule.isRequired())
123
169
  const shouldRunNestedObjectValidation =
@@ -144,12 +190,14 @@ export async function validateItem({
144
190
  const fieldType = fieldTypes[name]
145
191
  return normalizeValidationRules({...fieldType, validation}).map((subRule) => {
146
192
  const nestedValue = isRecord(value) ? value[name] : undefined
147
- return subRule.validate(nestedValue, {
148
- ...restOfContext,
149
- parent: value,
150
- path: path.concat(name),
151
- type: fieldType,
152
- })
193
+ return defer(() =>
194
+ subRule.validate(nestedValue, {
195
+ ...restOfContext,
196
+ parent: value,
197
+ path: path.concat(name),
198
+ type: fieldType,
199
+ })
200
+ )
153
201
  })
154
202
  })
155
203
  )
@@ -157,7 +205,7 @@ export async function validateItem({
157
205
  // Validation from each field's schema `validation: Rule => {/* ... */}` function
158
206
  nestedChecks = nestedChecks.concat(
159
207
  type.fields.map((field) =>
160
- validateItem({
208
+ validateItemObservable({
161
209
  ...restOfContext,
162
210
  parent: value,
163
211
  value: isRecord(value) ? value[field.name] : undefined,
@@ -177,7 +225,7 @@ export async function validateItem({
177
225
  if (shouldRunNestedValidationForArrays) {
178
226
  nestedChecks = nestedChecks.concat(
179
227
  value.map((item) =>
180
- validateItem({
228
+ validateItemObservable({
181
229
  ...restOfContext,
182
230
  parent: value,
183
231
  value: item,
@@ -198,16 +246,16 @@ export async function validateItem({
198
246
  const spanType = spanChildrenField.type.of.find(isSpanSchemaType)
199
247
 
200
248
  const annotations = (spanType?.annotations || []).reduce<Map<string, SchemaType>>(
201
- (map, annotationType) => {
202
- map.set(annotationType.name, annotationType)
203
- return map
249
+ (acc, annotationType) => {
250
+ acc.set(annotationType.name, annotationType)
251
+ return acc
204
252
  },
205
253
  new Map()
206
254
  )
207
255
 
208
256
  nestedChecks = nestedChecks.concat(
209
257
  value.markDefs.map((markDef) =>
210
- validateItem({
258
+ validateItemObservable({
211
259
  ...restOfContext,
212
260
  parent: value,
213
261
  value: markDef,
@@ -218,13 +266,31 @@ export async function validateItem({
218
266
  )
219
267
  }
220
268
 
221
- const results = (await Promise.all([...selfChecks, ...nestedChecks])).flat()
269
+ return defer(() => merge([...selfChecks, ...nestedChecks])).pipe(
270
+ mergeMap((validateNode) => concat(idle(), validateNode), 40),
271
+ mergeAll(),
272
+ toArray(),
273
+ map(flatten),
274
+ map((results) => {
275
+ // run `uniqBy` if `_fieldRules` are present because they can
276
+ // cause repeat markers
277
+ if (rules.some((rule) => rule._fieldRules)) {
278
+ return uniqBy(results, (rule) => JSON.stringify(rule))
279
+ }
280
+ return results
281
+ })
282
+ )
283
+ }
222
284
 
223
- // run `uniqBy` if `_fieldRules` are present because they can
224
- // cause repeat markers
225
- if (rules.some((rule) => rule._fieldRules)) {
226
- return uniqBy(results, (rule) => JSON.stringify(rule))
227
- }
285
+ function idle(timeout?: number): Observable<never> {
286
+ return new Observable<never>((observer) => {
287
+ const handle = requestIdleCallback(
288
+ () => {
289
+ observer.complete()
290
+ },
291
+ timeout ? {timeout} : undefined
292
+ )
228
293
 
229
- return results
294
+ return () => cancelIdleCallback(handle)
295
+ })
230
296
  }
@@ -23,7 +23,7 @@ function serializePath(path: Path): string {
23
23
  }
24
24
 
25
25
  const defaultIsUnique: SlugIsUniqueValidator = (slug, context) => {
26
- const {client, document, path, type} = context
26
+ const {getClient, document, path, type} = context
27
27
  const schemaOptions = type?.options as {disableArrayWarning?: boolean} | undefined
28
28
 
29
29
  if (!document) {
@@ -48,7 +48,7 @@ const defaultIsUnique: SlugIsUniqueValidator = (slug, context) => {
48
48
  `${atPath} == $slug`,
49
49
  ].join(' && ')
50
50
 
51
- return client.fetch<boolean>(
51
+ return getClient({apiVersion: '2022-09-09'}).fetch<boolean>(
52
52
  `!defined(*[${constraints}][0]._id)`,
53
53
  {
54
54
  docType,