@data-client/rest 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +110 -0
  3. package/dist/index.js +561 -0
  4. package/dist/index.umd.min.js +1 -0
  5. package/dist/next.js +520 -0
  6. package/dist/package.json +1 -0
  7. package/legacy/NetworkError.js +13 -0
  8. package/legacy/OptionsToFunction.js +2 -0
  9. package/legacy/RestEndpoint.js +225 -0
  10. package/legacy/RestEndpointTypeHelp.js +2 -0
  11. package/legacy/RestHelpers.js +33 -0
  12. package/legacy/createResource.js +88 -0
  13. package/legacy/extractCollection.js +14 -0
  14. package/legacy/getArrayPath.js +19 -0
  15. package/legacy/hookifyResource.js +24 -0
  16. package/legacy/index.js +10 -0
  17. package/legacy/mapCollection.js +14 -0
  18. package/legacy/next/OptionsToFunction.js +2 -0
  19. package/legacy/next/RestEndpoint.js +225 -0
  20. package/legacy/next/createResource.js +86 -0
  21. package/legacy/next/index.js +4 -0
  22. package/legacy/next/resourceTypes.js +2 -0
  23. package/legacy/paginatedCollections.js +29 -0
  24. package/legacy/paginationUpdate.js +36 -0
  25. package/legacy/paramsToString.js +6 -0
  26. package/legacy/paramsToString.native.js +9 -0
  27. package/legacy/pathTypes.js +2 -0
  28. package/legacy/test-fixtures.js +111 -0
  29. package/legacy/utiltypes.js +2 -0
  30. package/lib/NetworkError.d.ts +11 -0
  31. package/lib/NetworkError.d.ts.map +1 -0
  32. package/lib/NetworkError.js +13 -0
  33. package/lib/OptionsToFunction.d.ts +7 -0
  34. package/lib/OptionsToFunction.d.ts.map +1 -0
  35. package/lib/OptionsToFunction.js +2 -0
  36. package/lib/RestEndpoint.d.ts +548 -0
  37. package/lib/RestEndpoint.js +229 -0
  38. package/lib/RestEndpointTypeHelp.d.ts +10 -0
  39. package/lib/RestEndpointTypeHelp.d.ts.map +1 -0
  40. package/lib/RestEndpointTypeHelp.js +2 -0
  41. package/lib/RestHelpers.d.ts +7 -0
  42. package/lib/RestHelpers.d.ts.map +1 -0
  43. package/lib/RestHelpers.js +33 -0
  44. package/lib/createResource.d.ts +66 -0
  45. package/lib/createResource.d.ts.map +1 -0
  46. package/lib/createResource.js +104 -0
  47. package/lib/extractCollection.d.ts +11 -0
  48. package/lib/extractCollection.d.ts.map +1 -0
  49. package/lib/extractCollection.js +16 -0
  50. package/lib/getArrayPath.d.ts +3 -0
  51. package/lib/getArrayPath.d.ts.map +1 -0
  52. package/lib/getArrayPath.js +19 -0
  53. package/lib/hookifyResource.d.ts +14 -0
  54. package/lib/hookifyResource.d.ts.map +1 -0
  55. package/lib/hookifyResource.js +24 -0
  56. package/lib/index.d.ts +12 -0
  57. package/lib/index.d.ts.map +1 -0
  58. package/lib/index.js +10 -0
  59. package/lib/mapCollection.d.ts +16 -0
  60. package/lib/mapCollection.d.ts.map +1 -0
  61. package/lib/mapCollection.js +16 -0
  62. package/lib/next/OptionsToFunction.d.ts +7 -0
  63. package/lib/next/OptionsToFunction.d.ts.map +1 -0
  64. package/lib/next/OptionsToFunction.js +2 -0
  65. package/lib/next/RestEndpoint.d.ts +542 -0
  66. package/lib/next/RestEndpoint.js +229 -0
  67. package/lib/next/createResource.d.ts +86 -0
  68. package/lib/next/createResource.d.ts.map +1 -0
  69. package/lib/next/createResource.js +100 -0
  70. package/lib/next/index.d.ts +6 -0
  71. package/lib/next/index.d.ts.map +1 -0
  72. package/lib/next/index.js +4 -0
  73. package/lib/next/resourceTypes.d.ts +31 -0
  74. package/lib/next/resourceTypes.d.ts.map +1 -0
  75. package/lib/next/resourceTypes.js +2 -0
  76. package/lib/paginatedCollections.d.ts +3 -0
  77. package/lib/paginatedCollections.d.ts.map +1 -0
  78. package/lib/paginatedCollections.js +29 -0
  79. package/lib/paginationUpdate.d.ts +8 -0
  80. package/lib/paginationUpdate.d.ts.map +1 -0
  81. package/lib/paginationUpdate.js +40 -0
  82. package/lib/paramsToString.d.ts +2 -0
  83. package/lib/paramsToString.d.ts.map +1 -0
  84. package/lib/paramsToString.js +6 -0
  85. package/lib/paramsToString.native.d.ts +2 -0
  86. package/lib/paramsToString.native.d.ts.map +1 -0
  87. package/lib/paramsToString.native.js +9 -0
  88. package/lib/pathTypes.d.ts +20 -0
  89. package/lib/pathTypes.d.ts.map +1 -0
  90. package/lib/pathTypes.js +2 -0
  91. package/lib/test-fixtures.d.ts +141 -0
  92. package/lib/test-fixtures.d.ts.map +1 -0
  93. package/lib/test-fixtures.js +111 -0
  94. package/lib/utiltypes.d.ts +21 -0
  95. package/lib/utiltypes.d.ts.map +1 -0
  96. package/lib/utiltypes.js +2 -0
  97. package/node.mjs +1 -0
  98. package/package.json +121 -0
  99. package/src/NetworkError.ts +17 -0
  100. package/src/OptionsToFunction.ts +42 -0
  101. package/src/RestEndpoint.d.ts +548 -0
  102. package/src/RestEndpoint.js +262 -0
  103. package/src/RestEndpointTypeHelp.ts +20 -0
  104. package/src/RestHelpers.ts +48 -0
  105. package/src/__tests__/Resource.test.ts +317 -0
  106. package/src/__tests__/RestEndpoint.ts +1304 -0
  107. package/src/__tests__/__snapshots__/RestEndpoint.ts.snap +18 -0
  108. package/src/__tests__/__snapshots__/createResource.test.ts.snap +5 -0
  109. package/src/__tests__/createResource.test.ts +583 -0
  110. package/src/__tests__/getArrayPath.test.ts +30 -0
  111. package/src/__tests__/hookifyResource.ts +197 -0
  112. package/src/__tests__/types.test.ts +309 -0
  113. package/src/createResource.ts +172 -0
  114. package/src/extractCollection.ts +34 -0
  115. package/src/getArrayPath.ts +20 -0
  116. package/src/hookifyResource.ts +36 -0
  117. package/src/index.ts +42 -0
  118. package/src/mapCollection.ts +54 -0
  119. package/src/next/OptionsToFunction.ts +41 -0
  120. package/src/next/RestEndpoint.d.ts +542 -0
  121. package/src/next/RestEndpoint.js +262 -0
  122. package/src/next/__tests__/RestEndpoint.ts +1333 -0
  123. package/src/next/__tests__/__snapshots__/RestEndpoint.ts.snap +18 -0
  124. package/src/next/__tests__/types.test.ts +429 -0
  125. package/src/next/createResource.ts +194 -0
  126. package/src/next/index.ts +5 -0
  127. package/src/next/resourceTypes.ts +32 -0
  128. package/src/package.json +1 -0
  129. package/src/paginatedCollections.ts +32 -0
  130. package/src/paginationUpdate.ts +49 -0
  131. package/src/paramsToString.native.ts +10 -0
  132. package/src/paramsToString.ts +7 -0
  133. package/src/pathTypes.ts +58 -0
  134. package/src/test-fixtures.ts +128 -0
  135. package/src/utiltypes.ts +25 -0
  136. package/ts4.0/NetworkError.d.ts +11 -0
  137. package/ts4.0/OptionsToFunction.d.ts +16 -0
  138. package/ts4.0/RestEndpoint.d.ts +240 -0
  139. package/ts4.0/RestEndpointTypeHelp.d.ts +10 -0
  140. package/ts4.0/RestHelpers.d.ts +7 -0
  141. package/ts4.0/createResource.d.ts +66 -0
  142. package/ts4.0/extractCollection.d.ts +11 -0
  143. package/ts4.0/getArrayPath.d.ts +3 -0
  144. package/ts4.0/hookifyResource.d.ts +19 -0
  145. package/ts4.0/index.d.ts +12 -0
  146. package/ts4.0/mapCollection.d.ts +16 -0
  147. package/ts4.0/next/OptionsToFunction.d.ts +16 -0
  148. package/ts4.0/next/RestEndpoint.d.ts +226 -0
  149. package/ts4.0/next/createResource.d.ts +94 -0
  150. package/ts4.0/next/index.d.ts +6 -0
  151. package/ts4.0/next/resourceTypes.d.ts +31 -0
  152. package/ts4.0/paginatedCollections.d.ts +3 -0
  153. package/ts4.0/paginationUpdate.d.ts +10 -0
  154. package/ts4.0/paramsToString.d.ts +2 -0
  155. package/ts4.0/paramsToString.native.d.ts +2 -0
  156. package/ts4.0/pathTypes.d.ts +20 -0
  157. package/ts4.0/test-fixtures.d.ts +141 -0
  158. package/ts4.0/utiltypes.d.ts +5 -0
