@sanity/sdk 2.1.0 → 2.1.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.
@@ -0,0 +1,477 @@
1
+ import {describe, expect, test} from 'vitest'
2
+
3
+ import {defineIntent, type Intent, type IntentFilter} from './defineIntent'
4
+
5
+ describe('defineIntent', () => {
6
+ test('should return a valid intent object when all required fields are provided', () => {
7
+ const intent: Intent = {
8
+ id: 'viewHat',
9
+ action: 'view',
10
+ title: 'View a hat',
11
+ description: 'This lets you view a hat',
12
+ filters: [
13
+ {
14
+ projectId: 'some-project',
15
+ dataset: 'a-dataset',
16
+ types: ['hat'],
17
+ },
18
+ ],
19
+ }
20
+
21
+ const result = defineIntent(intent)
22
+
23
+ expect(result).toEqual(intent)
24
+ expect(result).toBe(intent)
25
+ })
26
+
27
+ test('should throw error when filters array is empty', () => {
28
+ const intent: Intent = {
29
+ id: 'globalIntent',
30
+ action: 'create',
31
+ title: 'Global Intent',
32
+ description: 'An intent with no filters',
33
+ filters: [],
34
+ }
35
+
36
+ expect(() => defineIntent(intent)).toThrow(
37
+ "Intent must have at least one filter. If you want to match everything, use {types: ['*']}",
38
+ )
39
+ })
40
+
41
+ test('should work with wildcard filter to match everything', () => {
42
+ const intent: Intent = {
43
+ id: 'globalIntent',
44
+ action: 'create',
45
+ title: 'Global Intent',
46
+ description: 'An intent that matches everything',
47
+ filters: [
48
+ {
49
+ types: ['*'],
50
+ },
51
+ ],
52
+ }
53
+
54
+ const result = defineIntent(intent)
55
+
56
+ expect(result).toEqual(intent)
57
+ expect(result.filters[0].types).toEqual(['*'])
58
+ })
59
+
60
+ test('should work with partial filters', () => {
61
+ const intent: Intent = {
62
+ id: 'partialFilter',
63
+ action: 'edit',
64
+ title: 'Partial Filter Intent',
65
+ description: 'An intent with partial filter criteria',
66
+ filters: [
67
+ {
68
+ projectId: 'some-project',
69
+ types: ['*'], // Add required types
70
+ },
71
+ {
72
+ types: ['document'],
73
+ // No projectId or dataset specified
74
+ },
75
+ ],
76
+ }
77
+
78
+ const result = defineIntent(intent)
79
+
80
+ expect(result).toEqual(intent)
81
+ })
82
+
83
+ test('should throw error when id is missing', () => {
84
+ const intent = {
85
+ action: 'view',
86
+ title: 'Test Intent',
87
+ description: 'Test description',
88
+ filters: [],
89
+ } as unknown as Intent
90
+
91
+ expect(() => defineIntent(intent)).toThrow('Intent must have an id')
92
+ })
93
+
94
+ test('should throw error when id is empty string', () => {
95
+ const intent: Intent = {
96
+ id: '',
97
+ action: 'view',
98
+ title: 'Test Intent',
99
+ description: 'Test description',
100
+ filters: [],
101
+ }
102
+
103
+ expect(() => defineIntent(intent)).toThrow('Intent must have an id')
104
+ })
105
+
106
+ test('should throw error when action is missing', () => {
107
+ const intent = {
108
+ id: 'test',
109
+ title: 'Test Intent',
110
+ description: 'Test description',
111
+ filters: [],
112
+ } as unknown as Intent
113
+
114
+ expect(() => defineIntent(intent)).toThrow('Intent must have an action')
115
+ })
116
+
117
+ test('should throw error when action is empty string', () => {
118
+ const intent = {
119
+ id: 'test',
120
+ action: '',
121
+ title: 'Test Intent',
122
+ description: 'Test description',
123
+ filters: [],
124
+ } as unknown as Intent
125
+
126
+ expect(() => defineIntent(intent)).toThrow('Intent must have an action')
127
+ })
128
+
129
+ test('should throw error when title is missing', () => {
130
+ const intent = {
131
+ id: 'test',
132
+ action: 'view',
133
+ description: 'Test description',
134
+ filters: [],
135
+ } as unknown as Intent
136
+
137
+ expect(() => defineIntent(intent)).toThrow('Intent must have a title')
138
+ })
139
+
140
+ test('should throw error when title is empty string', () => {
141
+ const intent: Intent = {
142
+ id: 'test',
143
+ action: 'view',
144
+ title: '',
145
+ description: 'Test description',
146
+ filters: [],
147
+ }
148
+
149
+ expect(() => defineIntent(intent)).toThrow('Intent must have a title')
150
+ })
151
+
152
+ test('should throw error when filters are missing', () => {
153
+ const intent = {
154
+ id: 'test',
155
+ action: 'view',
156
+ title: 'Test Intent',
157
+ description: 'Test description',
158
+ } as unknown as Intent
159
+
160
+ expect(() => defineIntent(intent)).toThrow('Intent must have a filters array')
161
+ })
162
+
163
+ test('should throw error when filters are not an array', () => {
164
+ const intent = {
165
+ id: 'test',
166
+ action: 'view',
167
+ title: 'Test Intent',
168
+ description: 'Test description',
169
+ filters: 'not an array',
170
+ } as unknown as Intent
171
+
172
+ expect(() => defineIntent(intent)).toThrow('Intent must have a filters array')
173
+ })
174
+
175
+ test('should work with complex filter combinations', () => {
176
+ const intent: Intent = {
177
+ id: 'complexIntent',
178
+ action: 'edit',
179
+ title: 'Complex Intent',
180
+ description: 'An intent with multiple complex filters',
181
+ filters: [
182
+ {
183
+ projectId: 'project-1',
184
+ dataset: 'production',
185
+ types: ['article', 'blogPost'],
186
+ },
187
+ {
188
+ projectId: 'project-2',
189
+ dataset: 'staging',
190
+ types: ['product'],
191
+ },
192
+ {
193
+ // Filter with only types
194
+ types: ['global-document'],
195
+ },
196
+ ],
197
+ }
198
+
199
+ const result = defineIntent(intent)
200
+
201
+ expect(result).toEqual(intent)
202
+ expect(result.filters).toHaveLength(3)
203
+ expect(result.filters[0].types).toEqual(['article', 'blogPost'])
204
+ expect(result.filters[2].projectId).toBeUndefined()
205
+ expect(result.filters[2].dataset).toBeUndefined()
206
+ })
207
+
208
+ // Filter validation tests
209
+ test('should throw error for empty filter object', () => {
210
+ const intent = {
211
+ id: 'test',
212
+ action: 'view',
213
+ title: 'Test Intent',
214
+ description: 'Test description',
215
+ filters: [{}],
216
+ } as unknown as Intent
217
+
218
+ expect(() => defineIntent(intent)).toThrow(
219
+ "Filter at index 0 must have a types property. Use ['*'] to match all document types.",
220
+ )
221
+ })
222
+
223
+ test('should throw error for non-object filter', () => {
224
+ const intent = {
225
+ id: 'test',
226
+ action: 'view',
227
+ title: 'Test Intent',
228
+ description: 'Test description',
229
+ filters: ['not an object'],
230
+ } as unknown as Intent
231
+
232
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0 must be an object')
233
+ })
234
+
235
+ test('should throw error for non-string projectId', () => {
236
+ const intent = {
237
+ id: 'test',
238
+ action: 'view',
239
+ title: 'Test Intent',
240
+ description: 'Test description',
241
+ filters: [{projectId: 123, types: ['*']}],
242
+ } as unknown as Intent
243
+
244
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: projectId must be a string')
245
+ })
246
+
247
+ test('should throw error for empty projectId', () => {
248
+ const intent: Intent = {
249
+ id: 'test',
250
+ action: 'view',
251
+ title: 'Test Intent',
252
+ description: 'Test description',
253
+ filters: [{projectId: '', types: ['*']}],
254
+ }
255
+
256
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: projectId cannot be empty')
257
+ })
258
+
259
+ test('should throw error for whitespace-only projectId', () => {
260
+ const intent: Intent = {
261
+ id: 'test',
262
+ action: 'view',
263
+ title: 'Test Intent',
264
+ description: 'Test description',
265
+ filters: [{projectId: ' ', types: ['*']}],
266
+ }
267
+
268
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: projectId cannot be empty')
269
+ })
270
+
271
+ test('should throw error for non-string dataset', () => {
272
+ const intent = {
273
+ id: 'test',
274
+ action: 'view',
275
+ title: 'Test Intent',
276
+ description: 'Test description',
277
+ filters: [{projectId: 'test', dataset: 123, types: ['*']}],
278
+ } as unknown as Intent
279
+
280
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: dataset must be a string')
281
+ })
282
+
283
+ test('should throw error for empty dataset', () => {
284
+ const intent: Intent = {
285
+ id: 'test',
286
+ action: 'view',
287
+ title: 'Test Intent',
288
+ description: 'Test description',
289
+ filters: [{dataset: '', types: ['*']}],
290
+ }
291
+
292
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: dataset cannot be empty')
293
+ })
294
+
295
+ test('should throw error when dataset is specified without projectId', () => {
296
+ const intent: Intent = {
297
+ id: 'test',
298
+ action: 'view',
299
+ title: 'Test Intent',
300
+ description: 'Test description',
301
+ filters: [{dataset: 'production', types: ['*']}],
302
+ }
303
+
304
+ expect(() => defineIntent(intent)).toThrow(
305
+ 'Filter at index 0: dataset cannot be specified without projectId',
306
+ )
307
+ })
308
+
309
+ test('should throw error when dataset is specified with empty projectId', () => {
310
+ const intent: Intent = {
311
+ id: 'test',
312
+ action: 'view',
313
+ title: 'Test Intent',
314
+ description: 'Test description',
315
+ filters: [{projectId: '', dataset: 'production', types: ['*']}],
316
+ }
317
+
318
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: projectId cannot be empty')
319
+ })
320
+
321
+ test('should work when dataset is specified with valid projectId', () => {
322
+ const intent: Intent = {
323
+ id: 'test',
324
+ action: 'view',
325
+ title: 'Test Intent',
326
+ description: 'Test description',
327
+ filters: [{projectId: 'my-project', dataset: 'production', types: ['*']}],
328
+ }
329
+
330
+ const result = defineIntent(intent)
331
+ expect(result).toEqual(intent)
332
+ })
333
+
334
+ test('should throw error for non-array types', () => {
335
+ const intent = {
336
+ id: 'test',
337
+ action: 'view',
338
+ title: 'Test Intent',
339
+ description: 'Test description',
340
+ filters: [{types: 'not-an-array'}],
341
+ } as unknown as Intent
342
+
343
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: types must be an array')
344
+ })
345
+
346
+ test('should throw error for empty types array', () => {
347
+ const intent: Intent = {
348
+ id: 'test',
349
+ action: 'view',
350
+ title: 'Test Intent',
351
+ description: 'Test description',
352
+ filters: [{types: []}],
353
+ }
354
+
355
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: types array cannot be empty')
356
+ })
357
+
358
+ test('should throw error for non-string type in types array', () => {
359
+ const intent = {
360
+ id: 'test',
361
+ action: 'view',
362
+ title: 'Test Intent',
363
+ description: 'Test description',
364
+ filters: [{types: ['valid', 123, 'also-valid']}],
365
+ } as unknown as Intent
366
+
367
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: types[1] must be a string')
368
+ })
369
+
370
+ test('should throw error for empty string in types array', () => {
371
+ const intent: Intent = {
372
+ id: 'test',
373
+ action: 'view',
374
+ title: 'Test Intent',
375
+ description: 'Test description',
376
+ filters: [{types: ['valid', '', 'also-valid']}],
377
+ }
378
+
379
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: types[1] cannot be empty')
380
+ })
381
+
382
+ test('should throw error for whitespace-only string in types array', () => {
383
+ const intent: Intent = {
384
+ id: 'test',
385
+ action: 'view',
386
+ title: 'Test Intent',
387
+ description: 'Test description',
388
+ filters: [{types: ['valid', ' ', 'also-valid']}],
389
+ }
390
+
391
+ expect(() => defineIntent(intent)).toThrow('Filter at index 0: types[1] cannot be empty')
392
+ })
393
+
394
+ test('should throw error when wildcard is mixed with other types', () => {
395
+ const intent: Intent = {
396
+ id: 'test',
397
+ action: 'view',
398
+ title: 'Test Intent',
399
+ description: 'Test description',
400
+ filters: [{types: ['*', 'document']}],
401
+ }
402
+
403
+ expect(() => defineIntent(intent)).toThrow(
404
+ "Filter at index 0: when using wildcard '*', it must be the only type in the array",
405
+ )
406
+ })
407
+
408
+ test('should throw error when wildcard appears with other types in different order', () => {
409
+ const intent: Intent = {
410
+ id: 'test',
411
+ action: 'view',
412
+ title: 'Test Intent',
413
+ description: 'Test description',
414
+ filters: [{types: ['document', 'article', '*']}],
415
+ }
416
+
417
+ expect(() => defineIntent(intent)).toThrow(
418
+ "Filter at index 0: when using wildcard '*', it must be the only type in the array",
419
+ )
420
+ })
421
+
422
+ test('should work with valid individual filter properties', () => {
423
+ const intent: Intent = {
424
+ id: 'test',
425
+ action: 'view',
426
+ title: 'Test Intent',
427
+ description: 'Test description',
428
+ filters: [
429
+ {projectId: 'my-project', types: ['*']},
430
+ {projectId: 'my-project', dataset: 'production', types: ['*']},
431
+ {types: ['document']},
432
+ {types: ['*']}, // Valid wildcard usage
433
+ ],
434
+ }
435
+
436
+ const result = defineIntent(intent)
437
+ expect(result).toEqual(intent)
438
+ })
439
+
440
+ test('should provide correct filter index in error messages for multiple filters', () => {
441
+ const intent: Intent = {
442
+ id: 'test',
443
+ action: 'view',
444
+ title: 'Test Intent',
445
+ description: 'Test description',
446
+ filters: [
447
+ {projectId: 'valid-project', types: ['*']},
448
+ {projectId: '', types: ['*']},
449
+ ],
450
+ }
451
+
452
+ expect(() => defineIntent(intent)).toThrow('Filter at index 1: projectId cannot be empty')
453
+ })
454
+ })
455
+
456
+ describe('IntentFilter interface', () => {
457
+ test('should require types property', () => {
458
+ // This is more of a TypeScript compile-time test
459
+ // but we can verify the structure is as expected
460
+ const filter1: IntentFilter = {types: ['*']} // types is now required
461
+ const filter2: IntentFilter = {projectId: 'test', types: ['*']} // types is now required
462
+ const filter3: IntentFilter = {dataset: 'test', types: ['*']} // types is now required
463
+ const filter4: IntentFilter = {types: ['test']}
464
+ const filter5: IntentFilter = {
465
+ projectId: 'test',
466
+ dataset: 'test',
467
+ types: ['test'],
468
+ }
469
+
470
+ // These should all be valid filter objects
471
+ expect(filter1).toBeDefined()
472
+ expect(filter2).toBeDefined()
473
+ expect(filter3).toBeDefined()
474
+ expect(filter4).toBeDefined()
475
+ expect(filter5).toBeDefined()
476
+ })
477
+ })