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