@@ -0,0 +1,1304 @@
1
+ import { Entity } from '@data-client/endpoint';
2
+ import { useController } from '@data-client/react';
3
+ import { useSuspense } from '@data-client/react';
4
+ import { CacheProvider } from '@data-client/react';
5
+ import { act } from '@testing-library/react-hooks';
6
+ import { Article, CoolerArticle, CoolerArticleResource } from '__tests__/new';
7
+ import nock from 'nock';
8
+
9
+ import { makeRenderRestHook } from '../../../test';
10
+ import RestEndpoint, {
11
+ Defaults,
12
+ RestEndpointConstructorOptions,
13
+ RestGenerics,
14
+ } from '../RestEndpoint';
15
+ import {
16
+ payload,
17
+ createPayload,
18
+ users,
19
+ nested,
20
+ moreNested,
21
+ paginatedFirstPage,
22
+ paginatedSecondPage,
23
+ } from '../test-fixtures';
24
+
25
+ export class User extends Entity {
26
+ readonly id: number | undefined = undefined;
27
+ readonly username: string = '';
28
+ readonly email: string = '';
29
+ readonly isAdmin: boolean = false;
30
+
31
+ pk() {
32
+ return this.id?.toString();
33
+ }
34
+ }
35
+ const getUser = new RestEndpoint({
36
+ path: 'http\\://test.com/user/:id',
37
+ name: 'User.get',
38
+ schema: User,
39
+ method: 'GET',
40
+ searchParams: {} as { extra?: string },
41
+ });
42
+ export class PaginatedArticle extends Entity {
43
+ readonly id: number | undefined = undefined;
44
+ readonly title: string = '';
45
+ readonly content: string = '';
46
+ readonly author: number | null = null;
47
+ readonly tags: string[] = [];
48
+
49
+ pk() {
50
+ return this.id?.toString();
51
+ }
52
+
53
+ static schema = {
54
+ author: User,
55
+ };
56
+ }
57
+ const getArticleList = new RestEndpoint({
58
+ urlPrefix: 'http://test.com',
59
+ path: '/article-paginated',
60
+ schema: {
61
+ nextPage: '',
62
+ data: { results: [PaginatedArticle] },
63
+ },
64
+ method: 'GET',
65
+ });
66
+ const getNextPage = getArticleList.paginated(
67
+ (v: { cursor: string | number }) => [],
68
+ );
69
+
70
+ const getArticleList2 = new RestEndpoint({
71
+ urlPrefix: 'http://test.com/article-paginated/',
72
+ path: ':group',
73
+ name: 'get',
74
+ schema: {
75
+ nextPage: '',
76
+ data: { results: [PaginatedArticle] },
77
+ },
78
+ method: 'GET',
79
+ });
80
+ const getNextPage2 = getArticleList2.paginated(
81
+ ({
82
+ cursor,
83
+ ...rest
84
+ }: {
85
+ cursor: string | number;
86
+ group: string | number;
87
+ }) => [rest],
88
+ );
89
+
90
+ describe('RestEndpoint', () => {
91
+ const renderRestHook: ReturnType<typeof makeRenderRestHook> =
92
+ makeRenderRestHook(CacheProvider);
93
+ let mynock: nock.Scope;
94
+
95
+ beforeEach(() => {
96
+ nock(/.*/)
97
+ .persist()
98
+ .defaultReplyHeaders({
99
+ 'Access-Control-Allow-Origin': '*',
100
+ 'Content-Type': 'application/json',
101
+ })
102
+ .options(/.*/)
103
+ .reply(200)
104
+ .get(`/article-cooler/${payload.id}`)
105
+ .reply(200, payload)
106
+ .delete(`/article-cooler/${payload.id}`)
107
+ .reply(204, '')
108
+ .delete(`/article/${payload.id}`)
109
+ .reply(200, {})
110
+ .get(`/article-cooler/0`)
111
+ .reply(403, {})
112
+ .get(`/article-cooler/666`)
113
+ .reply(200, '')
114
+ .get(`/article-cooler`)
115
+ .reply(200, nested)
116
+ .post(`/article-cooler`)
117
+ .reply(200, createPayload)
118
+ .get(`/user`)
119
+ .reply(200, users);
120
+ mynock = nock(/.*/).defaultReplyHeaders({
121
+ 'Access-Control-Allow-Origin': '*',
122
+ 'Content-Type': 'application/json',
123
+ });
124
+ });
125
+
126
+ afterEach(() => {
127
+ nock.cleanAll();
128
+ });
129
+
130
+ it('testKey should match keys', () => {
131
+ expect(getArticleList.testKey(getArticleList.key())).toBeTruthy();
132
+ expect(
133
+ getUser.testKey(getUser.key({ id: '100', extra: '345' })),
134
+ ).toBeTruthy();
135
+ expect(getUser.testKey(getUser.key({ id: 'xxx?*' }))).toBeTruthy();
136
+ });
137
+
138
+ it('should assign members', () => {
139
+ expect(getUser.path).toBe('http\\://test.com/user/:id');
140
+ expect(getUser.sideEffect).toBe(undefined);
141
+ expect(getUser.method).toBe('GET');
142
+
143
+ // @ts-expect-error
144
+ () => getUser.notassigned;
145
+ // @ts-expect-error
146
+ const a: true = getUser.sideEffect;
147
+ // @ts-expect-error
148
+ const b: 'POST' = getUser.method;
149
+ // @ts-expect-error
150
+ ((m: 'POST') => {})(getUser.method);
151
+
152
+ const updateUser = new RestEndpoint({
153
+ path: 'http\\://test.com/user/:id',
154
+ name: 'update',
155
+ schema: User,
156
+ method: 'POST',
157
+ });
158
+ expect(updateUser.sideEffect).toBe(true);
159
+ // @ts-expect-error
160
+ const y: undefined = updateUser.sideEffect;
161
+ });
162
+
163
+ it('only optional path means the arg is not required', () => {
164
+ const ep = new RestEndpoint({ path: '/users/:id?/:group?' });
165
+ const epbody = new RestEndpoint({
166
+ path: '/users/:id?/:group?',
167
+ body: { title: '' },
168
+ });
169
+ () => ep();
170
+ () => ep({ id: 5 });
171
+ () => ep({ group: 5 });
172
+ () => ep({ id: 5, group: 5 });
173
+ () => epbody({ title: 'hi' });
174
+ () => epbody({ id: 5 }, { title: 'hi' });
175
+ () => epbody({ group: 5 }, { title: 'hi' });
176
+ () => epbody({ id: 5, group: 5 }, { title: 'hi' });
177
+ // @ts-expect-error
178
+ () => epbody({ title: 'hi' }, { title: 'hi' });
179
+ });
180
+
181
+ /* TODO: it('should allow sideEffect overrides', () => {
182
+ const weirdGetUser = new RestEndpoint({
183
+ path: 'http\\://test.com/user/:id',
184
+ name: 'getter',
185
+ schema: User,
186
+ method: 'POST',
187
+ sideEffect: undefined,
188
+ });
189
+
190
+ expect(weirdGetUser.sideEffect).toBe(undefined);
191
+ //s @ts-expect-error
192
+ //const y: true = weirdGetUser.sideEffect;
193
+ });*/
194
+
195
+ it('should handle simple urls', () => {
196
+ expect(getUser.url({ id: '5' })).toBe('http://test.com/user/5');
197
+ expect(getUser.url({ id: '100' })).toBe('http://test.com/user/100');
198
+
199
+ // @ts-expect-error
200
+ () => getUser.url({ sdf: '5' });
201
+ });
202
+
203
+ it('should handle multiarg urls', () => {
204
+ const getMyUser = new RestEndpoint({
205
+ path: 'http\\://test.com/groups/:group/users/:id',
206
+ schema: User,
207
+ method: 'GET',
208
+ extra: 5,
209
+ });
210
+
211
+ expect(getMyUser.url({ group: 'big', id: '5' })).toBe(
212
+ 'http://test.com/groups/big/users/5',
213
+ );
214
+ expect(getMyUser.url({ group: 'big', id: '100' })).toBe(
215
+ 'http://test.com/groups/big/users/100',
216
+ );
217
+
218
+ // missing required
219
+ expect(() =>
220
+ // @ts-expect-error
221
+ getMyUser.url({ id: '5' }),
222
+ ).toThrow();
223
+ // extra fields
224
+ () =>
225
+ getMyUser.url({
226
+ group: 'mygroup',
227
+ id: '5',
228
+ // @ts-expect-error
229
+ notexisting: 'hi',
230
+ });
231
+
232
+ // @ts-expect-error
233
+ () => useSuspense(getMyUser, { id: '5' });
234
+ // @ts-expect-error
235
+ () => useSuspense(getMyUser);
236
+ () => useSuspense(getMyUser, { group: 'yay', id: '5' });
237
+ });
238
+
239
+ it('should automatically name methods', () => {
240
+ expect(getUser.name).toBe('User.get');
241
+ expect(getArticleList.name).toMatchInlineSnapshot(
242
+ `"http://test.com/article-paginated"`,
243
+ );
244
+ expect(
245
+ getArticleList.extend({ path: '/:something' }).name,
246
+ ).toMatchInlineSnapshot(`"http://test.com/:something"`);
247
+ });
248
+
249
+ it('should update on get for a paginated resource', async () => {
250
+ mynock.get(`/article-paginated`).reply(200, paginatedFirstPage);
251
+ mynock.get(`/article-paginated?cursor=2`).reply(200, paginatedSecondPage);
252
+
253
+ const { result, waitForNextUpdate, controller } = renderRestHook(() => {
254
+ const { fetch } = useController();
255
+ const {
256
+ data: { results: articles },
257
+ nextPage,
258
+ } = useSuspense(getArticleList);
259
+ return { articles, nextPage, fetch };
260
+ });
261
+ await waitForNextUpdate();
262
+ // @ts-expect-error
263
+ () => controller.fetch(getNextPage);
264
+ // @ts-expect-error
265
+ () => controller.fetch(getNextPage, { fake: 5 });
266
+ expect(result.current.nextPage).toEqual(paginatedFirstPage.nextPage);
267
+ await controller.fetch(getNextPage, {
268
+ cursor: result.current.nextPage,
269
+ });
270
+ expect(result.current.articles.map(({ id }) => id)).toEqual([5, 3, 7, 8]);
271
+ expect(result.current.nextPage).toBeUndefined();
272
+ });
273
+
274
+ it('should update on get for a paginated resource with parameter in path', async () => {
275
+ mynock.get(`/article-paginated/happy`).reply(200, paginatedFirstPage);
276
+ mynock
277
+ .get(`/article-paginated/happy?cursor=2`)
278
+ .reply(200, paginatedSecondPage);
279
+
280
+ const { result, waitForNextUpdate } = renderRestHook(() => {
281
+ const { fetch } = useController();
282
+ const {
283
+ data: { results: articles },
284
+ nextPage,
285
+ } = useSuspense(getArticleList2, {
286
+ group: 'happy',
287
+ });
288
+ return { articles, nextPage, fetch };
289
+ });
290
+ await waitForNextUpdate();
291
+ // @ts-expect-error
292
+ () => result.current.fetch(getNextPage2);
293
+ // @ts-expect-error
294
+ () => result.current.fetch(getNextPage2, { fake: 5 });
295
+ // @ts-expect-error
296
+ () => result.current.fetch(getNextPage2, { group: 'happy' });
297
+ // @ts-expect-error
298
+ () => result.current.fetch(getNextPage2, { cursor: 2 });
299
+ await result.current.fetch(getNextPage2, {
300
+ group: 'happy',
301
+ cursor: 2,
302
+ });
303
+ expect(result.current.articles.map(({ id }) => id)).toEqual([5, 3, 7, 8]);
304
+ });
305
+
306
+ it('should deduplicate results', async () => {
307
+ mynock.get(`/article-paginated`).reply(200, paginatedFirstPage);
308
+ mynock.get(`/article-paginated?cursor=2`).reply(200, {
309
+ ...paginatedSecondPage,
310
+ results: [nested[nested.length - 1], ...moreNested],
311
+ });
312
+
313
+ const { result, waitForNextUpdate } = renderRestHook(() => {
314
+ const { fetch } = useController();
315
+ const {
316
+ data: { results: articles },
317
+ nextPage,
318
+ } = useSuspense(getArticleList);
319
+ return { articles, nextPage, fetch };
320
+ });
321
+ await waitForNextUpdate();
322
+ await result.current.fetch(getNextPage, {
323
+ cursor: 2,
324
+ });
325
+ //TODO: Why is this broken? expect(result.current.articles.map(({ id }) => id)).toEqual([5, 3, 7, 8]);
326
+ });
327
+
328
+ it('should not deep-merge deeply defined entities', async () => {
329
+ interface Complex {
330
+ firstvalue: number;
331
+ secondthing: {
332
+ arg?: number;
333
+ other?: string;
334
+ };
335
+ }
336
+ class ComplexEntity extends Entity {
337
+ readonly id: string = '';
338
+ readonly complexThing?: Complex = undefined;
339
+ readonly extra: string = '';
340
+
341
+ pk() {
342
+ return this.id;
343
+ }
344
+ }
345
+ const getComplex = new RestEndpoint({
346
+ path: '/complex-thing/:id',
347
+ schema: ComplexEntity,
348
+ method: 'GET',
349
+ });
350
+
351
+ const firstResponse = {
352
+ id: '5',
353
+ complexThing: {
354
+ firstvalue: 233,
355
+ secondthing: { arg: 88 },
356
+ },
357
+ extra: 'hi',
358
+ };
359
+ mynock.get(`/complex-thing/5`).reply(200, firstResponse);
360
+
361
+ const { result, waitForNextUpdate } = renderRestHook(() => {
362
+ const { fetch } = useController();
363
+ const article = useSuspense(getComplex, { id: '5' });
364
+ return { article, fetch };
365
+ });
366
+ await waitForNextUpdate();
367
+ expect(result.current.article).toEqual(firstResponse);
368
+
369
+ const secondResponse = {
370
+ id: '5',
371
+ complexThing: {
372
+ firstvalue: 5,
373
+ secondthing: { other: 'hi' },
374
+ },
375
+ };
376
+
377
+ mynock.get(`/complex-thing/5`).reply(200, secondResponse);
378
+ await result.current.fetch(getComplex, {
379
+ id: '5',
380
+ });
381
+ expect(result.current.article).toEqual({ ...secondResponse, extra: 'hi' });
382
+ });
383
+
384
+ it('overriding methods should work', async () => {
385
+ mynock.put(`/user/5`).reply(200, (uri, body: any) => ({
386
+ id: 5,
387
+ username: 'bob',
388
+ ...body,
389
+ }));
390
+ const updateUser = new RestEndpoint({
391
+ method: 'PUT',
392
+ path: 'http\\://test.com/user/:id',
393
+ name: 'get',
394
+ schema: User,
395
+ getRequestInit(body) {
396
+ if (body && isPojo(body)) {
397
+ return RestEndpoint.prototype.getRequestInit.call(this, {
398
+ ...body,
399
+ email: 'always@always.com',
400
+ });
401
+ }
402
+ return RestEndpoint.prototype.getRequestInit.call(this, body);
403
+ },
404
+ });
405
+ const response = await updateUser(
406
+ { id: 5 },
407
+ { username: 'micky', email: 'micky@gmail.com' },
408
+ );
409
+ expect(response).toMatchInlineSnapshot(`
410
+ {
411
+ "email": "always@always.com",
412
+ "id": 5,
413
+ "username": "micky",
414
+ }
415
+ `);
416
+ });
417
+
418
+ describe('class extensions', () => {
419
+ class DefaultUser extends User {
420
+ defaultUserExtra = 'yay';
421
+ }
422
+
423
+ class MyEndpoint<O extends RestGenerics = any> extends RestEndpoint<
424
+ Defaults<O, { schema: DefaultUser }>
425
+ > {
426
+ constructor(options: Readonly<RestEndpointConstructorOptions<O> & O>) {
427
+ super({ schema: DefaultUser, ...options } as any);
428
+ }
429
+
430
+ parseResponse(response: Response): Promise<any> {
431
+ return super.parseResponse(response);
432
+ }
433
+
434
+ getRequestInit(body: any) {
435
+ if (isPojo(body)) {
436
+ return super.getRequestInit({ ...body, email: 'always@always.com' });
437
+ }
438
+ return super.getRequestInit(body);
439
+ }
440
+
441
+ additional = 5;
442
+ }
443
+
444
+ it('should work with constructor', async () => {
445
+ mynock.put('/user/5').reply(200, (uri, body: any) => ({
446
+ id: 5,
447
+ username: 'bob',
448
+ ...body,
449
+ }));
450
+
451
+ const updateUser = new MyEndpoint({
452
+ method: 'PUT',
453
+ path: 'http\\://test.com/user/:id',
454
+ name: 'update',
455
+ schema: User,
456
+ });
457
+ const response = await updateUser(
458
+ { id: 5 },
459
+ { username: 'micky', email: 'micky@gmail.com' },
460
+ );
461
+ expect(response).toMatchInlineSnapshot(`
462
+ {
463
+ "email": "always@always.com",
464
+ "id": 5,
465
+ "username": "micky",
466
+ }
467
+ `);
468
+ expect(updateUser.additional).toBe(5);
469
+ });
470
+
471
+ it('setting body in extend should work', async () => {
472
+ mynock.put('/charmer/5').reply(200, (uri, body: any) => ({
473
+ id: 5,
474
+ username: 'bob',
475
+ ...body,
476
+ }));
477
+
478
+ const updateUser = new MyEndpoint({
479
+ method: 'PUT',
480
+ path: 'http\\://test.com/user/:id',
481
+ name: 'update',
482
+ schema: User,
483
+ }).extend({
484
+ body: 5,
485
+ path: 'http\\://test.com/charmer/:charm',
486
+ });
487
+ const response = await updateUser(
488
+ { charm: 5 },
489
+ // @ts-expect-error
490
+ { username: 'micky', email: 'micky@gmail.com' },
491
+ );
492
+ () => updateUser({ charm: 5 }, 5);
493
+ expect(response).toMatchInlineSnapshot(`
494
+ {
495
+ "email": "always@always.com",
496
+ "id": 5,
497
+ "username": "micky",
498
+ }
499
+ `);
500
+ expect(updateUser.additional).toBe(5);
501
+ const nobody = updateUser.extend({
502
+ path: 'http\\://test.com/user/:charm',
503
+ body: undefined,
504
+ });
505
+ () => nobody({ charm: 5 });
506
+ // @ts-expect-error
507
+ () => nobody({ id: 5 });
508
+ });
509
+
510
+ it('setting body in extend should work without path', async () => {
511
+ mynock.put('/user/5').reply(200, (uri, body: any) => ({
512
+ id: 5,
513
+ username: 'bob',
514
+ ...body,
515
+ }));
516
+
517
+ const updateUser = new MyEndpoint({
518
+ method: 'PUT',
519
+ path: 'http\\://test.com/user/:id',
520
+ name: 'update',
521
+ schema: User,
522
+ }).extend({
523
+ body: 5,
524
+ });
525
+ const response = await updateUser(
526
+ { id: 5 },
527
+ // @ts-expect-error
528
+ { username: 'micky', email: 'micky@gmail.com' },
529
+ );
530
+ () => updateUser({ id: 5 }, 5);
531
+ expect(response).toMatchInlineSnapshot(`
532
+ {
533
+ "email": "always@always.com",
534
+ "id": 5,
535
+ "username": "micky",
536
+ }
537
+ `);
538
+ expect(updateUser.additional).toBe(5);
539
+ const nobody = updateUser.extend({
540
+ path: 'http\\://test.com/user/:charm',
541
+ body: undefined,
542
+ });
543
+ () => nobody({ charm: 5 });
544
+ // @ts-expect-error
545
+ () => nobody({ id: 5 });
546
+
547
+ const updateUser2 = new MyEndpoint({
548
+ method: 'PUT',
549
+ path: 'http\\://test.com/user/:id',
550
+ name: 'update',
551
+ schema: User,
552
+ }).extend({
553
+ searchParams: {} as { isAdmin: boolean },
554
+ });
555
+ () =>
556
+ updateUser2(
557
+ { id: 5, isAdmin: true },
558
+ { username: 'micky', email: 'micky@gmail.com' },
559
+ );
560
+ () =>
561
+ // @ts-expect-error
562
+ updateUser2({ id: 5 }, { username: 'micky', email: 'micky@gmail.com' });
563
+ () =>
564
+ updateUser2(
565
+ // @ts-expect-error
566
+ { isAdmin: true },
567
+ { username: 'micky', email: 'micky@gmail.com' },
568
+ );
569
+
570
+ const updateBasic = new MyEndpoint({
571
+ method: 'PUT',
572
+ path: 'http\\://test.com/user/:id',
573
+ name: 'update',
574
+ schema: User,
575
+ });
576
+ () =>
577
+ updateBasic({ id: 5 }, { username: 'micky', email: 'micky@gmail.com' });
578
+ () =>
579
+ updateBasic(
580
+ // @ts-expect-error
581
+ { id: 5, isAdmin: true },
582
+ { username: 'micky', email: 'micky@gmail.com' },
583
+ );
584
+ });
585
+
586
+ it('should work with default schema in class definition', async () => {
587
+ mynock.get('/user/5').reply(200, (uri, body: any) => ({
588
+ id: 5,
589
+ username: 'bob',
590
+ email: 'bob@gmail.com',
591
+ }));
592
+
593
+ const getUser = new MyEndpoint({
594
+ path: 'http\\://test.com/user/:id',
595
+ name: 'update',
596
+ });
597
+ const { result, waitForNextUpdate } = renderRestHook(() => {
598
+ return useSuspense(getUser, { id: 5 });
599
+ });
600
+ await waitForNextUpdate();
601
+
602
+ expect(result.current.username).toBe('bob');
603
+ // @ts-expect-error
604
+ expect(result.current.sdfsd).toBeUndefined();
605
+ expect(result.current.defaultUserExtra).toBe('yay');
606
+ expect(result.current.email).toBe('bob@gmail.com');
607
+ });
608
+
609
+ it('should work with default path in class definition', async () => {
610
+ mynock.get('/user/5').reply(200, (uri, body: any) => ({
611
+ id: 5,
612
+ username: 'bob',
613
+ email: 'bob@gmail.com',
614
+ }));
615
+ // this seems like a less common use case; so we're fine with it being annoying
616
+ class UserEndpoint<
617
+ O extends Partial<RestGenerics> = {
618
+ schema: DefaultUser;
619
+ path: 'http\\://test.com/user/:id';
620
+ },
621
+ > extends MyEndpoint<
622
+ Defaults<O, { schema: DefaultUser; path: 'http\\://test.com/user/:id' }>
623
+ > {
624
+ constructor({
625
+ path = 'http\\://test.com/user/:id',
626
+ ...options
627
+ }: Readonly<O & { name: string }>) {
628
+ super({ path, ...options } as any);
629
+ }
630
+ }
631
+
632
+ const getUser = new UserEndpoint({
633
+ method: 'GET',
634
+ name: 'update',
635
+ });
636
+ const { result, waitForNextUpdate } = renderRestHook(() => {
637
+ return useSuspense(getUser, { id: 5 });
638
+ });
639
+ await waitForNextUpdate();
640
+
641
+ expect(result.current.username).toBe('bob');
642
+ // @ts-expect-error
643
+ expect(result.current.sdfsd).toBeUndefined();
644
+ expect(result.current.defaultUserExtra).toBe('yay');
645
+ expect(result.current.email).toBe('bob@gmail.com');
646
+
647
+ // @ts-expect-error
648
+ () => getUser({ group: 5 });
649
+ // @ts-expect-error
650
+ () => useSuspense(getUser, { group: 5 });
651
+ // @ts-expect-error
652
+ () => useSuspense(getUser);
653
+ });
654
+
655
+ it('should work with extends', async () => {
656
+ mynock.put('/6/user/5').reply(200, (uri, body: any) => ({
657
+ id: 5,
658
+ username: 'charles',
659
+ ...body,
660
+ }));
661
+
662
+ const updateUser = new MyEndpoint({
663
+ method: 'PUT',
664
+ path: 'http\\://test.com/user/:id',
665
+ name: 'update',
666
+ schema: User,
667
+ }).extend({
668
+ path: 'http\\://test.com/:group/user/:id',
669
+ body: 0 as Partial<User>,
670
+ });
671
+ expect(updateUser.additional).toBe(5);
672
+ expect(updateUser.method).toBe('PUT');
673
+ const response = await updateUser(
674
+ { group: '6', id: 5 },
675
+ { email: 'micky@gmail.com' },
676
+ );
677
+ expect(response).toMatchInlineSnapshot(`
678
+ {
679
+ "email": "always@always.com",
680
+ "id": 5,
681
+ "username": "charles",
682
+ }
683
+ `);
684
+ });
685
+
686
+ it('should work with extends', async () => {
687
+ mynock.get('/6/user/5').reply(200, (uri, body: any) => ({
688
+ id: 5,
689
+ username2: 'charles',
690
+ ...body,
691
+ }));
692
+ class User2 extends Entity {
693
+ readonly id: number | undefined = undefined;
694
+ readonly username2: string = '';
695
+ readonly email: string = '';
696
+ readonly isAdmin: boolean = false;
697
+
698
+ pk() {
699
+ return this.id?.toString();
700
+ }
701
+ }
702
+
703
+ const getUserBase = new MyEndpoint({
704
+ method: 'GET',
705
+ path: 'http\\://test.com/user/:id',
706
+ name: 'getuser',
707
+ schema: User,
708
+ });
709
+ const getUser = getUserBase.extend({
710
+ path: 'http\\://test.com/:group/user/:id',
711
+ schema: User2,
712
+ });
713
+ getUserBase.body;
714
+ expect(getUserBase.name).toBe('getuser');
715
+ expect(getUserBase.extend({ method: 'GET' }).name).toBe('getuser');
716
+ expect(getUser.name).toBe('getuser');
717
+ expect(getUser.additional).toBe(5);
718
+ expect(getUser.method).toBe('GET');
719
+ const user = await getUser({ group: '6', id: 5 });
720
+ expect(user.username2).toBe('charles');
721
+ () => {
722
+ const a = useSuspense(getUser, { group: '6', id: 5 });
723
+ // @ts-expect-error
724
+ a.username;
725
+ };
726
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
727
+ // @ts-ignore
728
+ expect(user.username).toBeUndefined();
729
+ expect(user).toMatchInlineSnapshot(`
730
+ {
731
+ "id": 5,
732
+ "username2": "charles",
733
+ }
734
+ `);
735
+
736
+ const newBody = getUser.extend({
737
+ body: {} as { title: string },
738
+ });
739
+ () => newBody({ group: 'hi', id: 'what' }, { title: 'cool' });
740
+ // @ts-expect-error
741
+ () => newBody({ id: 'what' }, { title: 'cool' });
742
+ // @ts-expect-error
743
+ () => newBody({ title: 'cool' });
744
+ // @ts-expect-error
745
+ () => newBody({ group: 'hi', id: 'what' });
746
+ // @ts-expect-error
747
+ () => newBody({ group: 'hi', id: 'what' }, { sdfsd: 'cool' });
748
+
749
+ const bodyNoParams = newBody
750
+ .extend({
751
+ path: '/',
752
+ })
753
+ .extend({ body: {} as { happy: string } });
754
+ () => bodyNoParams({ happy: 'cool' });
755
+ // @ts-expect-error
756
+ () => bodyNoParams({ group: 'hi', id: 'what' }, { happy: 'cool' });
757
+ // @ts-expect-error
758
+ () => bodyNoParams({ group: 'hi', id: 'what' }, { title: 'cool' });
759
+ // @ts-expect-error
760
+ () => bodyNoParams({ sdfd: 'cool' });
761
+
762
+ const searchParams = getUser.extend({
763
+ path: 'http\\://test.com/:group/user/:id',
764
+ searchParams: {} as { isAdmin?: boolean; sort: 'asc' | 'desc' },
765
+ });
766
+ () => searchParams({ group: 'hi', id: 'what', sort: 'asc' });
767
+ () =>
768
+ searchParams({ group: 'hi', id: 'what', sort: 'asc', isAdmin: true });
769
+ // @ts-expect-error
770
+ () => searchParams({ group: 'hi', id: 'what', sort: 'abc' });
771
+ // @ts-expect-error
772
+ () => searchParams({ group: 'hi', id: 'what' });
773
+ // @ts-expect-error
774
+ () => searchParams.url({ group: 'hi', id: 'what' });
775
+ expect(
776
+ searchParams.url({
777
+ group: 'hi',
778
+ id: 'what',
779
+ sort: 'desc',
780
+ isAdmin: true,
781
+ }),
782
+ ).toMatchInlineSnapshot(
783
+ `"http://test.com/hi/user/what?isAdmin=true&sort=desc"`,
784
+ );
785
+
786
+ const searchParams2 = searchParams.extend({
787
+ searchParams: {} as { bigger: boolean },
788
+ });
789
+ () => searchParams2({ group: 'hi', id: 'what', bigger: true });
790
+ () => searchParams2({ group: 'hi', id: 'what', bigger: false });
791
+ // @ts-expect-error
792
+ () => searchParams2({ group: 'hi', id: 'what', bigger: 5 });
793
+ // @ts-expect-error
794
+ () => searchParams2({ group: 'hi', id: 'what', sort: 'asc' });
795
+ // @ts-expect-error
796
+ () => searchParams2({ group: 'hi', id: 'what' });
797
+ // @ts-expect-error
798
+ () => searchParams2.url({ group: 'hi', id: 'what' });
799
+ expect(
800
+ searchParams2.url({
801
+ group: 'hi',
802
+ id: 'what',
803
+ bigger: true,
804
+ }),
805
+ ).toMatchInlineSnapshot(`"http://test.com/hi/user/what?bigger=true"`);
806
+
807
+ const searchParams3 = getUserBase.extend({
808
+ searchParams: {} as { bigger: boolean },
809
+ });
810
+ () => searchParams3({ id: 'what', bigger: true });
811
+ () => searchParams3({ id: 'what', bigger: false });
812
+ // @ts-expect-error
813
+ () => searchParams3({ id: 'what', bigger: 5 });
814
+ // @ts-expect-error
815
+ () => searchParams3({ id: 'what', sort: 'asc' });
816
+ // @ts-expect-error
817
+ () => searchParams3({ id: 'what' });
818
+ // @ts-expect-error
819
+ () => searchParams3.url({ id: 'what' });
820
+ expect(
821
+ searchParams3.url({
822
+ id: 'what',
823
+ bigger: true,
824
+ }),
825
+ ).toMatchInlineSnapshot(`"http://test.com/user/what?bigger=true"`);
826
+
827
+ const searchParams4 = getUserBase
828
+ .extend({
829
+ path: '/users',
830
+ })
831
+ .extend({ searchParams: {} as { bigger?: boolean } | undefined });
832
+ () => searchParams4({ bigger: true });
833
+ () => searchParams4();
834
+ () => searchParams4({});
835
+ // @ts-expect-error
836
+ () => searchParams4({ id: 'what', bigger: false });
837
+ // @ts-expect-error
838
+ () => searchParams4({ bigger: 5 });
839
+ // @ts-expect-error
840
+ () => searchParams4({ id: 'what' });
841
+ // @ts-expect-error
842
+ () => searchParams4.url({ id: 'what' });
843
+ expect(
844
+ searchParams4.url({
845
+ bigger: true,
846
+ }),
847
+ ).toMatchInlineSnapshot(`"/users?bigger=true"`);
848
+ expect(searchParams4.url()).toMatchInlineSnapshot(`"/users"`);
849
+ });
850
+ });
851
+ it('extending with name should work', () => {
852
+ const endpoint = CoolerArticleResource.get.extend({ name: 'mything' });
853
+ const endpoint2 = CoolerArticleResource.get.extend({ path: '/:bob' });
854
+ expect(CoolerArticleResource.get.name).toMatchInlineSnapshot(
855
+ `"CoolerArticle.get"`,
856
+ );
857
+ expect(endpoint.name).toBe('mything');
858
+ expect(endpoint2.name).toMatchInlineSnapshot(`"CoolerArticle.get"`);
859
+ });
860
+ it('should infer default method when sideEffect is set', async () => {
861
+ const endpoint = new RestEndpoint({
862
+ sideEffect: true,
863
+ path: 'http\\://test.com/article-cooler',
864
+ body: 0 as any,
865
+ schema: CoolerArticle,
866
+ }).extend({ name: 'createarticle' });
867
+ expect(endpoint.method).toBe('POST');
868
+ const article = await endpoint(payload);
869
+ expect(article).toMatchInlineSnapshot(`
870
+ {
871
+ "content": "whatever",
872
+ "id": 1,
873
+ "tags": [
874
+ "a",
875
+ "best",
876
+ "react",
877
+ ],
878
+ "title": "hi ho",
879
+ }
880
+ `);
881
+ });
882
+
883
+ describe('body type setting', () => {
884
+ it('should work in constructors', () => {
885
+ interface TodoInterface {
886
+ title: string;
887
+ completed: boolean;
888
+ }
889
+ const update = new RestEndpoint({
890
+ path: '/:id',
891
+ method: 'POST',
892
+ body: {} as TodoInterface,
893
+ });
894
+ () => update({ id: 5 }, { title: 'updated', completed: true });
895
+ // @ts-expect-error
896
+ () => update({ id: 5 });
897
+ // @ts-expect-error
898
+ () => update({ id: 5 }, { title: 5, completed: true });
899
+ // @ts-expect-error
900
+ () => update({ id: 5 }, { completed: true });
901
+ });
902
+ });
903
+
904
+ describe('process() return type setting', () => {
905
+ const getArticle = new RestEndpoint({
906
+ path: 'http\\://test.com/article-cooler/:id',
907
+ process(value): CoolerArticle {
908
+ return value;
909
+ },
910
+ });
911
+
912
+ it('should work with constructors', async () => {
913
+ const article = await getArticle({ id: '5' });
914
+ article.author;
915
+ // @ts-expect-error
916
+ article.asdf;
917
+ () => useSuspense(getArticle, { id: '5' }).content;
918
+ // @ts-expect-error
919
+ () => useSuspense(getArticle, { id: '5' }).asdf;
920
+ });
921
+
922
+ it('should set on .extend()', async () => {
923
+ const getExtends = new RestEndpoint({
924
+ path: 'http\\://test.com/article-cooler/:id',
925
+ }).extend({
926
+ process(value: any): CoolerArticle {
927
+ return value;
928
+ },
929
+ });
930
+ const ex = await getExtends({ id: '5' });
931
+ ex.author;
932
+ // @ts-expect-error
933
+ ex.asdf;
934
+ () => useSuspense(getExtends, { id: '5' }).content;
935
+ // @ts-expect-error
936
+ () => useSuspense(getExtends, { id: '5' }).asdf;
937
+ });
938
+
939
+ it('should override existing type on .extend()', async () => {
940
+ const getOverride = getArticle.extend({
941
+ process(value: any, param: any): { asdf: string } {
942
+ return value;
943
+ },
944
+ });
945
+ const ov = await getOverride({ id: '5' });
946
+ ov.asdf;
947
+ // @ts-expect-error
948
+ ov.author;
949
+ () => useSuspense(getOverride, { id: '5' }).asdf;
950
+ // @ts-expect-error
951
+ () => useSuspense(getOverride, { id: '5' }).content;
952
+ });
953
+
954
+ it('should maintain existing type on .extend() when not specified', async () => {
955
+ const getOverride = getArticle.extend({
956
+ dataExpiryLength: 7,
957
+ });
958
+ const ov = await getOverride({ id: '5' });
959
+ ov.author;
960
+ // @ts-expect-error
961
+ ov.asdf;
962
+ });
963
+
964
+ it('should maintain existing type on .extend() when process is not supplied but path is', async () => {
965
+ const getOverride = getArticle.extend({
966
+ path: '/:a/:b',
967
+ });
968
+ async () => {
969
+ const ov = await getOverride({ a: '5', b: '7' });
970
+ ov.author;
971
+ // @ts-expect-error
972
+ ov.asdf;
973
+ };
974
+ });
975
+
976
+ it('should override existing type on .extend() when path is also supplied', async () => {
977
+ const getOverride = getArticle.extend({
978
+ path: '/:a/:b',
979
+ process(value: any, param: any): { asdf: string } {
980
+ return value;
981
+ },
982
+ });
983
+ async () => {
984
+ const ov = await getOverride({ a: '5', b: '7' });
985
+ ov.asdf;
986
+ // @ts-expect-error
987
+ ov.author;
988
+ };
989
+ () => useSuspense(getOverride, { a: '5', b: '7' }).asdf;
990
+ // @ts-expect-error
991
+ () => useSuspense(getOverride, { a: '5', b: '7' }).content;
992
+ });
993
+ });
994
+
995
+ it('extend options should match function of path set', () => {
996
+ const endpoint = new RestEndpoint({
997
+ sideEffect: true,
998
+ path: 'http\\://test.com/article-cooler/:id',
999
+ body: 0 as any,
1000
+ schema: CoolerArticle,
1001
+ getOptimisticResponse(snap, params, body) {
1002
+ params.id;
1003
+ // @ts-expect-error
1004
+ params.two;
1005
+
1006
+ body.hi;
1007
+ },
1008
+ }).extend({
1009
+ path: '/:group/next/:two',
1010
+ body: undefined,
1011
+ getOptimisticResponse(snap, params) {
1012
+ params.two;
1013
+ params.group;
1014
+ // @ts-expect-error
1015
+ params.id;
1016
+ },
1017
+ });
1018
+ });
1019
+ });
1020
+
1021
+ describe('RestEndpoint.fetch()', () => {
1022
+ const id = 5;
1023
+ const idHtml = 6;
1024
+ const idNoContent = 7;
1025
+ const payload = {
1026
+ id,
1027
+ title: 'happy',
1028
+ author: User.fromJS({ id: 5 }),
1029
+ };
1030
+ const putResponseBody = {
1031
+ id,
1032
+ title: 'happy',
1033
+ completed: true,
1034
+ };
1035
+ const patchPayload = {
1036
+ title: 'happy',
1037
+ };
1038
+ const patchResponseBody = {
1039
+ id,
1040
+ title: 'happy',
1041
+ completed: false,
1042
+ };
1043
+
1044
+ beforeEach(() => {
1045
+ nock(/.*/)
1046
+ .defaultReplyHeaders({
1047
+ 'Access-Control-Allow-Origin': '*',
1048
+ 'Content-Type': 'application/json',
1049
+ })
1050
+ .options(/.*/)
1051
+ .reply(200)
1052
+ .get(`/article-cooler/${payload.id}`)
1053
+ .reply(200, payload)
1054
+ .get(`/article-cooler/${idHtml}`)
1055
+ .reply(200, '<body>this is html</body>')
1056
+ .get(`/article-cooler/${idNoContent}`)
1057
+ .reply(204, '')
1058
+ .post('/article-cooler')
1059
+ .reply((uri, requestBody) => [
1060
+ 201,
1061
+ requestBody,
1062
+ { 'content-type': 'application/json' },
1063
+ ])
1064
+ .put('/article-cooler/5')
1065
+ .reply((uri, requestBody) => {
1066
+ let body = requestBody as any;
1067
+ if (typeof requestBody === 'string') {
1068
+ body = JSON.parse(requestBody);
1069
+ }
1070
+ for (const key of Object.keys(CoolerArticle.fromJS({}))) {
1071
+ if (key !== 'id' && !(key in body)) {
1072
+ return [400, {}, { 'content-type': 'application/json' }];
1073
+ }
1074
+ }
1075
+ return [200, putResponseBody, { 'content-type': 'application/json' }];
1076
+ })
1077
+ .patch('/article-cooler/5')
1078
+ .reply(() => [
1079
+ 200,
1080
+ patchResponseBody,
1081
+ { 'content-type': 'application/json' },
1082
+ ])
1083
+ .intercept('/article-cooler/5', 'DELETE')
1084
+ .reply(200, {});
1085
+ });
1086
+
1087
+ afterEach(() => {
1088
+ nock.cleanAll();
1089
+ });
1090
+
1091
+ it('should GET', async () => {
1092
+ const article = await CoolerArticleResource.get({
1093
+ id: payload.id,
1094
+ });
1095
+ expect(article).toBeDefined();
1096
+ if (!article) {
1097
+ throw new Error('ahh');
1098
+ }
1099
+ expect(article.title).toBe(payload.title);
1100
+ });
1101
+
1102
+ it('should POST', async () => {
1103
+ const payload2 = { id: 20, content: 'better task' };
1104
+ const article = await CoolerArticleResource.create(payload2);
1105
+ expect(article).toMatchObject(payload2);
1106
+ });
1107
+
1108
+ it('should PUT with multipart form data', async () => {
1109
+ const payload2 = { id: 500, content: 'another' };
1110
+ let lastRequest: any;
1111
+ nock(/.*/)
1112
+ .defaultReplyHeaders({
1113
+ 'Access-Control-Allow-Origin': '*',
1114
+ })
1115
+ .put('/article-cooler/500')
1116
+ .reply(function (uri, requestBody) {
1117
+ lastRequest = this.req;
1118
+ return [201, payload2, { 'Content-Type': 'application/json' }];
1119
+ });
1120
+ const newPhoto = new Blob();
1121
+ const body = new FormData();
1122
+ body.append('photo', newPhoto);
1123
+
1124
+ const article = await CoolerArticleResource.update.extend({
1125
+ path: CoolerArticleResource.update.path,
1126
+ body: new FormData(),
1127
+ })({ id: '500' }, body);
1128
+ expect(lastRequest.headers['content-type']).toContain(
1129
+ 'multipart/form-data',
1130
+ );
1131
+ expect(article).toMatchObject(payload2);
1132
+ });
1133
+
1134
+ it('should DELETE', async () => {
1135
+ const res = await CoolerArticleResource.delete({
1136
+ id: payload.id,
1137
+ });
1138
+ expect(res).toEqual({ id });
1139
+ });
1140
+
1141
+ it('should PUT', async () => {
1142
+ const response = await CoolerArticleResource.update(
1143
+ { id: payload.id },
1144
+ { ...CoolerArticle.fromJS(payload) },
1145
+ );
1146
+ expect(response).toEqual(putResponseBody);
1147
+ });
1148
+
1149
+ it('should PATCH', async () => {
1150
+ const response = await CoolerArticleResource.partialUpdate(
1151
+ { id },
1152
+ patchPayload,
1153
+ );
1154
+ expect(response).toEqual(patchResponseBody);
1155
+ });
1156
+
1157
+ it('should throw if response is not json', async () => {
1158
+ let error: any;
1159
+ try {
1160
+ await CoolerArticleResource.get({ id: idHtml });
1161
+ } catch (e) {
1162
+ error = e;
1163
+ }
1164
+ expect(error).toBeDefined();
1165
+ // This is very weird, but we're forced to use node-fetch for react native
1166
+ // node-fetch doesn't handle errors consistently with normal fetch implementations, so this won't work
1167
+ // react-native itself should match this correctly however.
1168
+ if (typeof window !== 'undefined') expect(error.status).toBe(400);
1169
+ });
1170
+
1171
+ it('should throw if network is down', async () => {
1172
+ const oldError = console.error;
1173
+ console.error = () => {};
1174
+
1175
+ const id = 10;
1176
+ nock(/.*/)
1177
+ .defaultReplyHeaders({
1178
+ 'Access-Control-Allow-Origin': '*',
1179
+ 'Content-Type': 'application/json',
1180
+ })
1181
+ .get(`/article-cooler/${id}`)
1182
+ .replyWithError(new TypeError('Network Down'));
1183
+
1184
+ let error: any;
1185
+ try {
1186
+ await CoolerArticleResource.get({ id });
1187
+ } catch (e) {
1188
+ error = e;
1189
+ }
1190
+ expect(error).toBeDefined();
1191
+ expect(error.status).toBe(500);
1192
+
1193
+ // eslint-disable-next-line require-atomic-updates
1194
+ console.error = oldError;
1195
+ });
1196
+
1197
+ it('should return raw response if status is 204 No Content', async () => {
1198
+ const res = await CoolerArticleResource.get({ id: idNoContent });
1199
+ expect(res).toBe(null);
1200
+ });
1201
+
1202
+ it('should reject if content-type is not json with schema', async () => {
1203
+ const id = 8;
1204
+ const text = '<body>this is html</body>';
1205
+ nock(/.*/)
1206
+ .defaultReplyHeaders({
1207
+ 'Access-Control-Allow-Origin': '*',
1208
+ 'Content-Type': 'application/json',
1209
+ })
1210
+ .get(`/article-cooler/${id}`)
1211
+ .reply(200, text, { 'content-type': 'html/text' });
1212
+
1213
+ await expect(
1214
+ async () => await CoolerArticleResource.get({ id }),
1215
+ ).rejects.toMatchSnapshot();
1216
+ });
1217
+
1218
+ it('should reject if content-type does not exist with schema', async () => {
1219
+ const id = 10;
1220
+ const text = '<body>this is html</body>';
1221
+ nock(/.*/)
1222
+ .defaultReplyHeaders({
1223
+ 'Access-Control-Allow-Origin': '*',
1224
+ })
1225
+ .get(`/article-cooler/${id}`)
1226
+ .reply(200, text, {});
1227
+
1228
+ await expect(
1229
+ async () => await CoolerArticleResource.get({ id }),
1230
+ ).rejects.toMatchSnapshot();
1231
+ });
1232
+
1233
+ it('should return text if content-type is not json with no schema', async () => {
1234
+ const id = 8;
1235
+ const text = '<body>this is html</body>';
1236
+ nock(/.*/)
1237
+ .defaultReplyHeaders({
1238
+ 'Access-Control-Allow-Origin': '*',
1239
+ 'Content-Type': 'application/json',
1240
+ })
1241
+ .get(`/article-cooler/${id}`)
1242
+ .reply(200, text, { 'content-type': 'html/text' });
1243
+
1244
+ const res = await CoolerArticleResource.get.extend({ schema: undefined })({
1245
+ id,
1246
+ });
1247
+ expect(res).toBe('<body>this is html</body>');
1248
+ });
1249
+
1250
+ it('should return text if content-type does not exist with no schema', async () => {
1251
+ const id = 10;
1252
+ const text = '<body>this is html</body>';
1253
+ nock(/.*/)
1254
+ .defaultReplyHeaders({
1255
+ 'Access-Control-Allow-Origin': '*',
1256
+ })
1257
+ .get(`/article-cooler/${id}`)
1258
+ .reply(200, text, {});
1259
+
1260
+ const res = await CoolerArticleResource.get.extend({ schema: undefined })({
1261
+ id,
1262
+ });
1263
+ expect(res).toBe(text);
1264
+ });
1265
+
1266
+ it('should reject with custom message if content type is set but json parsable', async () => {
1267
+ const id = 8;
1268
+ nock(/.*/)
1269
+ .defaultReplyHeaders({
1270
+ 'Access-Control-Allow-Origin': '*',
1271
+ 'Content-Type': 'text',
1272
+ })
1273
+ .get(`/article-cooler/${id}`)
1274
+ .reply(200, { id, title: 'hi' }, {});
1275
+
1276
+ await expect(
1277
+ async () => await CoolerArticleResource.get({ id }),
1278
+ ).rejects.toMatchSnapshot();
1279
+ });
1280
+
1281
+ it('should still work with empty content-type', async () => {
1282
+ const id = 8;
1283
+ nock(/.*/)
1284
+ .defaultReplyHeaders({
1285
+ 'Access-Control-Allow-Origin': '*',
1286
+ })
1287
+ .get(`/article-cooler/${id}`)
1288
+ .reply(200, { id, title: 'hi' });
1289
+
1290
+ const res = await CoolerArticleResource.get({
1291
+ id,
1292
+ });
1293
+ expect(res).toEqual({ id, title: 'hi' });
1294
+ });
1295
+ });
1296
+ const proto = Object.prototype;
1297
+ const gpo = Object.getPrototypeOf;
1298
+
1299
+ function isPojo(obj: unknown): obj is Record<string, any> {
1300
+ if (obj === null || typeof obj !== 'object') {
1301
+ return false;
1302
+ }
1303
+ return gpo(obj) === proto;
1304
+ }