@sanity/client 3.3.1 → 3.4.0-beta.esm.2

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.
@@ -1,2561 +0,0 @@
1
- const test = require('tape')
2
- const nock = require('nock')
3
- const assign = require('xtend')
4
- const path = require('path')
5
- const fs = require('fs')
6
- const validators = require('../src/validators')
7
- const observableOf = require('rxjs').of
8
- const {filter} = require('rxjs/operators')
9
- const sanityClient = require('../src/sanityClient')
10
-
11
- const SanityClient = sanityClient
12
- const noop = () => {
13
- /* intentional noop */
14
- }
15
- const bufferFrom = (content, enc) =>
16
- Buffer.from ? Buffer.from(content, enc) : new Buffer(content, enc) // eslint-disable-line no-buffer-constructor
17
-
18
- const apiHost = 'api.sanity.url'
19
- const defaultProjectId = 'bf1942'
20
- const projectHost = (projectId) => `https://${projectId || defaultProjectId}.${apiHost}`
21
- const clientConfig = {
22
- apiHost: `https://${apiHost}`,
23
- projectId: 'bf1942',
24
- apiVersion: '1',
25
- dataset: 'foo',
26
- useCdn: false,
27
- }
28
-
29
- const getClient = (conf) => sanityClient(assign({}, clientConfig, conf || {}))
30
- const fixture = (name) => path.join(__dirname, 'fixtures', name)
31
- const ifError = (t) => (err) => {
32
- t.ifError(err)
33
- if (err) {
34
- t.end()
35
- }
36
- }
37
-
38
- /*****************
39
- * BASE CLIENT *
40
- *****************/
41
- test('can construct client with new keyword', (t) => {
42
- const client = new SanityClient({projectId: 'abc123'})
43
- t.equal(client.config().projectId, 'abc123', 'constructor opts are set')
44
- t.end()
45
- })
46
-
47
- test('can construct client without new keyword', (t) => {
48
- const client = sanityClient({projectId: 'abc123'})
49
- t.equal(client.config().projectId, 'abc123', 'constructor opts are set')
50
- t.end()
51
- })
52
-
53
- test('can get and set config', (t) => {
54
- const client = sanityClient({projectId: 'abc123'})
55
- t.equal(client.config().projectId, 'abc123', 'constructor opts are set')
56
- t.equal(client.config({projectId: 'def456'}), client, 'returns client on set')
57
- t.equal(client.config().projectId, 'def456', 'new config is set')
58
- t.end()
59
- })
60
-
61
- test('config getter returns a cloned object', (t) => {
62
- const client = sanityClient({projectId: 'abc123'})
63
- t.equal(client.config().projectId, 'abc123', 'constructor opts are set')
64
- const config = client.config()
65
- config.projectId = 'def456'
66
- t.equal(client.config().projectId, 'abc123', 'returned object does not mutate client config')
67
- t.end()
68
- })
69
-
70
- test('calling config() reconfigures observable API too', (t) => {
71
- const client = sanityClient({projectId: 'abc123'})
72
-
73
- client.config({projectId: 'def456'})
74
- t.equal(client.observable.config().projectId, 'def456', 'Observable API gets reconfigured')
75
- t.end()
76
- })
77
-
78
- test('can clone client', (t) => {
79
- const client = sanityClient({projectId: 'abc123'})
80
- t.equal(client.config().projectId, 'abc123', 'constructor opts are set')
81
-
82
- const client2 = client.clone()
83
- client2.config({projectId: 'def456'})
84
- t.equal(client.config().projectId, 'abc123')
85
- t.equal(client2.config().projectId, 'def456')
86
- t.end()
87
- })
88
-
89
- test('can clone client with new config', (t) => {
90
- const client = sanityClient({projectId: 'abc123', apiVersion: 'v2021-03-25'})
91
- t.equal(client.config().projectId, 'abc123', 'constructor opts are set')
92
- t.equal(client.config().apiVersion, '2021-03-25', 'constructor opts are set')
93
-
94
- const client2 = client.withConfig({projectId: 'def456', apiVersion: 'v1'})
95
- t.equal(client.config().projectId, 'abc123')
96
- t.equal(client2.config().projectId, 'def456')
97
-
98
- t.equal(client.config().apiVersion, '2021-03-25')
99
- t.equal(client2.config().apiVersion, '1')
100
- t.end()
101
- })
102
-
103
- test('throws if no projectId is set', (t) => {
104
- t.throws(sanityClient, /projectId/)
105
- t.end()
106
- })
107
-
108
- test('throws on invalid project ids', (t) => {
109
- t.throws(() => sanityClient({projectId: '*foo*'}), /projectId.*?can only contain/i)
110
- t.end()
111
- })
112
-
113
- test('throws on invalid dataset names', (t) => {
114
- t.throws(
115
- () => sanityClient({projectId: 'abc123', dataset: '*foo*'}),
116
- /Datasets can only contain/i
117
- )
118
- t.end()
119
- })
120
-
121
- test('throws on invalid request tag prefix', (t) => {
122
- t.throws(
123
- () => sanityClient({projectId: 'abc123', dataset: 'foo', requestTagPrefix: 'no#shot'}),
124
- /tag can only contain alphanumeric/i
125
- )
126
- t.end()
127
- })
128
-
129
- test('accepts alias in dataset field', (t) => {
130
- t.doesNotThrow(
131
- () => sanityClient({projectId: 'abc123', dataset: '~alias'}),
132
- /Datasets can only contain/i
133
- )
134
- t.end()
135
- })
136
-
137
- test('can use request() for API-relative requests', (t) => {
138
- nock(projectHost()).get('/v1/ping').reply(200, {pong: true})
139
-
140
- getClient()
141
- .request({uri: '/ping'})
142
- .then((res) => t.equal(res.pong, true))
143
- .catch(t.ifError)
144
- .then(t.end)
145
- })
146
-
147
- test('can use request() for API-relative requests (custom api version)', (t) => {
148
- nock(projectHost()).get('/v2019-01-29/ping').reply(200, {pong: true})
149
-
150
- getClient({apiVersion: '2019-01-29'})
151
- .request({uri: '/ping'})
152
- .then((res) => t.equal(res.pong, true))
153
- .catch(t.ifError)
154
- .then(t.end)
155
- })
156
-
157
- test('observable requests are lazy', (t) => {
158
- let didRequest = false
159
- nock(projectHost())
160
- .get('/v1/ping')
161
- .reply(() => {
162
- didRequest = true
163
- return [200, {pong: true}]
164
- })
165
-
166
- const req = getClient().observable.request({uri: '/ping'})
167
-
168
- setTimeout(() => {
169
- t.false(didRequest)
170
- req.subscribe({
171
- next: (res) => {
172
- t.true(didRequest)
173
- },
174
- error: t.ifError,
175
- complete: t.end,
176
- })
177
- }, 1)
178
- })
179
-
180
- test('observable requests are cold', (t) => {
181
- let requestCount = 0
182
- nock(projectHost())
183
- .get('/v1/ping')
184
- .twice()
185
- .reply(() => {
186
- requestCount++
187
- return [200, {pong: true}]
188
- })
189
-
190
- const req = getClient().observable.request({uri: '/ping'})
191
-
192
- t.equal(requestCount, 0)
193
- req.subscribe({
194
- next: () => {
195
- t.equal(requestCount, 1)
196
- req.subscribe({
197
- next: () => {
198
- t.equal(requestCount, 2)
199
- },
200
- error: t.ifError,
201
- complete: t.end,
202
- })
203
- },
204
- error: t.ifError,
205
- })
206
- })
207
- test('can use getUrl() to get API-relative paths', (t) => {
208
- t.equal(getClient().getUrl('/bar/baz'), `${projectHost()}/v1/bar/baz`)
209
- t.end()
210
- })
211
-
212
- test('can use getUrl() to get API-relative paths (custom api version)', (t) => {
213
- t.equal(
214
- getClient({apiVersion: '2019-01-29'}).getUrl('/bar/baz'),
215
- `${projectHost()}/v2019-01-29/bar/baz`
216
- )
217
- t.end()
218
- })
219
-
220
- test('validation', (t) => {
221
- t.doesNotThrow(
222
- () => validators.validateDocumentId('op', 'barfoo'),
223
- /document ID in format/,
224
- 'does not throw on valid ID'
225
- )
226
- t.doesNotThrow(
227
- () => validators.validateDocumentId('op', 'bar.foo.baz'),
228
- /document ID in format/,
229
- 'does not throw on valid ID'
230
- )
231
- t.throws(
232
- () => validators.validateDocumentId('op', 'blah#blah'),
233
- /not a valid document ID/,
234
- 'throws on invalid ID'
235
- )
236
- t.end()
237
- })
238
-
239
- /*****************
240
- * PROJECTS *
241
- *****************/
242
- test('can request list of projects', (t) => {
243
- nock(`https://${apiHost}`)
244
- .get('/v1/projects')
245
- .reply(200, [{projectId: 'foo'}, {projectId: 'bar'}])
246
-
247
- const client = sanityClient({useProjectHostname: false, apiHost: `https://${apiHost}`})
248
- client.projects
249
- .list()
250
- .then((projects) => {
251
- t.equal(projects.length, 2, 'should have two projects')
252
- t.equal(projects[0].projectId, 'foo', 'should have project id')
253
- })
254
- .catch(t.ifError)
255
- .then(t.end)
256
- })
257
-
258
- test('can request list of projects (custom api version)', (t) => {
259
- nock(`https://${apiHost}`)
260
- .get('/v2019-01-29/projects')
261
- .reply(200, [{projectId: 'foo'}, {projectId: 'bar'}])
262
-
263
- const client = sanityClient({
264
- useProjectHostname: false,
265
- apiHost: `https://${apiHost}`,
266
- apiVersion: '2019-01-29',
267
- })
268
- client.projects
269
- .list()
270
- .then((projects) => {
271
- t.equal(projects.length, 2, 'should have two projects')
272
- t.equal(projects[0].projectId, 'foo', 'should have project id')
273
- })
274
- .catch(t.ifError)
275
- .then(t.end)
276
- })
277
-
278
- test('can request project by id', (t) => {
279
- const doc = {
280
- _id: 'projects.n1f7y',
281
- projectId: 'n1f7y',
282
- displayName: 'Movies Unlimited',
283
- studioHost: 'movies',
284
- members: [
285
- {
286
- id: 'someuserid',
287
- role: 'administrator',
288
- },
289
- ],
290
- }
291
-
292
- nock(`https://${apiHost}`).get('/v1/projects/n1f7y').reply(200, doc)
293
-
294
- const client = sanityClient({useProjectHostname: false, apiHost: `https://${apiHost}`})
295
- client.projects
296
- .getById('n1f7y')
297
- .then((project) => t.deepEqual(project, doc))
298
- .catch(t.ifError)
299
- .then(t.end)
300
- })
301
-
302
- /*****************
303
- * DATASETS *
304
- *****************/
305
- test('throws when trying to create dataset with invalid name', (t) => {
306
- t.throws(() => getClient().datasets.create('*foo*'), /Datasets can only contain/i)
307
- t.end()
308
- })
309
-
310
- test('throws when trying to delete dataset with invalid name', (t) => {
311
- t.throws(() => getClient().datasets.delete('*foo*'), /Datasets can only contain/i)
312
- t.end()
313
- })
314
-
315
- test('can create dataset', (t) => {
316
- nock(projectHost()).put('/v1/datasets/bar').reply(200)
317
- getClient().datasets.create('bar').catch(t.ifError).then(t.end)
318
- })
319
-
320
- test('can delete dataset', (t) => {
321
- nock(projectHost()).delete('/v1/datasets/bar').reply(200)
322
- getClient().datasets.delete('bar').catch(t.ifError).then(t.end)
323
- })
324
-
325
- test('can list datasets', (t) => {
326
- nock(projectHost()).get('/v1/datasets').reply(200, ['foo', 'bar'])
327
- getClient()
328
- .datasets.list()
329
- .then((sets) => {
330
- t.deepEqual(sets, ['foo', 'bar'])
331
- })
332
- .catch(t.ifError)
333
- .then(t.end)
334
- })
335
-
336
- /*****************
337
- * DATA *
338
- *****************/
339
- test('can query for documents', (t) => {
340
- const query = 'beerfiesta.beer[.title == $beerName]'
341
- const params = {beerName: 'Headroom Double IPA'}
342
- const qs =
343
- 'beerfiesta.beer%5B.title%20%3D%3D%20%24beerName%5D&%24beerName=%22Headroom%20Double%20IPA%22'
344
-
345
- nock(projectHost())
346
- .get(`/v1/data/query/foo?query=${qs}`)
347
- .reply(200, {
348
- ms: 123,
349
- q: query,
350
- result: [{_id: 'njgNkngskjg', rating: 5}],
351
- })
352
-
353
- getClient()
354
- .fetch(query, params)
355
- .then((res) => {
356
- t.equal(res.length, 1, 'length should match')
357
- t.equal(res[0].rating, 5, 'data should match')
358
- })
359
- .catch(t.ifError)
360
- .then(t.end)
361
- })
362
-
363
- test('can query for documents and return full response', (t) => {
364
- const query = 'beerfiesta.beer[.title == $beerName]'
365
- const params = {beerName: 'Headroom Double IPA'}
366
- const qs =
367
- 'beerfiesta.beer%5B.title%20%3D%3D%20%24beerName%5D&%24beerName=%22Headroom%20Double%20IPA%22'
368
-
369
- nock(projectHost())
370
- .get(`/v1/data/query/foo?query=${qs}`)
371
- .reply(200, {
372
- ms: 123,
373
- q: query,
374
- result: [{_id: 'njgNkngskjg', rating: 5}],
375
- })
376
-
377
- getClient()
378
- .fetch(query, params, {filterResponse: false})
379
- .then((res) => {
380
- t.equal(res.ms, 123, 'should include timing info')
381
- t.equal(res.q, query, 'should include query')
382
- t.equal(res.result.length, 1, 'length should match')
383
- t.equal(res.result[0].rating, 5, 'data should match')
384
- })
385
- .catch(t.ifError)
386
- .then(t.end)
387
- })
388
-
389
- test('can query for documents with request tag', (t) => {
390
- nock(projectHost())
391
- .get(`/v1/data/query/foo?query=*&tag=mycompany.syncjob`)
392
- .reply(200, {
393
- ms: 123,
394
- q: '*',
395
- result: [{_id: 'njgNkngskjg', rating: 5}],
396
- })
397
-
398
- getClient()
399
- .fetch('*', {}, {tag: 'mycompany.syncjob'})
400
- .then((res) => {
401
- t.equal(res.length, 1, 'length should match')
402
- t.equal(res[0].rating, 5, 'data should match')
403
- })
404
- .catch(t.ifError)
405
- .then(t.end)
406
- })
407
-
408
- test('throws on invalid request tag on request', (t) => {
409
- nock(projectHost())
410
- .get(`/v1/data/query/foo?query=*&tag=mycompany.syncjob`)
411
- .reply(200, {
412
- ms: 123,
413
- q: '*',
414
- result: [{_id: 'njgNkngskjg', rating: 5}],
415
- })
416
-
417
- t.throws(() => {
418
- getClient().fetch('*', {}, {tag: 'mycompany syncjob ok'}).catch(t.ifError).then(t.end)
419
- }, /tag can only contain alphanumeric/i)
420
- t.end()
421
- })
422
-
423
- test('can use a tag-prefixed client', (t) => {
424
- nock(projectHost())
425
- .get(`/v1/data/query/foo?query=*&tag=mycompany.syncjob`)
426
- .reply(200, {
427
- ms: 123,
428
- q: '*',
429
- result: [{_id: 'njgNkngskjg', rating: 5}],
430
- })
431
-
432
- getClient({requestTagPrefix: 'mycompany'})
433
- .fetch('*', {}, {tag: 'syncjob'})
434
- .then((res) => {
435
- t.equal(res.length, 1, 'length should match')
436
- t.equal(res[0].rating, 5, 'data should match')
437
- })
438
- .catch(t.ifError)
439
- .then(t.end)
440
- })
441
-
442
- test('handles api errors gracefully', (t) => {
443
- const response = {
444
- statusCode: 403,
445
- error: 'Forbidden',
446
- message: 'You are not allowed to access this resource',
447
- }
448
-
449
- nock(projectHost()).get('/v1/data/query/foo?query=area51').times(5).reply(403, response)
450
-
451
- getClient()
452
- .fetch('area51')
453
- .then((res) => {
454
- t.fail('Resolve handler should not be called on failure')
455
- t.end()
456
- })
457
- .catch((err) => {
458
- t.ok(err instanceof Error, 'should be error')
459
- t.ok(err.message.includes(response.error), 'should contain error code')
460
- t.ok(err.message.includes(response.message), 'should contain error message')
461
- t.ok(err.responseBody.includes(response.message), 'responseBody should be populated')
462
- t.end()
463
- })
464
- })
465
-
466
- test('handles db errors gracefully', (t) => {
467
- const response = {
468
- error: {
469
- column: 13,
470
- line: 'foo.bar.baz 12#[{',
471
- lineNumber: 1,
472
- description: 'Unable to parse entire expression',
473
- query: 'foo.bar.baz 12#[{',
474
- type: 'gqlParseError',
475
- },
476
- }
477
-
478
- nock(projectHost())
479
- .get('/v1/data/query/foo?query=foo.bar.baz%20%2012%23%5B%7B')
480
- .reply(400, response)
481
-
482
- getClient()
483
- .fetch('foo.bar.baz 12#[{')
484
- .then((res) => {
485
- t.fail('Resolve handler should not be called on failure')
486
- t.end()
487
- })
488
- .catch((err) => {
489
- t.ok(err instanceof Error, 'should be error')
490
- t.ok(err.message.includes(response.error.description), 'should contain error description')
491
- t.equal(err.details.column, response.error.column, 'error should have details object')
492
- t.equal(err.details.line, response.error.line, 'error should have details object')
493
- t.end()
494
- })
495
- })
496
-
497
- test('can query for single document', (t) => {
498
- nock(projectHost())
499
- .get('/v1/data/doc/foo/abc123')
500
- .reply(200, {
501
- ms: 123,
502
- documents: [{_id: 'abc123', mood: 'lax'}],
503
- })
504
-
505
- getClient()
506
- .getDocument('abc123')
507
- .then((res) => {
508
- t.equal(res.mood, 'lax', 'data should match')
509
- })
510
- .catch(t.ifError)
511
- .then(t.end)
512
- })
513
-
514
- test('can query for single document with request tag', (t) => {
515
- nock(projectHost())
516
- .get('/v1/data/doc/foo/abc123?tag=some.tag')
517
- .reply(200, {
518
- ms: 123,
519
- documents: [{_id: 'abc123', mood: 'lax'}],
520
- })
521
-
522
- getClient()
523
- .getDocument('abc123', {tag: 'some.tag'})
524
- .then((res) => {
525
- t.equal(res.mood, 'lax', 'data should match')
526
- })
527
- .catch(t.ifError)
528
- .then(t.end)
529
- })
530
-
531
- test('can query for multiple documents', (t) => {
532
- nock(projectHost())
533
- .get('/v1/data/doc/foo/abc123,abc321')
534
- .reply(200, {
535
- ms: 123,
536
- documents: [
537
- {_id: 'abc123', mood: 'lax'},
538
- {_id: 'abc321', mood: 'tense'},
539
- ],
540
- })
541
-
542
- getClient()
543
- .getDocuments(['abc123', 'abc321'])
544
- .then(([abc123, abc321]) => {
545
- t.equal(abc123.mood, 'lax', 'data should match')
546
- t.equal(abc321.mood, 'tense', 'data should match')
547
- })
548
- .catch(t.ifError)
549
- .then(t.end)
550
- })
551
-
552
- test('can query for multiple documents with tag', (t) => {
553
- nock(projectHost())
554
- .get('/v1/data/doc/foo/abc123,abc321?tag=mood.docs')
555
- .reply(200, {
556
- ms: 123,
557
- documents: [
558
- {_id: 'abc123', mood: 'lax'},
559
- {_id: 'abc321', mood: 'tense'},
560
- ],
561
- })
562
-
563
- getClient()
564
- .getDocuments(['abc123', 'abc321'], {tag: 'mood.docs'})
565
- .then(([abc123, abc321]) => {
566
- t.equal(abc123.mood, 'lax', 'data should match')
567
- t.equal(abc321.mood, 'tense', 'data should match')
568
- })
569
- .catch(t.ifError)
570
- .then(t.end)
571
- })
572
-
573
- test('preserves the position of requested documents', (t) => {
574
- nock(projectHost())
575
- .get('/v1/data/doc/foo/abc123,abc321,abc456')
576
- .reply(200, {
577
- ms: 123,
578
- documents: [
579
- {_id: 'abc456', mood: 'neutral'},
580
- {_id: 'abc321', mood: 'tense'},
581
- ],
582
- })
583
-
584
- getClient()
585
- .getDocuments(['abc123', 'abc321', 'abc456'])
586
- .then(([abc123, abc321, abc456]) => {
587
- t.equal(abc123, null, 'first item should be null')
588
- t.equal(abc321.mood, 'tense', 'data should match')
589
- t.equal(abc456.mood, 'neutral', 'data should match')
590
- })
591
- .catch(t.ifError)
592
- .then(t.end)
593
- })
594
-
595
- test('gives http statuscode as error if no body is present on errors', (t) => {
596
- nock(projectHost()).get('/v1/data/doc/foo/abc123').reply(400)
597
-
598
- getClient()
599
- .getDocument('abc123')
600
- .then((res) => {
601
- t.fail('Resolve handler should not be called on failure')
602
- t.end()
603
- })
604
- .catch((err) => {
605
- t.ok(err instanceof Error, 'should be error')
606
- t.ok(err.message.includes('HTTP 400'), 'should contain status code')
607
- t.end()
608
- })
609
- })
610
-
611
- test('populates response body on errors', (t) => {
612
- nock(projectHost()).get('/v1/data/doc/foo/abc123').times(5).reply(400, 'Some Weird Error')
613
-
614
- getClient()
615
- .getDocument('abc123')
616
- .then((res) => {
617
- t.fail('Resolve handler should not be called on failure')
618
- t.end()
619
- })
620
- .catch((err) => {
621
- t.ok(err instanceof Error, 'should be error')
622
- t.ok(err.message.includes('HTTP 400'), 'should contain status code')
623
- t.ok((err.responseBody || '').includes('Some Weird Error'), 'body populated')
624
- t.end()
625
- })
626
- })
627
-
628
- test('throws if trying to perform data request without dataset', (t) => {
629
- t.throws(
630
- () => sanityClient({projectId: 'foo'}).fetch('blah'),
631
- Error,
632
- /dataset.*?must be provided/
633
- )
634
- t.end()
635
- })
636
-
637
- test('can create documents', (t) => {
638
- const doc = {_id: 'abc123', name: 'Raptor'}
639
-
640
- nock(projectHost())
641
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', {
642
- mutations: [{create: doc}],
643
- })
644
- .reply(200, {
645
- transactionId: 'abc123',
646
- results: [
647
- {
648
- document: {_id: 'abc123', _createdAt: '2016-10-24T08:09:32.997Z', name: 'Raptor'},
649
- operation: 'create',
650
- },
651
- ],
652
- })
653
-
654
- getClient()
655
- .create(doc)
656
- .then((res) => {
657
- t.equal(res._id, doc._id, 'document id returned')
658
- t.ok(res._createdAt, 'server-generated attributes are included')
659
- })
660
- .catch(t.ifError)
661
- .then(t.end)
662
- })
663
-
664
- test('can create documents without specifying ID', (t) => {
665
- const doc = {name: 'Raptor'}
666
- const expectedBody = {mutations: [{create: Object.assign({}, doc)}]}
667
- nock(projectHost())
668
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
669
- .reply(200, {
670
- transactionId: '123abc',
671
- results: [
672
- {
673
- id: 'abc456',
674
- document: {_id: 'abc456', name: 'Raptor'},
675
- },
676
- ],
677
- })
678
-
679
- getClient()
680
- .create(doc)
681
- .then((res) => {
682
- t.equal(res._id, 'abc456', 'document id returned')
683
- })
684
- .catch(t.ifError)
685
- .then(t.end)
686
- })
687
-
688
- test('can create documents with request tag', (t) => {
689
- const doc = {name: 'Raptor'}
690
- const expectedBody = {mutations: [{create: Object.assign({}, doc)}]}
691
- nock(projectHost())
692
- .post(
693
- '/v1/data/mutate/foo?tag=dino.import&returnIds=true&returnDocuments=true&visibility=sync',
694
- expectedBody
695
- )
696
- .reply(200, {
697
- transactionId: '123abc',
698
- results: [
699
- {
700
- id: 'abc456',
701
- document: {_id: 'abc456', name: 'Raptor'},
702
- },
703
- ],
704
- })
705
-
706
- getClient()
707
- .create(doc, {tag: 'dino.import'})
708
- .then((res) => {
709
- t.equal(res._id, 'abc456', 'document id returned')
710
- })
711
- .catch(t.ifError)
712
- .then(t.end)
713
- })
714
-
715
- test('can tell create() not to return documents', (t) => {
716
- const doc = {_id: 'abc123', name: 'Raptor'}
717
- nock(projectHost())
718
- .post('/v1/data/mutate/foo?returnIds=true&visibility=sync', {mutations: [{create: doc}]})
719
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'create'}]})
720
-
721
- getClient()
722
- .create(doc, {returnDocuments: false})
723
- .then((res) => {
724
- t.equal(res.transactionId, 'abc123', 'returns transaction ID')
725
- t.equal(res.documentId, 'abc123', 'returns document id')
726
- })
727
- .catch(t.ifError)
728
- .then(t.end)
729
- })
730
-
731
- test('can tell create() to use non-default visibility mode', (t) => {
732
- const doc = {_id: 'abc123', name: 'Raptor'}
733
- nock(projectHost())
734
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=async', {
735
- mutations: [{create: doc}],
736
- })
737
- .reply(200, {
738
- transactionId: 'abc123',
739
- results: [{id: 'abc123', document: doc, operation: 'create'}],
740
- })
741
-
742
- getClient()
743
- .create(doc, {visibility: 'async'})
744
- .then((res) => {
745
- t.equal(res._id, 'abc123', 'document id returned')
746
- })
747
- .catch(t.ifError)
748
- .then(t.end)
749
- })
750
-
751
- test('can tell create() to auto-generate array keys', (t) => {
752
- const doc = {
753
- _id: 'abc123',
754
- name: 'Dromaeosauridae',
755
- genus: [{_type: 'dino', name: 'Velociraptor'}],
756
- }
757
- nock(projectHost())
758
- .post(
759
- '/v1/data/mutate/foo?returnIds=true&returnDocuments=true&autoGenerateArrayKeys=true&visibility=sync',
760
- {
761
- mutations: [{create: doc}],
762
- }
763
- )
764
- .reply(200, {
765
- transactionId: 'abc123',
766
- results: [
767
- {
768
- id: 'abc123',
769
- document: {...doc, genus: [{...doc.genus[0], _key: 'r4p70r'}]},
770
- operation: 'create',
771
- },
772
- ],
773
- })
774
-
775
- getClient()
776
- .create(doc, {autoGenerateArrayKeys: true})
777
- .then((res) => {
778
- t.equal(res._id, 'abc123', 'document id returned')
779
- t.equal(res.genus[0]._key, 'r4p70r', 'array keys generated returned')
780
- })
781
- .catch(t.ifError)
782
- .then(t.end)
783
- })
784
-
785
- test('can tell create() to do a dry-run', (t) => {
786
- const doc = {_id: 'abc123', name: 'Dromaeosauridae'}
787
- nock(projectHost())
788
- .post('/v1/data/mutate/foo?dryRun=true&returnIds=true&returnDocuments=true&visibility=sync', {
789
- mutations: [{create: doc}],
790
- })
791
- .reply(200, {
792
- transactionId: 'abc123',
793
- results: [
794
- {
795
- id: 'abc123',
796
- document: doc,
797
- operation: 'create',
798
- },
799
- ],
800
- })
801
-
802
- getClient()
803
- .create(doc, {dryRun: true})
804
- .then((res) => t.equal(res._id, 'abc123', 'document id returned'))
805
- .catch(t.ifError)
806
- .then(t.end)
807
- })
808
-
809
- test('createIfNotExists() sends correct mutation', (t) => {
810
- const doc = {_id: 'abc123', name: 'Raptor'}
811
- const expectedBody = {mutations: [{createIfNotExists: doc}]}
812
- nock(projectHost())
813
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
814
- .reply(200, {
815
- transactionId: '123abc',
816
- results: [{id: 'abc123', document: doc, operation: 'create'}],
817
- })
818
-
819
- getClient()
820
- .createIfNotExists(doc)
821
- .catch(t.ifError)
822
- .then(() => t.end())
823
- })
824
-
825
- test('can tell createIfNotExists() not to return documents', (t) => {
826
- const doc = {_id: 'abc123', name: 'Raptor'}
827
- const expectedBody = {mutations: [{createIfNotExists: doc}]}
828
- nock(projectHost())
829
- .post('/v1/data/mutate/foo?returnIds=true&visibility=sync', expectedBody)
830
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'create'}]})
831
-
832
- getClient()
833
- .createIfNotExists(doc, {returnDocuments: false})
834
- .then((res) => {
835
- t.equal(res.transactionId, 'abc123', 'returns transaction ID')
836
- t.equal(res.documentId, 'abc123', 'returns document id')
837
- })
838
- .catch(t.ifError)
839
- .then(t.end)
840
- })
841
-
842
- test('can use request tag with createIfNotExists()', (t) => {
843
- const doc = {_id: 'abc123', name: 'Raptor'}
844
- const expectedBody = {mutations: [{createIfNotExists: doc}]}
845
- nock(projectHost())
846
- .post('/v1/data/mutate/foo?tag=mysync&returnIds=true&visibility=sync', expectedBody)
847
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'create'}]})
848
-
849
- getClient()
850
- .createIfNotExists(doc, {returnDocuments: false, tag: 'mysync'})
851
- .then((res) => {
852
- t.equal(res.transactionId, 'abc123', 'returns transaction ID')
853
- t.equal(res.documentId, 'abc123', 'returns document id')
854
- })
855
- .catch(t.ifError)
856
- .then(t.end)
857
- })
858
-
859
- test('createOrReplace() sends correct mutation', (t) => {
860
- const doc = {_id: 'abc123', name: 'Raptor'}
861
- const expectedBody = {mutations: [{createOrReplace: doc}]}
862
- nock(projectHost())
863
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
864
- .reply(200, {transactionId: '123abc', results: [{id: 'abc123', operation: 'create'}]})
865
-
866
- getClient().createOrReplace(doc).catch(t.ifError).then(t.end)
867
- })
868
-
869
- test('can tell createOrReplace() not to return documents', (t) => {
870
- const doc = {_id: 'abc123', name: 'Raptor'}
871
- const expectedBody = {mutations: [{createOrReplace: doc}]}
872
- nock(projectHost())
873
- .post('/v1/data/mutate/foo?returnIds=true&visibility=sync', expectedBody)
874
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'create'}]})
875
-
876
- getClient()
877
- .createOrReplace(doc, {returnDocuments: false})
878
- .then((res) => {
879
- t.equal(res.transactionId, 'abc123', 'returns transaction ID')
880
- t.equal(res.documentId, 'abc123', 'returns document id')
881
- })
882
- .catch(t.ifError)
883
- .then(t.end)
884
- })
885
-
886
- test('delete() sends correct mutation', (t) => {
887
- const expectedBody = {mutations: [{delete: {id: 'abc123'}}]}
888
- nock(projectHost())
889
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
890
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'delete'}]})
891
-
892
- getClient()
893
- .delete('abc123')
894
- .catch(t.ifError)
895
- .then(() => t.end())
896
- })
897
-
898
- test('delete() can use query', (t) => {
899
- const expectedBody = {mutations: [{delete: {query: 'foo.sometype'}}]}
900
- nock(projectHost())
901
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
902
- .reply(200, {transactionId: 'abc123'})
903
-
904
- getClient()
905
- .delete({query: 'foo.sometype'})
906
- .catch(t.ifError)
907
- .then(() => t.end())
908
- })
909
-
910
- test('delete() can use request tag', (t) => {
911
- const expectedBody = {mutations: [{delete: {id: 'abc123'}}]}
912
- nock(projectHost())
913
- .post(
914
- '/v1/data/mutate/foo?tag=delete.abc&returnIds=true&returnDocuments=true&visibility=sync',
915
- expectedBody
916
- )
917
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'delete'}]})
918
-
919
- getClient()
920
- .delete('abc123', {tag: 'delete.abc'})
921
- .catch(t.ifError)
922
- .then(() => t.end())
923
- })
924
-
925
- test('delete() can use query with params', (t) => {
926
- const query = '*[_type == "beer" && title == $beerName]'
927
- const params = {beerName: 'Headroom Double IPA'}
928
- const expectedBody = {mutations: [{delete: {query: query, params: params}}]}
929
- nock(projectHost())
930
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
931
- .reply(200, {transactionId: 'abc123'})
932
-
933
- getClient()
934
- .delete({query: query, params: params})
935
- .catch(t.ifError)
936
- .then(() => t.end())
937
- })
938
-
939
- test('delete() can be told not to return documents', (t) => {
940
- const expectedBody = {mutations: [{delete: {id: 'abc123'}}]}
941
- nock(projectHost())
942
- .post('/v1/data/mutate/foo?returnIds=true&visibility=sync', expectedBody)
943
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'delete'}]})
944
-
945
- getClient()
946
- .delete('abc123', {returnDocuments: false})
947
- .catch(t.ifError)
948
- .then(() => t.end())
949
- })
950
-
951
- test('mutate() accepts multiple mutations', (t) => {
952
- const docs = [
953
- {
954
- _id: 'movies.raiders-of-the-lost-ark',
955
- title: 'Raiders of the Lost Ark',
956
- year: 1981,
957
- },
958
- {
959
- _id: 'movies.the-phantom-menace',
960
- title: 'Star Wars: Episode I - The Phantom Menace',
961
- year: 1999,
962
- },
963
- ]
964
-
965
- const mutations = [{create: docs[0]}, {delete: {id: 'movies.the-phantom-menace'}}]
966
-
967
- nock(projectHost())
968
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', {mutations})
969
- .reply(200, {
970
- transactionId: 'foo',
971
- results: [
972
- {id: 'movies.raiders-of-the-lost-ark', operation: 'create', document: docs[0]},
973
- {id: 'movies.the-phantom-menace', operation: 'delete', document: docs[1]},
974
- ],
975
- })
976
-
977
- getClient()
978
- .mutate(mutations)
979
- .catch(t.ifError)
980
- .then(() => t.end())
981
- })
982
-
983
- test('mutate() accepts request tag', (t) => {
984
- const mutations = [{delete: {id: 'abc123'}}]
985
-
986
- nock(projectHost())
987
- .post('/v1/data/mutate/foo?tag=foobar&returnIds=true&returnDocuments=true&visibility=sync', {
988
- mutations,
989
- })
990
- .reply(200, {
991
- transactionId: 'foo',
992
- results: [{id: 'abc123', operation: 'delete', document: {_id: 'abc123'}}],
993
- })
994
-
995
- getClient()
996
- .mutate(mutations, {tag: 'foobar'})
997
- .catch(t.ifError)
998
- .then(() => t.end())
999
- })
1000
-
1001
- test('mutate() accepts `autoGenerateArrayKeys`', (t) => {
1002
- const mutations = [
1003
- {
1004
- create: {
1005
- _id: 'abc123',
1006
- _type: 'post',
1007
- items: [{_type: 'block', children: [{_type: 'span', text: 'Hello there'}]}],
1008
- },
1009
- },
1010
- ]
1011
-
1012
- nock(projectHost())
1013
- .post(
1014
- '/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync&autoGenerateArrayKeys=true',
1015
- {mutations}
1016
- )
1017
- .reply(200, {
1018
- transactionId: 'foo',
1019
- results: [{id: 'abc123', operation: 'create', document: {_id: 'abc123'}}],
1020
- })
1021
-
1022
- getClient()
1023
- .mutate(mutations, {autoGenerateArrayKeys: true})
1024
- .catch(t.ifError)
1025
- .then(() => t.end())
1026
- })
1027
-
1028
- test('mutate() accepts `dryRun`', (t) => {
1029
- const mutations = [{create: {_id: 'abc123', _type: 'post'}}]
1030
-
1031
- nock(projectHost())
1032
- .post('/v1/data/mutate/foo?dryRun=true&returnIds=true&returnDocuments=true&visibility=sync', {
1033
- mutations,
1034
- })
1035
- .reply(200, {
1036
- transactionId: 'foo',
1037
- results: [{id: 'abc123', operation: 'create', document: {_id: 'abc123'}}],
1038
- })
1039
-
1040
- getClient()
1041
- .mutate(mutations, {dryRun: true})
1042
- .catch(t.ifError)
1043
- .then(() => t.end())
1044
- })
1045
-
1046
- test('mutate() accepts `skipCrossDatasetReferenceValidation`', (t) => {
1047
- const mutations = [{delete: {id: 'abc123'}}]
1048
-
1049
- nock(projectHost())
1050
- .post(
1051
- '/v1/data/mutate/foo?tag=foobar&returnIds=true&returnDocuments=true&visibility=sync&skipCrossDatasetReferenceValidation=true',
1052
- {mutations}
1053
- )
1054
- .reply(200, {
1055
- transactionId: 'foo',
1056
- results: [{id: 'abc123', operation: 'delete', document: {_id: 'abc123'}}],
1057
- })
1058
-
1059
- getClient()
1060
- .mutate(mutations, {tag: 'foobar', skipCrossDatasetReferenceValidation: true})
1061
- .catch(t.ifError)
1062
- .then(() => t.end())
1063
- })
1064
-
1065
- test('mutate() skips/falls back to defaults on undefined but known properties', (t) => {
1066
- const mutations = [{delete: {id: 'abc123'}}]
1067
-
1068
- nock(projectHost())
1069
- .post('/v1/data/mutate/foo?tag=foobar&returnIds=true&returnDocuments=true&visibility=sync', {
1070
- mutations,
1071
- })
1072
- .reply(200, {
1073
- transactionId: 'foo',
1074
- results: [{id: 'abc123', operation: 'delete', document: {_id: 'abc123'}}],
1075
- })
1076
-
1077
- getClient()
1078
- .mutate(mutations, {
1079
- tag: 'foobar',
1080
- skipCrossDatasetReferenceValidation: undefined,
1081
- returnDocuments: undefined,
1082
- autoGenerateArrayKeys: undefined,
1083
- })
1084
- .catch(t.ifError)
1085
- .then(() => t.end())
1086
- })
1087
-
1088
- test('uses GET for queries below limit', (t) => {
1089
- // Please dont ever do this. Just... don't.
1090
- const clause = []
1091
- const qParams = {}
1092
- const params = {}
1093
- for (let i = 1950; i <= 2016; i++) {
1094
- clause.push(`title == $beerName${i}`)
1095
- params[`beerName${i}`] = `some beer ${i}`
1096
- qParams[`$beerName${i}`] = JSON.stringify(`some beer ${i}`)
1097
- }
1098
-
1099
- // Again, just... don't do this.
1100
- const query = `*[_type == "beer" && (${clause.join(' || ')})]`
1101
-
1102
- nock(projectHost())
1103
- .get('/v1/data/query/foo')
1104
- .query(Object.assign({query}, qParams))
1105
- .reply(200, {
1106
- ms: 123,
1107
- q: query,
1108
- result: [{_id: 'njgNkngskjg', rating: 5}],
1109
- })
1110
-
1111
- getClient()
1112
- .fetch(query, params)
1113
- .then((res) => {
1114
- t.equal(res.length, 1, 'length should match')
1115
- t.equal(res[0].rating, 5, 'data should match')
1116
- })
1117
- .catch(t.ifError)
1118
- .then(t.end)
1119
- })
1120
-
1121
- test('uses POST for long queries', (t) => {
1122
- // Please dont ever do this. Just... don't.
1123
- const clause = []
1124
- const params = {}
1125
- for (let i = 1866; i <= 2016; i++) {
1126
- clause.push(`title == $beerName${i}`)
1127
- params[`beerName${i}`] = `some beer ${i}`
1128
- }
1129
-
1130
- // Again, just... don't do this.
1131
- const query = `*[_type == "beer" && (${clause.join(' || ')})]`
1132
-
1133
- nock(projectHost())
1134
- .filteringRequestBody(/.*/, '*')
1135
- .post('/v1/data/query/foo', '*')
1136
- .reply(200, {
1137
- ms: 123,
1138
- q: query,
1139
- result: [{_id: 'njgNkngskjg', rating: 5}],
1140
- })
1141
-
1142
- getClient()
1143
- .fetch(query, params)
1144
- .then((res) => {
1145
- t.equal(res.length, 1, 'length should match')
1146
- t.equal(res[0].rating, 5, 'data should match')
1147
- })
1148
- .catch(t.ifError)
1149
- .then(t.end)
1150
- })
1151
-
1152
- test('uses POST for long queries, but puts request tag as query param', (t) => {
1153
- const clause = []
1154
- const params = {}
1155
- for (let i = 1866; i <= 2016; i++) {
1156
- clause.push(`title == $beerName${i}`)
1157
- params[`beerName${i}`] = `some beer ${i}`
1158
- }
1159
-
1160
- // Again, just... don't do this.
1161
- const query = `*[_type == "beer" && (${clause.join(' || ')})]`
1162
-
1163
- nock(projectHost())
1164
- .filteringRequestBody(/.*/, '*')
1165
- .post('/v1/data/query/foo?tag=myapp.silly-query', '*')
1166
- .reply(200, {
1167
- ms: 123,
1168
- q: query,
1169
- result: [{_id: 'njgNkngskjg', rating: 5}],
1170
- })
1171
-
1172
- getClient()
1173
- .fetch(query, params, {tag: 'myapp.silly-query'})
1174
- .then((res) => {
1175
- t.equal(res.length, 1, 'length should match')
1176
- t.equal(res[0].rating, 5, 'data should match')
1177
- })
1178
- .catch(t.ifError)
1179
- .then(t.end)
1180
- })
1181
-
1182
- test('uses POST for long queries also towards CDN', (t) => {
1183
- const client = sanityClient({projectId: 'abc123', dataset: 'foo', useCdn: true})
1184
-
1185
- const clause = []
1186
- const params = {}
1187
- for (let i = 1866; i <= 2016; i++) {
1188
- clause.push(`title == $beerName${i}`)
1189
- params[`beerName${i}`] = `some beer ${i}`
1190
- }
1191
-
1192
- const query = `*[_type == "beer" && (${clause.join(' || ')})]`
1193
-
1194
- nock('https://abc123.apicdn.sanity.io')
1195
- .filteringRequestBody(/.*/, '*')
1196
- .post('/v1/data/query/foo', '*')
1197
- .reply(200, {
1198
- ms: 123,
1199
- q: query,
1200
- result: [{_id: 'njgNkngskjg', rating: 5}],
1201
- })
1202
-
1203
- client
1204
- .fetch(query, params)
1205
- .then((res) => {
1206
- t.equal(res.length, 1, 'length should match')
1207
- t.equal(res[0].rating, 5, 'data should match')
1208
- })
1209
- .catch(t.ifError)
1210
- .then(t.end)
1211
- })
1212
-
1213
- /*****************
1214
- * PATCH OPS *
1215
- *****************/
1216
- test('can build and serialize a patch of operations', (t) => {
1217
- const patch = getClient().patch('abc123').inc({count: 1}).set({brownEyes: true}).serialize()
1218
-
1219
- t.deepEqual(patch, {id: 'abc123', inc: {count: 1}, set: {brownEyes: true}})
1220
- t.end()
1221
- })
1222
-
1223
- test('patch() can take an array of IDs', (t) => {
1224
- const patch = getClient().patch(['abc123', 'foo.456']).inc({count: 1}).serialize()
1225
- t.deepEqual(patch, {id: ['abc123', 'foo.456'], inc: {count: 1}})
1226
- t.end()
1227
- })
1228
-
1229
- test('patch() can take a query', (t) => {
1230
- const patch = getClient().patch({query: '*[_type == "beer]'}).inc({count: 1}).serialize()
1231
- t.deepEqual(patch, {query: '*[_type == "beer]', inc: {count: 1}})
1232
- t.end()
1233
- })
1234
-
1235
- test('patch() can take a query and params', (t) => {
1236
- const patch = getClient()
1237
- .patch({query: '*[_type == $type]', params: {type: 'beer'}})
1238
- .inc({count: 1})
1239
- .serialize()
1240
-
1241
- t.deepEqual(patch, {query: '*[_type == $type]', params: {type: 'beer'}, inc: {count: 1}})
1242
- t.end()
1243
- })
1244
-
1245
- test('setIfMissing() patch can be applied multiple times', (t) => {
1246
- const patch = getClient()
1247
- .patch('abc123')
1248
- .setIfMissing({count: 1, foo: 'bar'})
1249
- .setIfMissing({count: 2, bar: 'foo'})
1250
- .serialize()
1251
-
1252
- t.deepEqual(patch, {id: 'abc123', setIfMissing: {count: 2, foo: 'bar', bar: 'foo'}})
1253
- t.end()
1254
- })
1255
-
1256
- test('only last replace() patch call gets applied', (t) => {
1257
- const patch = getClient()
1258
- .patch('abc123')
1259
- .replace({count: 1, foo: 'bar'})
1260
- .replace({count: 2, bar: 'foo'})
1261
- .serialize()
1262
-
1263
- t.deepEqual(patch, {id: 'abc123', set: {$: {count: 2, bar: 'foo'}}})
1264
- t.end()
1265
- })
1266
-
1267
- test('can apply inc() and dec()', (t) => {
1268
- const patch = getClient()
1269
- .patch('abc123')
1270
- .inc({count: 1}) // One step forward
1271
- .dec({count: 2}) // Two steps back
1272
- .serialize()
1273
-
1274
- t.deepEqual(patch, {id: 'abc123', inc: {count: 1}, dec: {count: 2}})
1275
- t.end()
1276
- })
1277
-
1278
- test('can apply unset()', (t) => {
1279
- const patch = getClient()
1280
- .patch('abc123')
1281
- .inc({count: 1})
1282
- .unset(['bitter', 'enchilada'])
1283
- .serialize()
1284
-
1285
- t.deepEqual(patch, {id: 'abc123', inc: {count: 1}, unset: ['bitter', 'enchilada']})
1286
- t.end()
1287
- })
1288
-
1289
- test('throws if non-array is passed to unset()', (t) => {
1290
- t.throws(() => getClient().patch('abc123').unset('bitter').serialize(), /non-array given/)
1291
- t.end()
1292
- })
1293
-
1294
- test('can apply insert()', (t) => {
1295
- const patch = getClient()
1296
- .patch('abc123')
1297
- .inc({count: 1})
1298
- .insert('after', 'tags[-1]', ['hotsauce'])
1299
- .serialize()
1300
-
1301
- t.deepEqual(patch, {
1302
- id: 'abc123',
1303
- inc: {count: 1},
1304
- insert: {after: 'tags[-1]', items: ['hotsauce']},
1305
- })
1306
- t.end()
1307
- })
1308
-
1309
- test('throws on invalid insert()', (t) => {
1310
- t.throws(
1311
- () => getClient().patch('abc123').insert('bitter', 'sel', ['raf']),
1312
- /one of: "before", "after", "replace"/
1313
- )
1314
-
1315
- t.throws(() => getClient().patch('abc123').insert('before', 123, ['raf']), /must be a string/)
1316
-
1317
- t.throws(() => getClient().patch('abc123').insert('before', 'prop', 'blah'), /must be an array/)
1318
- t.end()
1319
- })
1320
-
1321
- test('can apply append()', (t) => {
1322
- const patch = getClient().patch('abc123').inc({count: 1}).append('tags', ['sriracha']).serialize()
1323
-
1324
- t.deepEqual(patch, {
1325
- id: 'abc123',
1326
- inc: {count: 1},
1327
- insert: {after: 'tags[-1]', items: ['sriracha']},
1328
- })
1329
- t.end()
1330
- })
1331
-
1332
- test('can apply prepend()', (t) => {
1333
- const patch = getClient()
1334
- .patch('abc123')
1335
- .inc({count: 1})
1336
- .prepend('tags', ['sriracha', 'hotsauce'])
1337
- .serialize()
1338
-
1339
- t.deepEqual(patch, {
1340
- id: 'abc123',
1341
- inc: {count: 1},
1342
- insert: {before: 'tags[0]', items: ['sriracha', 'hotsauce']},
1343
- })
1344
- t.end()
1345
- })
1346
-
1347
- test('can apply splice()', (t) => {
1348
- const patch = () => getClient().patch('abc123')
1349
- const replaceFirst = patch().splice('tags', 0, 1, ['foo']).serialize()
1350
- const insertInMiddle = patch().splice('tags', 5, 0, ['foo']).serialize()
1351
- const deleteLast = patch().splice('tags', -1, 1).serialize()
1352
- const deleteAllFromIndex = patch().splice('tags', 3, -1).serialize()
1353
- const allFromIndexDefault = patch().splice('tags', 3).serialize()
1354
- const negativeDelete = patch().splice('tags', -2, -2, ['foo']).serialize()
1355
-
1356
- t.deepEqual(replaceFirst.insert, {replace: 'tags[0:1]', items: ['foo']})
1357
- t.deepEqual(insertInMiddle.insert, {replace: 'tags[5:5]', items: ['foo']})
1358
- t.deepEqual(deleteLast.insert, {replace: 'tags[-2:]', items: []})
1359
- t.deepEqual(deleteAllFromIndex.insert, {replace: 'tags[3:-1]', items: []})
1360
- t.deepEqual(allFromIndexDefault.insert, {replace: 'tags[3:-1]', items: []})
1361
- t.deepEqual(negativeDelete, patch().splice('tags', -2, 0, ['foo']).serialize())
1362
- t.end()
1363
- })
1364
-
1365
- test('serializing invalid selectors throws', (t) => {
1366
- t.throws(() => getClient().patch(123).serialize(), /unknown selection/i)
1367
- t.end()
1368
- })
1369
-
1370
- test('can apply diffMatchPatch()', (t) => {
1371
- const patch = getClient()
1372
- .patch('abc123')
1373
- .inc({count: 1})
1374
- .diffMatchPatch({description: '@@ -1,13 +1,12 @@\n The \n-rabid\n+nice\n dog\n'})
1375
- .serialize()
1376
-
1377
- t.deepEqual(patch, {
1378
- id: 'abc123',
1379
- inc: {count: 1},
1380
- diffMatchPatch: {description: '@@ -1,13 +1,12 @@\n The \n-rabid\n+nice\n dog\n'},
1381
- })
1382
- t.end()
1383
- })
1384
-
1385
- test('all patch methods throw on non-objects being passed as argument', (t) => {
1386
- const patch = getClient().patch('abc123')
1387
- t.throws(() => patch.set(null), /set\(\) takes an object of properties/, 'set throws')
1388
- t.throws(
1389
- () => patch.setIfMissing('foo'),
1390
- /setIfMissing\(\) takes an object of properties/,
1391
- 'setIfMissing throws'
1392
- )
1393
- t.throws(
1394
- () => patch.replace('foo'),
1395
- /replace\(\) takes an object of properties/,
1396
- 'replace throws'
1397
- )
1398
- t.throws(() => patch.inc('foo'), /inc\(\) takes an object of properties/, 'inc throws')
1399
- t.throws(() => patch.dec('foo'), /dec\(\) takes an object of properties/, 'dec throws')
1400
- t.throws(
1401
- () => patch.diffMatchPatch('foo'),
1402
- /diffMatchPatch\(\) takes an object of properties/,
1403
- 'diffMatchPatch throws'
1404
- )
1405
- t.end()
1406
- })
1407
-
1408
- test('executes patch when commit() is called', (t) => {
1409
- const expectedPatch = {patch: {id: 'abc123', inc: {count: 1}, set: {visited: true}}}
1410
- nock(projectHost())
1411
- .post('/v1/data/mutate/foo?returnIds=true&visibility=sync', {mutations: [expectedPatch]})
1412
- .reply(200, {transactionId: 'blatti'})
1413
-
1414
- getClient()
1415
- .patch('abc123')
1416
- .inc({count: 1})
1417
- .set({visited: true})
1418
- .commit({returnDocuments: false})
1419
- .then((res) => {
1420
- t.equal(res.transactionId, 'blatti', 'applies given patch')
1421
- })
1422
- .catch(t.ifError)
1423
- .then(t.end)
1424
- })
1425
-
1426
- test('executes patch with request tag when commit() is called with tag', (t) => {
1427
- const expectedPatch = {patch: {id: 'abc123', set: {visited: true}}}
1428
- nock(projectHost())
1429
- .post('/v1/data/mutate/foo?tag=company.setvisited&returnIds=true&visibility=sync', {
1430
- mutations: [expectedPatch],
1431
- })
1432
- .reply(200, {transactionId: 'blatti'})
1433
-
1434
- getClient()
1435
- .patch('abc123')
1436
- .set({visited: true})
1437
- .commit({returnDocuments: false, tag: 'company.setvisited'})
1438
- .then((res) => {
1439
- t.equal(res.transactionId, 'blatti', 'applies given patch')
1440
- })
1441
- .catch(t.ifError)
1442
- .then(t.end)
1443
- })
1444
-
1445
- test('executes patch with auto generate key option if specified commit()', (t) => {
1446
- const expectedPatch = {patch: {id: 'abc123', set: {visited: true}}}
1447
- nock(projectHost())
1448
- .post('/v1/data/mutate/foo?returnIds=true&autoGenerateArrayKeys=true&visibility=sync', {
1449
- mutations: [expectedPatch],
1450
- })
1451
- .reply(200, {transactionId: 'blatti'})
1452
-
1453
- getClient()
1454
- .patch('abc123')
1455
- .set({visited: true})
1456
- .commit({returnDocuments: false, autoGenerateArrayKeys: true})
1457
- .then((res) => {
1458
- t.equal(res.transactionId, 'blatti', 'applies given patch')
1459
- })
1460
- .catch(t.ifError)
1461
- .then(t.end)
1462
- })
1463
-
1464
- test('executes patch with given token override commit() is called', (t) => {
1465
- const expectedPatch = {patch: {id: 'abc123', inc: {count: 1}, set: {visited: true}}}
1466
- nock(projectHost(), {reqheaders: {Authorization: 'Bearer abc123'}})
1467
- .post('/v1/data/mutate/foo?returnIds=true&visibility=sync', {mutations: [expectedPatch]})
1468
- .reply(200, {transactionId: 'blatti'})
1469
-
1470
- getClient()
1471
- .patch('abc123')
1472
- .inc({count: 1})
1473
- .set({visited: true})
1474
- .commit({returnDocuments: false, token: 'abc123'})
1475
- .then((res) => {
1476
- t.equal(res.transactionId, 'blatti', 'applies given patch')
1477
- })
1478
- .catch(t.ifError)
1479
- .then(t.end)
1480
- })
1481
-
1482
- test('returns patched document by default', (t) => {
1483
- const expectedPatch = {patch: {id: 'abc123', inc: {count: 1}, set: {visited: true}}}
1484
- const expectedBody = {mutations: [expectedPatch]}
1485
- nock(projectHost())
1486
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
1487
- .reply(200, {
1488
- transactionId: 'blatti',
1489
- results: [
1490
- {
1491
- id: 'abc123',
1492
- operation: 'update',
1493
- document: {
1494
- _id: 'abc123',
1495
- _createdAt: '2016-10-24T08:09:32.997Z',
1496
- count: 2,
1497
- visited: true,
1498
- },
1499
- },
1500
- ],
1501
- })
1502
-
1503
- getClient()
1504
- .patch('abc123')
1505
- .inc({count: 1})
1506
- .set({visited: true})
1507
- .commit()
1508
- .then((res) => {
1509
- t.equal(res._id, 'abc123', 'returns patched document')
1510
- })
1511
- .catch(t.ifError)
1512
- .then(t.end)
1513
- })
1514
-
1515
- test('commit() returns promise', (t) => {
1516
- const expectedPatch = {patch: {id: 'abc123', inc: {count: 1}, set: {visited: true}}}
1517
- const expectedBody = {mutations: [expectedPatch]}
1518
- nock(projectHost())
1519
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
1520
- .reply(400)
1521
-
1522
- getClient()
1523
- .patch('abc123')
1524
- .inc({count: 1})
1525
- .set({visited: true})
1526
- .commit()
1527
- .catch((err) => {
1528
- t.ok(err instanceof Error, 'should call applied error handler')
1529
- t.end()
1530
- })
1531
- })
1532
-
1533
- test('each patch operation returns same patch', (t) => {
1534
- const patch = getClient().patch('abc123')
1535
- const inc = patch.inc({count: 1})
1536
- const dec = patch.dec({count: 1})
1537
- const combined = inc.dec({count: 1})
1538
-
1539
- t.equal(patch, inc, 'should return same patch')
1540
- t.equal(inc, dec, 'should return same patch')
1541
- t.equal(inc, combined, 'should return same patch')
1542
-
1543
- t.deepEqual(
1544
- combined.serialize(),
1545
- {id: 'abc123', inc: {count: 1}, dec: {count: 1}},
1546
- 'combined patch should have both inc and dec ops'
1547
- )
1548
-
1549
- t.end()
1550
- })
1551
-
1552
- test('can reset patches to no operations, keeping document ID', (t) => {
1553
- const patch = getClient().patch('abc123').inc({count: 1}).dec({visits: 1})
1554
- const reset = patch.reset()
1555
-
1556
- t.deepEqual(patch.serialize(), {id: 'abc123'}, 'correct patch')
1557
- t.deepEqual(reset.serialize(), {id: 'abc123'}, 'reset patch should be empty')
1558
- t.equal(patch, reset, 'reset mutates, does not clone')
1559
- t.end()
1560
- })
1561
-
1562
- test('patch has toJSON() which serializes patch', (t) => {
1563
- const patch = getClient().patch('abc123').inc({count: 1})
1564
- t.deepEqual(
1565
- JSON.parse(JSON.stringify(patch)),
1566
- JSON.parse(JSON.stringify({id: 'abc123', inc: {count: 1}}))
1567
- )
1568
- t.end()
1569
- })
1570
-
1571
- test('Patch is available on client and can be used without instantiated client', (t) => {
1572
- const patch = new sanityClient.Patch('foo.bar')
1573
- t.deepEqual(
1574
- patch.inc({foo: 1}).dec({bar: 2}).serialize(),
1575
- {id: 'foo.bar', inc: {foo: 1}, dec: {bar: 2}},
1576
- 'patch should work without context'
1577
- )
1578
- t.end()
1579
- })
1580
-
1581
- test('patch commit() throws if called without a client', (t) => {
1582
- const patch = new sanityClient.Patch('foo.bar')
1583
- t.throws(() => patch.dec({bar: 2}).commit(), /client.*mutate/i)
1584
- t.end()
1585
- })
1586
-
1587
- test('can manually call clone on patch', (t) => {
1588
- const patch1 = getClient().patch('abc123').inc({count: 1})
1589
- const patch2 = patch1.clone()
1590
-
1591
- t.notEqual(patch1, patch2, 'actually cloned')
1592
- t.deepEqual(patch1.serialize(), patch2.serialize(), 'serialized to the same')
1593
- t.end()
1594
- })
1595
-
1596
- test('can apply ifRevisionId constraint', (t) => {
1597
- t.deepEqual(
1598
- getClient().patch('abc123').inc({count: 1}).ifRevisionId('someRev').serialize(),
1599
- {id: 'abc123', inc: {count: 1}, ifRevisionID: 'someRev'},
1600
- 'patch should be able to apply ifRevisionId constraint'
1601
- )
1602
- t.end()
1603
- })
1604
-
1605
- /*****************
1606
- * TRANSACTIONS *
1607
- *****************/
1608
- test('can build and serialize a transaction of operations', (t) => {
1609
- const trans = getClient()
1610
- .transaction()
1611
- .create({_id: 'moofoo', name: 'foobar'})
1612
- .delete('nznjkAJnjgnk')
1613
- .serialize()
1614
-
1615
- t.deepEqual(trans, [{create: {_id: 'moofoo', name: 'foobar'}}, {delete: {id: 'nznjkAJnjgnk'}}])
1616
- t.end()
1617
- })
1618
-
1619
- test('each transaction operation mutates transaction', (t) => {
1620
- const trans = getClient().transaction()
1621
- const create = trans.create({count: 1})
1622
- const combined = create.delete('foobar')
1623
-
1624
- t.equal(trans, create, 'should be mutated')
1625
- t.equal(create, combined, 'should be mutated')
1626
-
1627
- t.deepEqual(
1628
- combined.serialize(),
1629
- [{create: {count: 1}}, {delete: {id: 'foobar'}}],
1630
- 'combined transaction should have both create and delete ops'
1631
- )
1632
-
1633
- t.end()
1634
- })
1635
-
1636
- test('transaction methods are chainable', (t) => {
1637
- const trans = getClient()
1638
- .transaction()
1639
- .create({moo: 'tools'})
1640
- .createIfNotExists({_id: 'someId', j: 'query'})
1641
- .createOrReplace({_id: 'someOtherId', do: 'jo'})
1642
- .delete('prototype')
1643
- .patch('foobar', {inc: {sales: 1}})
1644
-
1645
- t.deepEqual(trans.serialize(), [
1646
- {
1647
- create: {
1648
- moo: 'tools',
1649
- },
1650
- },
1651
- {
1652
- createIfNotExists: {
1653
- _id: 'someId',
1654
- j: 'query',
1655
- },
1656
- },
1657
- {
1658
- createOrReplace: {
1659
- _id: 'someOtherId',
1660
- do: 'jo',
1661
- },
1662
- },
1663
- {
1664
- delete: {
1665
- id: 'prototype',
1666
- },
1667
- },
1668
- {
1669
- patch: {
1670
- id: 'foobar',
1671
- inc: {sales: 1},
1672
- },
1673
- },
1674
- ])
1675
-
1676
- t.equal(trans.reset().serialize().length, 0, 'resets to 0 operations')
1677
- t.end()
1678
- })
1679
-
1680
- test('patches can be built with callback', (t) => {
1681
- const trans = getClient()
1682
- .transaction()
1683
- .patch('moofoo', (p) => p.inc({sales: 1}).dec({stock: 1}))
1684
- .serialize()
1685
-
1686
- t.deepEqual(trans, [
1687
- {
1688
- patch: {
1689
- id: 'moofoo',
1690
- inc: {sales: 1},
1691
- dec: {stock: 1},
1692
- },
1693
- },
1694
- ])
1695
- t.end()
1696
- })
1697
-
1698
- test('throws if patch builder does not return patch', (t) => {
1699
- t.throws(() => getClient().transaction().patch('moofoo', noop), /must return the patch/)
1700
- t.end()
1701
- })
1702
-
1703
- test('patch can take an existing patch', (t) => {
1704
- const client = getClient()
1705
- const incPatch = client.patch('bar').inc({sales: 1})
1706
- const trans = getClient().transaction().patch(incPatch).serialize()
1707
-
1708
- t.deepEqual(trans, [
1709
- {
1710
- patch: {
1711
- id: 'bar',
1712
- inc: {sales: 1},
1713
- },
1714
- },
1715
- ])
1716
- t.end()
1717
- })
1718
-
1719
- test('executes transaction when commit() is called', (t) => {
1720
- const mutations = [{create: {bar: true}}, {delete: {id: 'barfoo'}}]
1721
- nock(projectHost())
1722
- .post('/v1/data/mutate/foo?returnIds=true&visibility=sync', {mutations})
1723
- .reply(200, {transactionId: 'blatti'})
1724
-
1725
- getClient()
1726
- .transaction()
1727
- .create({bar: true})
1728
- .delete('barfoo')
1729
- .commit()
1730
- .then((res) => {
1731
- t.equal(res.transactionId, 'blatti', 'applies given transaction')
1732
- })
1733
- .catch(t.ifError)
1734
- .then(t.end)
1735
- })
1736
-
1737
- test('executes transaction with request tag when commit() is called with tag', (t) => {
1738
- const mutations = [{create: {_type: 'bar', name: 'Toronado'}}]
1739
- nock(projectHost())
1740
- .post('/v1/data/mutate/foo?tag=sfcraft.createbar&returnIds=true&visibility=sync', {mutations})
1741
- .reply(200, {transactionId: 'blatti'})
1742
-
1743
- getClient()
1744
- .transaction()
1745
- .create({_type: 'bar', name: 'Toronado'})
1746
- .commit({tag: 'sfcraft.createbar'})
1747
- .then((res) => {
1748
- t.equal(res.transactionId, 'blatti', 'applies given transaction')
1749
- })
1750
- .catch(t.ifError)
1751
- .then(t.end)
1752
- })
1753
-
1754
- test('throws when passing incorrect input to transaction operations', (t) => {
1755
- const trans = getClient().transaction()
1756
- t.throws(() => trans.create('foo'), /object of prop/, 'throws on create()')
1757
- t.throws(() => trans.createIfNotExists('foo'), /object of prop/, 'throws on createIfNotExists()')
1758
- t.throws(() => trans.createOrReplace('foo'), /object of prop/, 'throws on createOrReplace()')
1759
- t.throws(() => trans.delete({id: 'moofoo'}), /not a valid document ID/, 'throws on delete()')
1760
- t.end()
1761
- })
1762
-
1763
- test('throws when not including document ID in createOrReplace/createIfNotExists in transaction', (t) => {
1764
- const trans = getClient().transaction()
1765
- t.throws(
1766
- () => trans.createIfNotExists({_type: 'movie', a: 1}),
1767
- /contains an ID/,
1768
- 'throws on createIfNotExists()'
1769
- )
1770
- t.throws(
1771
- () => trans.createOrReplace({_type: 'movie', a: 1}),
1772
- /contains an ID/,
1773
- 'throws on createOrReplace()'
1774
- )
1775
- t.end()
1776
- })
1777
-
1778
- test('can manually call clone on transaction', (t) => {
1779
- const trans1 = getClient().transaction().delete('foo.bar')
1780
- const trans2 = trans1.clone()
1781
-
1782
- t.notEqual(trans1, trans2, 'actually cloned')
1783
- t.deepEqual(trans1.serialize(), trans2.serialize(), 'serialized to the same')
1784
- t.end()
1785
- })
1786
-
1787
- test('transaction has toJSON() which serializes patch', (t) => {
1788
- const trans = getClient().transaction().create({count: 1})
1789
- t.deepEqual(JSON.parse(JSON.stringify(trans)), JSON.parse(JSON.stringify([{create: {count: 1}}])))
1790
- t.end()
1791
- })
1792
-
1793
- test('Transaction is available on client and can be used without instantiated client', (t) => {
1794
- const trans = new sanityClient.Transaction()
1795
- t.deepEqual(
1796
- trans.delete('barfoo').serialize(),
1797
- [{delete: {id: 'barfoo'}}],
1798
- 'transaction should work without context'
1799
- )
1800
- t.end()
1801
- })
1802
-
1803
- test('transaction can be created without client and passed to mutate()', (t) => {
1804
- const trx = new sanityClient.Transaction()
1805
- trx.delete('foo')
1806
-
1807
- const mutations = [{delete: {id: 'foo'}}]
1808
- nock(projectHost())
1809
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', {mutations})
1810
- .reply(200, {results: [{id: 'foo', operation: 'delete'}]})
1811
-
1812
- getClient()
1813
- .mutate(trx)
1814
- .catch(t.ifError)
1815
- .then(() => t.end())
1816
- })
1817
-
1818
- test('transaction commit() throws if called without a client', (t) => {
1819
- const trans = new sanityClient.Transaction()
1820
- t.throws(() => trans.delete('foo.bar').commit(), /client.*mutate/i)
1821
- t.end()
1822
- })
1823
-
1824
- test('transaction can be given an explicit transaction ID', (t) => {
1825
- const transactionId = 'moop'
1826
- const mutations = [{create: {bar: true}}, {delete: {id: 'barfoo'}}]
1827
- nock(projectHost())
1828
- .post('/v1/data/mutate/foo?returnIds=true&visibility=sync', {mutations, transactionId})
1829
- .reply(200, {transactionId})
1830
-
1831
- getClient()
1832
- .transaction()
1833
- .create({bar: true})
1834
- .delete('barfoo')
1835
- .transactionId(transactionId)
1836
- .commit()
1837
- .then((res) => {
1838
- t.equal(res.transactionId, transactionId, 'applies given transaction')
1839
- })
1840
- .catch(t.ifError)
1841
- .then(t.end)
1842
- })
1843
-
1844
- /*****************
1845
- * LISTENERS *
1846
- *****************/
1847
- test('listeners connect to listen endpoint, emits events', (t) => {
1848
- const doc = {_id: 'mooblah', _type: 'foo.bar', prop: 'value'}
1849
- const response = [
1850
- ':',
1851
- '',
1852
- 'event: welcome',
1853
- 'data: {"listenerName":"LGFXwOqrf1GHawAjZRnhd6"}',
1854
- '',
1855
- 'event: mutation',
1856
- `data: ${JSON.stringify({document: doc})}`,
1857
- '',
1858
- 'event: disconnect',
1859
- 'data: {"reason":"forcefully closed"}',
1860
- ].join('\n')
1861
-
1862
- nock(projectHost())
1863
- .get('/v1/data/listen/foo?query=foo.bar&includeResult=true')
1864
- .reply(200, response, {
1865
- 'cache-control': 'no-cache',
1866
- 'content-type': 'text/event-stream; charset=utf-8',
1867
- 'transfer-encoding': 'chunked',
1868
- })
1869
-
1870
- const sub = getClient()
1871
- .listen('foo.bar')
1872
- .subscribe({
1873
- next: (evt) => {
1874
- sub.unsubscribe()
1875
- t.deepEqual(evt.document, doc)
1876
- t.end()
1877
- },
1878
- error: (err) => {
1879
- sub.unsubscribe()
1880
- t.ifError(err)
1881
- t.fail('Should not call error handler')
1882
- t.end()
1883
- },
1884
- })
1885
- })
1886
-
1887
- test('listeners connect to listen endpoint with request tag, emits events', (t) => {
1888
- const doc = {_id: 'mooblah', _type: 'foo.bar', prop: 'value'}
1889
- const response = [
1890
- ':',
1891
- '',
1892
- 'event: welcome',
1893
- 'data: {"listenerName":"LGFXwOqrf1GHawAjZRnhd6"}',
1894
- '',
1895
- 'event: mutation',
1896
- `data: ${JSON.stringify({document: doc})}`,
1897
- '',
1898
- 'event: disconnect',
1899
- 'data: {"reason":"forcefully closed"}',
1900
- ].join('\n')
1901
-
1902
- nock(projectHost())
1903
- .get(
1904
- '/v1/data/listen/foo?tag=sfcraft.checkins&query=*%5B_type%20%3D%3D%20%22checkin%22%5D&includeResult=true'
1905
- )
1906
- .reply(200, response, {
1907
- 'cache-control': 'no-cache',
1908
- 'content-type': 'text/event-stream; charset=utf-8',
1909
- 'transfer-encoding': 'chunked',
1910
- })
1911
-
1912
- const sub = getClient()
1913
- .listen('*[_type == "checkin"]', {}, {tag: 'sfcraft.checkins'})
1914
- .subscribe({
1915
- next: (evt) => {
1916
- sub.unsubscribe()
1917
- t.deepEqual(evt.document, doc)
1918
- t.end()
1919
- },
1920
- error: (err) => {
1921
- sub.unsubscribe()
1922
- t.ifError(err)
1923
- t.fail('Should not call error handler')
1924
- t.end()
1925
- },
1926
- })
1927
- })
1928
-
1929
- test('listeners connect to listen endpoint with prefixed request tag, emits events', (t) => {
1930
- const doc = {_id: 'mooblah', _type: 'foo.bar', prop: 'value'}
1931
- const response = [
1932
- ':',
1933
- '',
1934
- 'event: welcome',
1935
- 'data: {"listenerName":"LGFXwOqrf1GHawAjZRnhd6"}',
1936
- '',
1937
- 'event: mutation',
1938
- `data: ${JSON.stringify({document: doc})}`,
1939
- '',
1940
- 'event: disconnect',
1941
- 'data: {"reason":"forcefully closed"}',
1942
- ].join('\n')
1943
-
1944
- nock(projectHost())
1945
- .get(
1946
- '/v1/data/listen/foo?tag=sf.craft.checkins&query=*%5B_type%20%3D%3D%20%22checkin%22%5D&includeResult=true'
1947
- )
1948
- .reply(200, response, {
1949
- 'cache-control': 'no-cache',
1950
- 'content-type': 'text/event-stream; charset=utf-8',
1951
- 'transfer-encoding': 'chunked',
1952
- })
1953
-
1954
- const sub = getClient({requestTagPrefix: 'sf.craft.'})
1955
- .listen('*[_type == "checkin"]', {}, {tag: 'checkins'})
1956
- .subscribe({
1957
- next: (evt) => {
1958
- sub.unsubscribe()
1959
- t.deepEqual(evt.document, doc)
1960
- t.end()
1961
- },
1962
- error: (err) => {
1963
- sub.unsubscribe()
1964
- t.ifError(err)
1965
- t.fail('Should not call error handler')
1966
- t.end()
1967
- },
1968
- })
1969
- })
1970
-
1971
- test('listeners requests are lazy', (t) => {
1972
- const response = [
1973
- ':',
1974
- '',
1975
- 'event: welcome',
1976
- 'data: {"listenerName":"LGFXwOqrf1GHawAjZRnhd6"}',
1977
- '',
1978
- 'event: mutation',
1979
- `data: ${JSON.stringify({})}`,
1980
- ].join('\n')
1981
-
1982
- let didRequest = false
1983
- nock(projectHost())
1984
- .get('/v1/data/listen/foo?query=foo.bar&includeResult=true')
1985
- .reply(() => {
1986
- didRequest = true
1987
- return [200, response]
1988
- })
1989
- const req = getClient().listen('foo.bar', {}, {events: ['welcome']})
1990
- setTimeout(() => {
1991
- t.false(didRequest)
1992
- const sub = req.subscribe({
1993
- next: (r) => {
1994
- t.true(didRequest)
1995
- sub.unsubscribe()
1996
- t.end()
1997
- },
1998
- })
1999
- }, 10)
2000
- })
2001
-
2002
- test('listener requests are cold', (t) => {
2003
- const response = [
2004
- ':',
2005
- '',
2006
- 'event: welcome',
2007
- 'data: {"listenerName":"LGFXwOqrf1GHawAjZRnhd6"}',
2008
- '',
2009
- ':',
2010
- ].join('\n')
2011
-
2012
- let requestCount = 0
2013
- nock(projectHost())
2014
- .get('/v1/data/listen/foo?query=foo.bar&includeResult=true')
2015
- .twice()
2016
- .reply(() => {
2017
- requestCount++
2018
- return [200, response]
2019
- })
2020
-
2021
- const req = getClient().listen('foo.bar', {}, {events: ['welcome']})
2022
-
2023
- t.equal(requestCount, 0)
2024
- const firstSub = req.subscribe({
2025
- next: (r) => {
2026
- t.equal(requestCount, 1)
2027
- firstSub.unsubscribe()
2028
- const secondSub = req.subscribe({
2029
- next: () => {
2030
- t.equal(requestCount, 2)
2031
- secondSub.unsubscribe()
2032
- t.end()
2033
- },
2034
- error: t.ifError,
2035
- })
2036
- },
2037
- error: t.ifError,
2038
- })
2039
- })
2040
-
2041
- /*****************
2042
- * ASSETS *
2043
- *****************/
2044
- test('uploads images', (t) => {
2045
- const fixturePath = fixture('horsehead-nebula.jpg')
2046
- const isImage = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2047
-
2048
- nock(projectHost())
2049
- .post('/v1/assets/images/foo', isImage)
2050
- .reply(201, {document: {url: 'https://some.asset.url'}})
2051
-
2052
- getClient()
2053
- .assets.upload('image', fs.createReadStream(fixturePath))
2054
- .then((document) => {
2055
- t.equal(document.url, 'https://some.asset.url')
2056
- t.end()
2057
- }, ifError(t))
2058
- })
2059
-
2060
- test('uploads images with request tag if given', (t) => {
2061
- const fixturePath = fixture('horsehead-nebula.jpg')
2062
- const isImage = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2063
-
2064
- nock(projectHost())
2065
- .post('/v1/assets/images/foo?tag=galaxy.images', isImage)
2066
- .reply(201, {document: {url: 'https://some.asset.url'}})
2067
-
2068
- getClient()
2069
- .assets.upload('image', fs.createReadStream(fixturePath), {tag: 'galaxy.images'})
2070
- .then((document) => {
2071
- t.equal(document.url, 'https://some.asset.url')
2072
- t.end()
2073
- }, ifError(t))
2074
- })
2075
-
2076
- test('uploads images with prefixed request tag if given', (t) => {
2077
- const fixturePath = fixture('horsehead-nebula.jpg')
2078
- const isImage = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2079
-
2080
- nock(projectHost())
2081
- .post('/v1/assets/images/foo?tag=galaxy.images', isImage)
2082
- .reply(201, {document: {url: 'https://some.asset.url'}})
2083
-
2084
- getClient({requestTagPrefix: 'galaxy'})
2085
- .assets.upload('image', fs.createReadStream(fixturePath), {tag: 'images'})
2086
- .then((document) => {
2087
- t.equal(document.url, 'https://some.asset.url')
2088
- t.end()
2089
- }, ifError(t))
2090
- })
2091
-
2092
- test('uploads images with given content type', (t) => {
2093
- const fixturePath = fixture('horsehead-nebula.jpg')
2094
- const isImage = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2095
-
2096
- nock(projectHost(), {reqheaders: {'Content-Type': 'image/jpeg'}})
2097
- .post('/v1/assets/images/foo', isImage)
2098
- .reply(201, {document: {url: 'https://some.asset.url'}})
2099
-
2100
- getClient()
2101
- .assets.upload('image', fs.createReadStream(fixturePath), {contentType: 'image/jpeg'})
2102
- .then((document) => {
2103
- t.equal(document.url, 'https://some.asset.url')
2104
- t.end()
2105
- }, ifError(t))
2106
- })
2107
-
2108
- test('uploads images with specified metadata to be extracted', (t) => {
2109
- const fixturePath = fixture('horsehead-nebula.jpg')
2110
- const isImage = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2111
-
2112
- nock(projectHost())
2113
- .post('/v1/assets/images/foo?meta=palette&meta=location', isImage)
2114
- .reply(201, {document: {url: 'https://some.asset.url'}})
2115
-
2116
- const options = {extract: ['palette', 'location']}
2117
- getClient()
2118
- .assets.upload('image', fs.createReadStream(fixturePath), options)
2119
- .then((document) => {
2120
- t.equal(document.url, 'https://some.asset.url')
2121
- t.end()
2122
- }, ifError(t))
2123
- })
2124
-
2125
- test('empty extract array sends `none` as metadata', (t) => {
2126
- const fixturePath = fixture('horsehead-nebula.jpg')
2127
- const isImage = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2128
-
2129
- nock(projectHost())
2130
- .post('/v1/assets/images/foo?meta=none', isImage)
2131
- .reply(201, {document: {url: 'https://some.asset.url'}})
2132
-
2133
- const options = {extract: []}
2134
- getClient()
2135
- .assets.upload('image', fs.createReadStream(fixturePath), options)
2136
- .then((document) => {
2137
- t.equal(document.url, 'https://some.asset.url')
2138
- t.end()
2139
- }, ifError(t))
2140
- })
2141
-
2142
- test('uploads images with progress events', (t) => {
2143
- const fixturePath = fixture('horsehead-nebula.jpg')
2144
- const isImage = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2145
-
2146
- nock(projectHost())
2147
- .post('/v1/assets/images/foo', isImage)
2148
- .reply(201, {url: 'https://some.asset.url'})
2149
-
2150
- getClient()
2151
- .observable.assets.upload('image', fs.createReadStream(fixturePath))
2152
- .pipe(filter((event) => event.type === 'progress'))
2153
- .subscribe(
2154
- (event) => t.equal(event.type, 'progress'),
2155
- ifError(t),
2156
- () => t.end()
2157
- )
2158
- })
2159
-
2160
- test('uploads images with custom label', (t) => {
2161
- const fixturePath = fixture('horsehead-nebula.jpg')
2162
- const isImage = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2163
- const label = 'xy zzy'
2164
- nock(projectHost())
2165
- .post(`/v1/assets/images/foo?label=${encodeURIComponent(label)}`, isImage)
2166
- .reply(201, {document: {label: label}})
2167
-
2168
- getClient()
2169
- .assets.upload('image', fs.createReadStream(fixturePath), {label: label})
2170
- .then((body) => {
2171
- t.equal(body.label, label)
2172
- t.end()
2173
- }, ifError(t))
2174
- })
2175
-
2176
- test('uploads files', (t) => {
2177
- const fixturePath = fixture('pdf-sample.pdf')
2178
- const isFile = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2179
-
2180
- nock(projectHost())
2181
- .post('/v1/assets/files/foo', isFile)
2182
- .reply(201, {document: {url: 'https://some.asset.url'}})
2183
-
2184
- getClient()
2185
- .assets.upload('file', fs.createReadStream(fixturePath))
2186
- .then((document) => {
2187
- t.equal(document.url, 'https://some.asset.url')
2188
- t.end()
2189
- }, ifError(t))
2190
- })
2191
-
2192
- test('uploads images and can cast to promise', (t) => {
2193
- const fixturePath = fixture('horsehead-nebula.jpg')
2194
- const isImage = (body) => bufferFrom(body, 'hex').compare(fs.readFileSync(fixturePath)) === 0
2195
-
2196
- nock(projectHost())
2197
- .post('/v1/assets/images/foo', isImage)
2198
- .reply(201, {document: {url: 'https://some.asset.url'}})
2199
-
2200
- getClient()
2201
- .assets.upload('image', fs.createReadStream(fixturePath))
2202
- .then((document) => {
2203
- t.equal(document.url, 'https://some.asset.url')
2204
- t.end()
2205
- }, ifError(t))
2206
- })
2207
-
2208
- test('delete assets', (t) => {
2209
- const expectedBody = {mutations: [{delete: {id: 'image-abc123_foobar-123x123-png'}}]}
2210
- nock(projectHost())
2211
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
2212
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'delete'}]})
2213
-
2214
- getClient()
2215
- .assets.delete('image', 'image-abc123_foobar-123x123-png')
2216
- .catch(t.ifError)
2217
- .then(() => t.end())
2218
- })
2219
-
2220
- test('delete assets with prefix', (t) => {
2221
- const expectedBody = {mutations: [{delete: {id: 'image-abc123_foobar-123x123-png'}}]}
2222
- nock(projectHost())
2223
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
2224
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'delete'}]})
2225
-
2226
- getClient()
2227
- .assets.delete('image', 'abc123_foobar-123x123-png')
2228
- .catch(t.ifError)
2229
- .then(() => t.end())
2230
- })
2231
-
2232
- test('delete assets given whole asset document', (t) => {
2233
- const expectedBody = {mutations: [{delete: {id: 'image-abc123_foobar-123x123-png'}}]}
2234
- nock(projectHost())
2235
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
2236
- .reply(200, {transactionId: 'abc123', results: [{id: 'abc123', operation: 'delete'}]})
2237
-
2238
- const doc = {_id: 'image-abc123_foobar-123x123-png', _type: 'sanity.imageAsset'}
2239
- getClient()
2240
- .assets.delete(doc, 'image-abc123_foobar-123x123-png')
2241
- .catch(t.ifError)
2242
- .then(() => t.end())
2243
- })
2244
-
2245
- test('can get an image URL from a reference ID string', (t) => {
2246
- const url = getClient().assets.getImageUrl('image-someImageId-200x300-png')
2247
- t.equal(url, 'https://cdn.sanity.io/images/bf1942/foo/someImageId-200x300.png')
2248
- t.end()
2249
- })
2250
-
2251
- test('can get an image URL from a reference object', (t) => {
2252
- const url = getClient().assets.getImageUrl({_ref: 'image-someImageId-200x300-png'})
2253
- t.equal(url, 'https://cdn.sanity.io/images/bf1942/foo/someImageId-200x300.png')
2254
- t.end()
2255
- })
2256
-
2257
- test('can get an image URL with added query string', (t) => {
2258
- const url = getClient().assets.getImageUrl('image-someImageId-200x300-png', {
2259
- w: 320,
2260
- fit: 'crop',
2261
- crop: 'bottom,right',
2262
- })
2263
-
2264
- const base = 'https://cdn.sanity.io/images/bf1942/foo/someImageId-200x300.png'
2265
- const qs = 'w=320&fit=crop&crop=bottom%2Cright'
2266
- t.equal(url, [base, qs].join('?'))
2267
- t.end()
2268
- })
2269
-
2270
- test('throws if trying to get image URL from object without ref', (t) => {
2271
- t.throws(() => {
2272
- getClient().assets.getImageUrl({id: 'image-someImageId-200x300-png'})
2273
- }, /object with a _ref/)
2274
- t.end()
2275
- })
2276
-
2277
- test('throws if trying to get image URL from string in invalid format', (t) => {
2278
- t.throws(() => {
2279
- getClient().assets.getImageUrl('file-someImageId-200x300-png')
2280
- }, /Unsupported asset ID/)
2281
- t.end()
2282
- })
2283
-
2284
- /*****************
2285
- * AUTH *
2286
- *****************/
2287
- test('can retrieve auth providers', (t) => {
2288
- const response = {
2289
- providers: [
2290
- {
2291
- name: 'providerid',
2292
- title: 'providertitle',
2293
- url: 'https://some/login/url',
2294
- },
2295
- ],
2296
- }
2297
-
2298
- nock(projectHost()).get('/v1/auth/providers').reply(200, response)
2299
-
2300
- getClient()
2301
- .auth.getLoginProviders()
2302
- .then((body) => {
2303
- t.deepEqual(body, response)
2304
- t.end()
2305
- }, ifError(t))
2306
- })
2307
-
2308
- test('can logout', (t) => {
2309
- nock(projectHost()).post('/v1/auth/logout').reply(200)
2310
-
2311
- getClient()
2312
- .auth.logout()
2313
- .then(() => t.end(), ifError(t))
2314
- })
2315
-
2316
- /*****************
2317
- * USERS *
2318
- *****************/
2319
- test('can retrieve user by id', (t) => {
2320
- const response = {
2321
- role: null,
2322
- id: 'Z29vZA2MTc2MDY5MDI1MDA3MzA5MTAwOjozMjM',
2323
- name: 'Mannen i Gata',
2324
- email: 'some@email.com',
2325
- }
2326
-
2327
- nock(projectHost()).get('/v1/users/me').reply(200, response)
2328
-
2329
- getClient()
2330
- .users.getById('me')
2331
- .then((body) => {
2332
- t.deepEqual(body, response)
2333
- t.end()
2334
- }, ifError(t))
2335
- })
2336
-
2337
- /*****************
2338
- * CDN API USAGE *
2339
- *****************/
2340
- test('will use live API by default', (t) => {
2341
- const client = sanityClient({projectId: 'abc123', dataset: 'foo'})
2342
-
2343
- const response = {result: []}
2344
- nock('https://abc123.api.sanity.io').get('/v1/data/query/foo?query=*').reply(200, response)
2345
-
2346
- client
2347
- .fetch('*')
2348
- .then((docs) => {
2349
- t.equal(docs.length, 0)
2350
- })
2351
- .catch(t.ifError)
2352
- .then(t.end)
2353
- })
2354
-
2355
- test('will use CDN API if told to', (t) => {
2356
- const client = sanityClient({projectId: 'abc123', dataset: 'foo', useCdn: true})
2357
-
2358
- const response = {result: []}
2359
- nock('https://abc123.apicdn.sanity.io').get('/v1/data/query/foo?query=*').reply(200, response)
2360
-
2361
- client
2362
- .fetch('*')
2363
- .then((docs) => {
2364
- t.equal(docs.length, 0)
2365
- })
2366
- .catch(t.ifError)
2367
- .then(t.end)
2368
- })
2369
-
2370
- test('will use live API for mutations', (t) => {
2371
- const client = sanityClient({projectId: 'abc123', dataset: 'foo', useCdn: true})
2372
-
2373
- nock('https://abc123.api.sanity.io')
2374
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync')
2375
- .reply(200, {})
2376
-
2377
- client.create({_type: 'foo', title: 'yep'}).then(noop).catch(t.ifError).then(t.end)
2378
- })
2379
-
2380
- test('will use cdn for queries even when with token specified', (t) => {
2381
- const client = sanityClient({
2382
- projectId: 'abc123',
2383
- dataset: 'foo',
2384
- useCdn: true,
2385
- token: 'foo',
2386
- })
2387
-
2388
- const reqheaders = {Authorization: 'Bearer foo'}
2389
- nock('https://abc123.apicdn.sanity.io', {reqheaders})
2390
- .get('/v1/data/query/foo?query=*')
2391
- .reply(200, {result: []})
2392
-
2393
- client.fetch('*').then(noop).catch(t.ifError).then(t.end)
2394
- })
2395
-
2396
- test('allows overriding headers', (t) => {
2397
- const client = sanityClient({
2398
- projectId: 'abc123',
2399
- dataset: 'foo',
2400
- token: 'foo',
2401
- })
2402
-
2403
- const reqheaders = {foo: 'bar'}
2404
- nock('https://abc123.api.sanity.io', {reqheaders})
2405
- .get('/v1/data/query/foo?query=*')
2406
- .reply(200, {result: []})
2407
-
2408
- client
2409
- .fetch('*', {}, {headers: {foo: 'bar'}})
2410
- .then(noop)
2411
- .catch(t.ifError)
2412
- .then(t.end)
2413
- })
2414
-
2415
- test('will use live API if withCredentials is set to true', (t) => {
2416
- const client = sanityClient({
2417
- withCredentials: true,
2418
- projectId: 'abc123',
2419
- dataset: 'foo',
2420
- useCdn: true,
2421
- })
2422
-
2423
- nock('https://abc123.api.sanity.io').get('/v1/data/query/foo?query=*').reply(200, {result: []})
2424
-
2425
- client.fetch('*').then(noop).catch(t.ifError).then(t.end)
2426
- })
2427
-
2428
- /*****************
2429
- * HTTP REQUESTS *
2430
- *****************/
2431
-
2432
- test('includes token if set', (t) => {
2433
- const qs = '?query=foo.bar'
2434
- const token = 'abcdefghijklmnopqrstuvwxyz'
2435
- const reqheaders = {Authorization: `Bearer ${token}`}
2436
- nock(projectHost(), {reqheaders}).get(`/v1/data/query/foo${qs}`).reply(200, {result: []})
2437
-
2438
- getClient({token})
2439
- .fetch('foo.bar')
2440
- .then((docs) => {
2441
- t.equal(docs.length, 0)
2442
- })
2443
- .catch(t.ifError)
2444
- .then(t.end)
2445
- })
2446
-
2447
- test('allows overriding token', (t) => {
2448
- const qs = '?query=foo.bar'
2449
- const token = 'abcdefghijklmnopqrstuvwxyz'
2450
- const override = '123456789'
2451
- const reqheaders = {Authorization: `Bearer ${override}`}
2452
- nock(projectHost(), {reqheaders}).get(`/v1/data/query/foo${qs}`).reply(200, {result: []})
2453
-
2454
- getClient({token})
2455
- .fetch('foo.bar', {}, {token: override})
2456
- .then((docs) => {
2457
- t.equal(docs.length, 0)
2458
- })
2459
- .catch(t.ifError)
2460
- .then(t.end)
2461
- })
2462
-
2463
- test('allows overriding timeout', (t) => {
2464
- const qs = `?query=${encodeURIComponent('*[][0]')}`
2465
- nock(projectHost()).get(`/v1/data/query/foo${qs}`).reply(200, {result: []})
2466
-
2467
- getClient()
2468
- .fetch('*[][0]', {}, {timeout: 60 * 1000})
2469
- .then((docs) => {
2470
- t.equal(docs.length, 0)
2471
- })
2472
- .catch(t.ifError)
2473
- .then(t.end)
2474
- })
2475
-
2476
- test('includes user agent in node', (t) => {
2477
- const pkg = require('../package.json')
2478
- const reqheaders = {'User-Agent': `${pkg.name} ${pkg.version}`}
2479
- nock(projectHost(), {reqheaders}).get('/v1/data/doc/foo/bar').reply(200, {documents: []})
2480
-
2481
- getClient().getDocument('bar').catch(t.ifError).then(t.end)
2482
- })
2483
-
2484
- // Don't rely on this unless you're working at Sanity Inc ;)
2485
- test('can use alternative http requester', (t) => {
2486
- const requester = () =>
2487
- observableOf({
2488
- type: 'response',
2489
- body: {documents: [{foo: 'bar'}]},
2490
- })
2491
-
2492
- getClient({requester})
2493
- .getDocument('foo.bar')
2494
- .then((res) => {
2495
- t.equal(res.foo, 'bar')
2496
- t.end()
2497
- })
2498
- .catch((err) => {
2499
- t.ifError(err)
2500
- t.fail('should not call catch handler')
2501
- t.end()
2502
- })
2503
- })
2504
-
2505
- test('ClientError includes message in stack', (t) => {
2506
- const body = {error: {description: 'Invalid query'}}
2507
- const error = new SanityClient.ClientError({statusCode: 400, headers: {}, body})
2508
- t.ok(error.stack.includes(body.error.description))
2509
- t.end()
2510
- })
2511
-
2512
- test('ServerError includes message in stack', (t) => {
2513
- const body = {error: 'Gateway Time-Out', message: 'The upstream service did not respond in time'}
2514
- const error = new SanityClient.ClientError({statusCode: 504, headers: {}, body})
2515
- t.ok(error.stack.includes(body.error))
2516
- t.ok(error.stack.includes(body.message))
2517
- t.end()
2518
- })
2519
-
2520
- test('exposes ClientError', (t) => {
2521
- t.equal(typeof sanityClient.ClientError, 'function')
2522
- const error = new SanityClient.ClientError({statusCode: 400, headers: {}, body: {}})
2523
- t.ok(error instanceof Error)
2524
- t.ok(error instanceof sanityClient.ClientError)
2525
- t.end()
2526
- })
2527
-
2528
- test('exposes ServerError', (t) => {
2529
- t.equal(typeof sanityClient.ServerError, 'function')
2530
- const error = new SanityClient.ServerError({statusCode: 500, headers: {}, body: {}})
2531
- t.ok(error instanceof Error)
2532
- t.ok(error instanceof sanityClient.ServerError)
2533
- t.end()
2534
- })
2535
-
2536
- // Don't rely on this unless you're working at Sanity Inc ;)
2537
- test('exposes default requester', (t) => {
2538
- t.equal(typeof sanityClient.requester, 'function')
2539
- t.end()
2540
- })
2541
-
2542
- test('handles HTTP errors gracefully', (t) => {
2543
- const doc = {_id: 'barfoo', visits: 5}
2544
- const expectedBody = {mutations: [{create: doc}]}
2545
- nock(projectHost())
2546
- .post('/v1/data/mutate/foo?returnIds=true&returnDocuments=true&visibility=sync', expectedBody)
2547
- .times(6)
2548
- .replyWithError(new Error('Something went wrong'))
2549
-
2550
- getClient()
2551
- .create(doc)
2552
- .then(() => {
2553
- t.fail('Should not call success handler on error')
2554
- t.end()
2555
- })
2556
- .catch((err) => {
2557
- t.ok(err instanceof Error, 'should error')
2558
- t.equal(err.message, 'Something went wrong', 'has message')
2559
- t.end()
2560
- })
2561
- })