@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,197 @@
1
+ import { useController, useSuspense } from '@data-client/react';
2
+ import { CacheProvider } from '@data-client/react';
3
+ import { CoolerArticleResource, CoolerArticle, User } from '__tests__/new';
4
+ import nock from 'nock';
5
+
6
+ import { makeRenderRestHook } from '../../../test';
7
+ import hookifyResource from '../hookifyResource';
8
+
9
+ const CoolerArticleHookResource = hookifyResource(
10
+ CoolerArticleResource,
11
+ () => ({}),
12
+ );
13
+
14
+ function onError(e: any) {
15
+ e.preventDefault();
16
+ }
17
+ beforeEach(() => {
18
+ if (typeof addEventListener === 'function')
19
+ addEventListener('error', onError);
20
+ });
21
+ afterEach(() => {
22
+ if (typeof removeEventListener === 'function')
23
+ removeEventListener('error', onError);
24
+ });
25
+
26
+ describe('hookifyResource()', () => {
27
+ beforeAll(() => {
28
+ nock(/.*/)
29
+ .persist()
30
+ .defaultReplyHeaders({
31
+ 'Access-Control-Allow-Origin': '*',
32
+ 'Content-Type': 'application/json',
33
+ })
34
+ .options(/.*/)
35
+ .reply(200);
36
+ });
37
+
38
+ afterAll(() => {
39
+ nock.cleanAll();
40
+ });
41
+
42
+ describe('HookableResource endpoints', () => {
43
+ const id = 5;
44
+ const idHtml = 6;
45
+ const idNoContent = 7;
46
+ const payload = {
47
+ id,
48
+ title: 'happy',
49
+ author: User.fromJS({ id: 5 }),
50
+ };
51
+ const putResponseBody = {
52
+ id,
53
+ title: 'happy',
54
+ completed: true,
55
+ };
56
+ const patchPayload = {
57
+ title: 'happy',
58
+ };
59
+ const patchResponseBody = {
60
+ id,
61
+ title: 'happy',
62
+ completed: false,
63
+ };
64
+
65
+ let renderRestHook: ReturnType<typeof makeRenderRestHook>;
66
+
67
+ beforeEach(() => {
68
+ renderRestHook = makeRenderRestHook(CacheProvider);
69
+
70
+ nock(/.*/)
71
+ .defaultReplyHeaders({
72
+ 'Access-Control-Allow-Origin': '*',
73
+ 'Content-Type': 'application/json',
74
+ })
75
+ .get(`/article-cooler/${payload.id}`)
76
+ .reply(200, payload)
77
+ .get(`/article-cooler`)
78
+ .reply(200, [payload])
79
+ .get(`/article-cooler/${idHtml}`)
80
+ .reply(200, '<body>this is html</body>')
81
+ .get(`/article-cooler/${idNoContent}`)
82
+ .reply(204, '')
83
+ .post('/article-cooler')
84
+ .reply((uri, requestBody) => [
85
+ 201,
86
+ requestBody,
87
+ { 'content-type': 'application/json' },
88
+ ])
89
+ .put('/article-cooler/5')
90
+ .reply((uri, requestBody) => {
91
+ let body = requestBody as any;
92
+ if (typeof requestBody === 'string') {
93
+ body = JSON.parse(requestBody);
94
+ }
95
+ for (const key of Object.keys(CoolerArticle.fromJS({}))) {
96
+ if (key !== 'id' && !(key in body)) {
97
+ return [400, {}, { 'content-type': 'application/json' }];
98
+ }
99
+ }
100
+ return [200, putResponseBody, { 'content-type': 'application/json' }];
101
+ })
102
+ .patch('/article-cooler/5')
103
+ .reply(() => [
104
+ 200,
105
+ patchResponseBody,
106
+ { 'content-type': 'application/json' },
107
+ ])
108
+ .intercept('/article-cooler/5', 'DELETE')
109
+ .reply(200, {});
110
+ });
111
+
112
+ it('useDetail', async () => {
113
+ const { result, waitForNextUpdate } = renderRestHook(() =>
114
+ useSuspense(CoolerArticleHookResource.useGet(), { id: payload.id }),
115
+ );
116
+ await waitForNextUpdate();
117
+ expect(result.current).toBeDefined();
118
+ expect(result.current.title).toBe(payload.title);
119
+ });
120
+
121
+ it('useList', async () => {
122
+ const { result, waitForNextUpdate } = renderRestHook(() =>
123
+ useSuspense(CoolerArticleHookResource.useGetList()),
124
+ );
125
+ await waitForNextUpdate();
126
+ expect(result.current).toBeDefined();
127
+ });
128
+
129
+ it('useCreate', async () => {
130
+ const { result } = renderRestHook(() => ({
131
+ endpoint: CoolerArticleHookResource.useCreate(),
132
+ fetch: useController().fetch,
133
+ }));
134
+ const payload2 = { id: 20, content: 'better task' };
135
+ const article = await result.current.fetch(
136
+ result.current.endpoint,
137
+ payload2,
138
+ );
139
+ // @ts-expect-error
140
+ () => result.current.fetch(result.current.endpoint, {}, payload2);
141
+ expect(article).toMatchObject(payload2);
142
+ });
143
+
144
+ it('useDelete', async () => {
145
+ const { result } = renderRestHook(() => ({
146
+ endpoint: CoolerArticleHookResource.useDelete(),
147
+ fetch: useController().fetch,
148
+ }));
149
+
150
+ const res = await result.current.fetch(result.current.endpoint, {
151
+ id: payload.id,
152
+ });
153
+ expect(res).toEqual({ id: 5 });
154
+ });
155
+
156
+ it('useUpdate', async () => {
157
+ const { result } = renderRestHook(() => ({
158
+ endpoint: CoolerArticleHookResource.useUpdate(),
159
+ fetch: useController().fetch,
160
+ }));
161
+
162
+ const res = await result.current.fetch(
163
+ result.current.endpoint,
164
+ { id: payload.id },
165
+ {
166
+ ...CoolerArticle.fromJS(payload),
167
+ },
168
+ );
169
+ expect(res).toEqual(putResponseBody);
170
+ });
171
+
172
+ it('usePartialUpdate', async () => {
173
+ const { result } = renderRestHook(() => ({
174
+ endpoint: CoolerArticleHookResource.usePartialUpdate(),
175
+ fetch: useController().fetch,
176
+ }));
177
+
178
+ const res = await result.current.fetch(
179
+ result.current.endpoint,
180
+ { id },
181
+ patchPayload,
182
+ );
183
+ expect(res).toEqual(patchResponseBody);
184
+ });
185
+
186
+ it('should use useFetchInit if defined (in endpoint method)', async () => {
187
+ const useFetchInit = jest.fn(() => ({}));
188
+ const FetchResource = hookifyResource(
189
+ CoolerArticleResource,
190
+ useFetchInit,
191
+ );
192
+ const articleDetail = FetchResource.useGet();
193
+ expect(articleDetail).toBeDefined();
194
+ expect(useFetchInit.mock.calls.length).toBeGreaterThan(0);
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,309 @@
1
+ import { Entity, schema } from '@data-client/endpoint';
2
+ import { User } from '__tests__/new';
3
+
4
+ import createResource from '../createResource';
5
+ import {
6
+ KeysToArgs,
7
+ PathArgs,
8
+ PathArgsAndSearch,
9
+ PathKeys,
10
+ ShortenPath,
11
+ } from '../pathTypes';
12
+ import RestEndpoint, { GetEndpoint, NewGetEndpoint } from '../RestEndpoint';
13
+ import { RequiredKeys } from '../utiltypes';
14
+
15
+ describe('PathArgs', () => {
16
+ it('should infer types', () => {
17
+ type C =
18
+ 'http\\://test.com/groups/:group?/\\:blah/users/:id?:extra?/:next\\?bob/:last';
19
+ function A(args: PathArgs<C>) {}
20
+ // @ts-expect-error
21
+ () => A({});
22
+ () => A({ next: 'hi', last: 'ho' });
23
+ // @ts-expect-error
24
+ () => A({ next: 'hi', last: 'ho', doesnotexist: 'hi' });
25
+ // @ts-expect-error
26
+ () => A({ next: 'hi', last: 'ho', blah: 'hi' });
27
+ () => A({ next: 'hi', last: 'ho', id: '5', group: 'whatever' });
28
+ () => A({ next: 'hi', last: 'ho', extra: '5', group: 'whatever' });
29
+ });
30
+
31
+ it('should infer types2', () => {
32
+ type C = 'http\\://test.com/article-cooler/:id?:title?';
33
+ function A(args: PathArgs<C>) {}
34
+ () => A({});
35
+ () => A({ id: 'ho' });
36
+ () => A({ title: 'ho' });
37
+ () => A({ id: 'ho', title: 'ho' });
38
+ // @ts-expect-error
39
+ () => A({ idasd: 'ho' });
40
+ // @ts-expect-error
41
+ () => A({ next: 'hi', title: 'ho', id: 'hi' });
42
+ // @ts-expect-error
43
+ () => A(5);
44
+ });
45
+
46
+ it('should infer types with other characters to stop', () => {
47
+ type C = 'http\\://test.com/search/issues?q=:q?%20repo\\::owner/:repo';
48
+ function A(args: PathArgs<C>) {}
49
+ () => A({ owner: 'data-client', repo: 'data-client' });
50
+ () => A({ owner: 'data-client', repo: 'data-client', q: 'is:issue' });
51
+ // @ts-expect-error
52
+ () => A({ idasd: 'ho' });
53
+ // @ts-expect-error
54
+ () => A({ owner: 'data-client', repo: 'data-client', extra: 'is:issue' });
55
+ // @ts-expect-error
56
+ () => A({ owner: 'data-client' });
57
+ // @ts-expect-error
58
+ () => A(5);
59
+ });
60
+
61
+ it('should be flexible for string type', () => {
62
+ class Parent {
63
+ constructor() {}
64
+ A(args: PathArgs<string>) {
65
+ const b = args['hi'];
66
+ }
67
+ }
68
+ class Child extends Parent {
69
+ A(args: { item: string }) {}
70
+ }
71
+ const thing = new Parent();
72
+ () => thing.A({});
73
+ () => thing.A({ next: 'hi', last: 'ho' });
74
+ const thing2 = new Child();
75
+ //@ts-expect-error
76
+ () => thing2.A({});
77
+ () => thing2.A({ item: 'win' });
78
+ });
79
+
80
+ it('works with no arguments', () => {
81
+ const a: GetEndpoint<PathArgs<'http\\://test.com/article-cooler/'>> =
82
+ 0 as any;
83
+ () => a();
84
+ //@ts-expect-error
85
+ () => a({ page: '5' });
86
+ //@ts-expect-error
87
+ () => a({ sdf: '5' });
88
+ //@ts-expect-error
89
+ () => a(5);
90
+ });
91
+
92
+ it('works when combining with known types', () => {
93
+ const a: GetEndpoint<
94
+ PathArgs<ShortenPath<'/hi'>> & { page: string | number }
95
+ > = 0 as any;
96
+ () => a({ page: '5' });
97
+ //@ts-expect-error
98
+ () => a({ sdf: '5' });
99
+ //@ts-expect-error
100
+ () => a({ page: '5' }, { hi: '5' });
101
+ //@ts-expect-error
102
+ () => a();
103
+ });
104
+
105
+ it('works with unions of non-intersecting types', () => {
106
+ const a: GetEndpoint<{ id: string | number } | { title: string | number }> =
107
+ 0 as any;
108
+ () => a({ id: '5' });
109
+ () => a({ title: '5' });
110
+ // @ts-expect-error
111
+ () => a({ id: '5', title: '5' });
112
+ // @ts-expect-error
113
+ () => a({ id: '5', sdfds: '5' });
114
+ //@ts-expect-error
115
+ () => a({ sdf: '5' });
116
+ //@ts-expect-error
117
+ () => a({ page: '5' }, { hi: '5' });
118
+ //@ts-expect-error
119
+ () => a();
120
+ });
121
+
122
+ it('works with optional positional args', () => {
123
+ const a: GetEndpoint<undefined | { cursor?: string | number }> = 0 as any;
124
+ () => a();
125
+ () => a({});
126
+ () => a({ cursor: 5 });
127
+ // @ts-expect-error
128
+ () => a({ id: '5' });
129
+ //@ts-expect-error
130
+ () => a({ cursor: '5' }, { cursor: '5' });
131
+ });
132
+
133
+ it('searchParams', () => {
134
+ const a: GetEndpoint<undefined | { cursor?: string | number }> = 0 as any;
135
+ () => a();
136
+ () => a({});
137
+ () => a({ cursor: 5 });
138
+ // @ts-expect-error
139
+ () => a({ id: '5' });
140
+ //@ts-expect-error
141
+ () => a({ cursor: '5' }, { cursor: '5' });
142
+ });
143
+
144
+ describe('PathArgsAndSearch', () => {
145
+ it('works with required path member', () => {
146
+ const a: GetEndpoint<
147
+ PathArgsAndSearch<'http\\://test.com/groups/:group/users/'>
148
+ > = 0 as any;
149
+ () => a({ group: '5' });
150
+ () => a({ group: '5', sdf: '5' });
151
+ //@ts-expect-error
152
+ () => a({ asdf: '5' });
153
+ //@ts-expect-error
154
+ () => a({});
155
+ //@ts-expect-error
156
+ () => a({ page: '5' }, { hi: '5' });
157
+ //@ts-expect-error
158
+ () => a();
159
+ });
160
+ it('works with no path members', () => {
161
+ const a: NewGetEndpoint<{
162
+ path: 'http\\://test.com/groups';
163
+ searchParams: Record<string, number | string | boolean> | undefined;
164
+ }> = 0 as any;
165
+ /*const a: GetEndpoint<PathArgsAndSearch<'http\\://test.com/groups/'>> =
166
+ 0 as any;*/
167
+ () => a({ group: '5' });
168
+ () => a({ group: '5', sdf: '5' });
169
+ () => a();
170
+ () => a({ asdf: '5' });
171
+ () => a({});
172
+ //@ts-expect-error
173
+ () => a({ page: '5' }, { hi: '5' });
174
+
175
+ () => {
176
+ const b = a.extend({
177
+ searchParams: {} as { userId?: number | string } | undefined,
178
+ });
179
+ b({ userId: '5' });
180
+ b();
181
+ // @ts-expect-error
182
+ b({ sdf: '5' });
183
+ };
184
+ });
185
+ });
186
+ });
187
+
188
+ describe('RequiredKeys', () => {
189
+ type Y = {
190
+ opt?: string;
191
+ bob: string;
192
+ charles: number;
193
+ [k: string]: string | number | undefined;
194
+ };
195
+ const a: Record<RequiredKeys<Y>, any> = 5 as any;
196
+ () => a.bob;
197
+ () => a.charles;
198
+ // @ts-expect-error
199
+ () => a.opt;
200
+ // @ts-expect-error
201
+ () => a.sdfsdf;
202
+ });
203
+
204
+ it('RestEndpoint construct and extend with typed options', () => {
205
+ new RestEndpoint({
206
+ path: '/todos/',
207
+ getOptimisticResponse(snap, body) {
208
+ return body;
209
+ },
210
+ schema: User,
211
+ method: 'POST',
212
+ });
213
+ // variable/unknown number of args
214
+ new RestEndpoint({
215
+ path: '/todos/',
216
+ searchParams: {} as { userId?: string | number } | undefined,
217
+ getOptimisticResponse(snap, ...args) {
218
+ return args[args.length - 1];
219
+ },
220
+ schema: User,
221
+ method: 'POST',
222
+ });
223
+ new RestEndpoint({
224
+ path: '/todos/:id',
225
+ searchParams: {} as { userId?: string | number } | undefined,
226
+ getOptimisticResponse(snap, params, body) {
227
+ return body;
228
+ },
229
+ schema: User,
230
+ method: 'POST',
231
+ });
232
+ new RestEndpoint({
233
+ path: '/todos/:id',
234
+ searchParams: {} as { userId?: string | number } | undefined,
235
+ getOptimisticResponse(snap, params, body) {
236
+ return body;
237
+ },
238
+ schema: User,
239
+ method: 'POST',
240
+ });
241
+
242
+ const nopath = new RestEndpoint({
243
+ path: '/todos/',
244
+ schema: User,
245
+ method: 'POST',
246
+ });
247
+ const somepath = new RestEndpoint({
248
+ path: '/todos/:id',
249
+ schema: User,
250
+ method: 'POST',
251
+ });
252
+
253
+ nopath.extend({
254
+ getOptimisticResponse(snap, body) {
255
+ return body;
256
+ },
257
+ });
258
+ nopath.extend({
259
+ searchParams: {} as { userId?: string | number } | undefined,
260
+ getOptimisticResponse(snap, ...args) {
261
+ return args[args.length - 1];
262
+ },
263
+ });
264
+ somepath.extend({
265
+ searchParams: {} as { userId?: string | number } | undefined,
266
+ getOptimisticResponse(snap, args, body) {
267
+ return body;
268
+ },
269
+ });
270
+ });
271
+
272
+ it('should customize resources', () => {
273
+ class Todo extends Entity {
274
+ id = '';
275
+ userId = 0;
276
+ title = '';
277
+ completed = false;
278
+
279
+ static key = 'Todo';
280
+ pk() {
281
+ return this.id;
282
+ }
283
+ }
284
+
285
+ const TodoResourceBase = createResource({
286
+ path: '/todos/:id',
287
+ schema: Todo,
288
+ });
289
+ TodoResourceBase.create.extend({
290
+ searchParams: {} as { userId?: string | number } | undefined,
291
+ getOptimisticResponse(snap, ...args) {
292
+ return args[args.length - 1];
293
+ },
294
+ });
295
+ TodoResourceBase.create
296
+ .extend({
297
+ schema: new schema.Collection([Todo], {
298
+ argsKey(...args) {
299
+ return { a: 5 };
300
+ },
301
+ }),
302
+ })
303
+ .extend({
304
+ searchParams: {} as { userId?: string | number } | undefined,
305
+ getOptimisticResponse(snap, ...args) {
306
+ return args[args.length - 1];
307
+ },
308
+ });
309
+ });
@@ -0,0 +1,172 @@
1
+ import {
2
+ AbortOptimistic,
3
+ schema,
4
+ SnapshotInterface,
5
+ } from '@data-client/endpoint';
6
+ import type {
7
+ Schema,
8
+ Denormalize,
9
+ EndpointExtraOptions,
10
+ } from '@data-client/endpoint';
11
+
12
+ import { PathArgs, ShortenPath } from './pathTypes.js';
13
+ import RestEndpoint, {
14
+ NewGetEndpoint,
15
+ NewMutateEndpoint,
16
+ RestInstance,
17
+ RestTypeNoBody,
18
+ } from './RestEndpoint.js';
19
+ import { shortenPath } from './RestHelpers.js';
20
+
21
+ const { Delete } = schema;
22
+
23
+ /** Creates collection of Endpoints for common operations on a given data/schema.
24
+ *
25
+ * @see https://resthooks.io/rest/api/createResource
26
+ */
27
+ export default function createResource<U extends string, S extends Schema>({
28
+ path,
29
+ schema,
30
+ Endpoint = RestEndpoint,
31
+ optimistic,
32
+ ...extraOptions
33
+ }: {
34
+ readonly path: U;
35
+ readonly schema: S;
36
+ readonly Endpoint?: typeof RestEndpoint;
37
+ urlPrefix?: string;
38
+ optimistic?: boolean;
39
+ } & EndpointExtraOptions): Resource<U, S> {
40
+ const shortenedPath = shortenPath(path);
41
+ const getName = (name: string) => `${(schema as any)?.name}.${name}`;
42
+ const extraMutateOptions = { ...extraOptions };
43
+ const extraPartialOptions = { ...extraOptions };
44
+ const get: NewGetEndpoint<{ path: U }, S> = new Endpoint({
45
+ ...extraOptions,
46
+ path,
47
+ schema,
48
+ name: getName('get'),
49
+ });
50
+ if (optimistic) {
51
+ (extraMutateOptions as any).getOptimisticResponse = optimisticUpdate;
52
+ (extraPartialOptions as any).getOptimisticResponse = optimisticPartial(get);
53
+ }
54
+ const getList = new Endpoint({
55
+ ...extraOptions,
56
+ path: shortenedPath,
57
+ schema: [schema],
58
+ name: getName('getList'),
59
+ });
60
+ return {
61
+ get,
62
+ getList,
63
+ create: new Endpoint({
64
+ ...extraMutateOptions,
65
+ path: shortenedPath,
66
+ schema,
67
+ method: 'POST',
68
+ name: getName('create'),
69
+ }),
70
+ update: new Endpoint({
71
+ ...extraMutateOptions,
72
+ path,
73
+ schema,
74
+ method: 'PUT',
75
+ name: getName('update'),
76
+ }),
77
+ partialUpdate: new Endpoint({
78
+ ...extraPartialOptions,
79
+ path,
80
+ schema,
81
+ method: 'PATCH',
82
+ name: getName('partialUpdate'),
83
+ }),
84
+ delete: new Endpoint({
85
+ ...extraMutateOptions,
86
+ path,
87
+ schema: (schema as any).process ? new Delete(schema as any) : schema,
88
+ method: 'DELETE',
89
+ name: getName('delete'),
90
+ process(res, params) {
91
+ return res && Object.keys(res).length ? res : params;
92
+ },
93
+ getOptimisticResponse: optimistic ? (optimisticDelete as any) : undefined,
94
+ }),
95
+ } as any;
96
+ }
97
+
98
+ function optimisticUpdate(snap: SnapshotInterface, params: any, body: any) {
99
+ return {
100
+ ...params,
101
+ // even tho we don't always have two arguments, the extra one will simply be undefined which spreads fine
102
+ ...body,
103
+ };
104
+ }
105
+ function optimisticPartial(getEndpoint: NewGetEndpoint<any, any>) {
106
+ return function (snap: SnapshotInterface, params: any, body: any) {
107
+ const { data } = snap.getResponse(getEndpoint, params);
108
+ if (!data) throw new AbortOptimistic();
109
+ return {
110
+ ...params,
111
+ ...data,
112
+ // even tho we don't always have two arguments, the extra one will simply be undefined which spreads fine
113
+ ...body,
114
+ };
115
+ };
116
+ }
117
+ function optimisticDelete(snap: SnapshotInterface, params: any) {
118
+ return params;
119
+ }
120
+
121
+ export interface Resource<U extends string, S extends Schema> {
122
+ /** Get a singular item
123
+ *
124
+ * @see https://resthooks.io/rest/api/createResource#get
125
+ */
126
+ get: NewGetEndpoint<{ path: U }, S>;
127
+ /** Get a list of item
128
+ *
129
+ * @see https://resthooks.io/rest/api/createResource#getlist
130
+ */
131
+ getList: NewGetEndpoint<
132
+ {
133
+ path: ShortenPath<U>;
134
+ searchParams: Record<string, number | string | boolean> | undefined;
135
+ },
136
+ S[]
137
+ >;
138
+ /** Create a new item (POST)
139
+ *
140
+ * @see https://resthooks.io/rest/api/createResource#create
141
+ */
142
+ create: NewMutateEndpoint<
143
+ { path: ShortenPath<U>; body: Partial<Denormalize<S>> },
144
+ S
145
+ >;
146
+ /** Update an item (PUT)
147
+ *
148
+ * @see https://resthooks.io/rest/api/createResource#update
149
+ */
150
+ update: NewMutateEndpoint<{ path: U; body: Partial<Denormalize<S>> }, S>;
151
+ /** Update an item (PATCH)
152
+ *
153
+ * @see https://resthooks.io/rest/api/createResource#partialupdate
154
+ */
155
+ partialUpdate: NewMutateEndpoint<
156
+ { path: U; body: Partial<Denormalize<S>> },
157
+ S
158
+ >;
159
+ /** Delete an item (DELETE)
160
+ *
161
+ * @see https://resthooks.io/rest/api/createResource#delete
162
+ */
163
+ delete: RestTypeNoBody<
164
+ PathArgs<U>,
165
+ S extends schema.EntityInterface & { process: any } ? schema.Delete<S> : S,
166
+ undefined,
167
+ Partial<PathArgs<U>>,
168
+ {
169
+ path: U;
170
+ }
171
+ >;
172
+ }
@@ -0,0 +1,34 @@
1
+ import { Schema, schema } from '@data-client/endpoint';
2
+
3
+ export default function extractCollection<
4
+ M extends <C extends schema.Collection>(collection: C) => any,
5
+ S extends Schema | undefined,
6
+ >(s: S, mapper: M): ExtractCollection<S> | undefined {
7
+ if (typeof s !== 'object' || s === undefined || Array.isArray(s)) return;
8
+ if (s instanceof schema.Collection) {
9
+ return mapper(s as any);
10
+ }
11
+ const objCopy: Record<string, Schema> = {
12
+ ...(s instanceof schema.Object ? (s as any).schema : s),
13
+ };
14
+ for (const k in objCopy) {
15
+ if (!objCopy[k]) continue;
16
+ const collection = extractCollection(objCopy[k], mapper);
17
+ if (collection) return collection;
18
+ }
19
+ }
20
+
21
+ export type ExtractCollection<S extends Schema | undefined> =
22
+ S extends schema.CollectionInterface
23
+ ? S
24
+ : S extends schema.Object<infer T>
25
+ ? ExtractObject<T>
26
+ : S extends Exclude<Schema, { [K: string]: any }>
27
+ ? never
28
+ : S extends { [K: string]: Schema }
29
+ ? ExtractObject<S>
30
+ : never;
31
+
32
+ export type ExtractObject<S extends Record<string, any>> = {
33
+ [K in keyof S]: S[K] extends Schema ? ExtractCollection<S[K]> : never;
34
+ }[keyof S];