@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.
- package/LICENSE +201 -0
- package/README.md +110 -0
- package/dist/index.js +561 -0
- package/dist/index.umd.min.js +1 -0
- package/dist/next.js +520 -0
- package/dist/package.json +1 -0
- package/legacy/NetworkError.js +13 -0
- package/legacy/OptionsToFunction.js +2 -0
- package/legacy/RestEndpoint.js +225 -0
- package/legacy/RestEndpointTypeHelp.js +2 -0
- package/legacy/RestHelpers.js +33 -0
- package/legacy/createResource.js +88 -0
- package/legacy/extractCollection.js +14 -0
- package/legacy/getArrayPath.js +19 -0
- package/legacy/hookifyResource.js +24 -0
- package/legacy/index.js +10 -0
- package/legacy/mapCollection.js +14 -0
- package/legacy/next/OptionsToFunction.js +2 -0
- package/legacy/next/RestEndpoint.js +225 -0
- package/legacy/next/createResource.js +86 -0
- package/legacy/next/index.js +4 -0
- package/legacy/next/resourceTypes.js +2 -0
- package/legacy/paginatedCollections.js +29 -0
- package/legacy/paginationUpdate.js +36 -0
- package/legacy/paramsToString.js +6 -0
- package/legacy/paramsToString.native.js +9 -0
- package/legacy/pathTypes.js +2 -0
- package/legacy/test-fixtures.js +111 -0
- package/legacy/utiltypes.js +2 -0
- package/lib/NetworkError.d.ts +11 -0
- package/lib/NetworkError.d.ts.map +1 -0
- package/lib/NetworkError.js +13 -0
- package/lib/OptionsToFunction.d.ts +7 -0
- package/lib/OptionsToFunction.d.ts.map +1 -0
- package/lib/OptionsToFunction.js +2 -0
- package/lib/RestEndpoint.d.ts +548 -0
- package/lib/RestEndpoint.js +229 -0
- package/lib/RestEndpointTypeHelp.d.ts +10 -0
- package/lib/RestEndpointTypeHelp.d.ts.map +1 -0
- package/lib/RestEndpointTypeHelp.js +2 -0
- package/lib/RestHelpers.d.ts +7 -0
- package/lib/RestHelpers.d.ts.map +1 -0
- package/lib/RestHelpers.js +33 -0
- package/lib/createResource.d.ts +66 -0
- package/lib/createResource.d.ts.map +1 -0
- package/lib/createResource.js +104 -0
- package/lib/extractCollection.d.ts +11 -0
- package/lib/extractCollection.d.ts.map +1 -0
- package/lib/extractCollection.js +16 -0
- package/lib/getArrayPath.d.ts +3 -0
- package/lib/getArrayPath.d.ts.map +1 -0
- package/lib/getArrayPath.js +19 -0
- package/lib/hookifyResource.d.ts +14 -0
- package/lib/hookifyResource.d.ts.map +1 -0
- package/lib/hookifyResource.js +24 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +10 -0
- package/lib/mapCollection.d.ts +16 -0
- package/lib/mapCollection.d.ts.map +1 -0
- package/lib/mapCollection.js +16 -0
- package/lib/next/OptionsToFunction.d.ts +7 -0
- package/lib/next/OptionsToFunction.d.ts.map +1 -0
- package/lib/next/OptionsToFunction.js +2 -0
- package/lib/next/RestEndpoint.d.ts +542 -0
- package/lib/next/RestEndpoint.js +229 -0
- package/lib/next/createResource.d.ts +86 -0
- package/lib/next/createResource.d.ts.map +1 -0
- package/lib/next/createResource.js +100 -0
- package/lib/next/index.d.ts +6 -0
- package/lib/next/index.d.ts.map +1 -0
- package/lib/next/index.js +4 -0
- package/lib/next/resourceTypes.d.ts +31 -0
- package/lib/next/resourceTypes.d.ts.map +1 -0
- package/lib/next/resourceTypes.js +2 -0
- package/lib/paginatedCollections.d.ts +3 -0
- package/lib/paginatedCollections.d.ts.map +1 -0
- package/lib/paginatedCollections.js +29 -0
- package/lib/paginationUpdate.d.ts +8 -0
- package/lib/paginationUpdate.d.ts.map +1 -0
- package/lib/paginationUpdate.js +40 -0
- package/lib/paramsToString.d.ts +2 -0
- package/lib/paramsToString.d.ts.map +1 -0
- package/lib/paramsToString.js +6 -0
- package/lib/paramsToString.native.d.ts +2 -0
- package/lib/paramsToString.native.d.ts.map +1 -0
- package/lib/paramsToString.native.js +9 -0
- package/lib/pathTypes.d.ts +20 -0
- package/lib/pathTypes.d.ts.map +1 -0
- package/lib/pathTypes.js +2 -0
- package/lib/test-fixtures.d.ts +141 -0
- package/lib/test-fixtures.d.ts.map +1 -0
- package/lib/test-fixtures.js +111 -0
- package/lib/utiltypes.d.ts +21 -0
- package/lib/utiltypes.d.ts.map +1 -0
- package/lib/utiltypes.js +2 -0
- package/node.mjs +1 -0
- package/package.json +121 -0
- package/src/NetworkError.ts +17 -0
- package/src/OptionsToFunction.ts +42 -0
- package/src/RestEndpoint.d.ts +548 -0
- package/src/RestEndpoint.js +262 -0
- package/src/RestEndpointTypeHelp.ts +20 -0
- package/src/RestHelpers.ts +48 -0
- package/src/__tests__/Resource.test.ts +317 -0
- package/src/__tests__/RestEndpoint.ts +1304 -0
- package/src/__tests__/__snapshots__/RestEndpoint.ts.snap +18 -0
- package/src/__tests__/__snapshots__/createResource.test.ts.snap +5 -0
- package/src/__tests__/createResource.test.ts +583 -0
- package/src/__tests__/getArrayPath.test.ts +30 -0
- package/src/__tests__/hookifyResource.ts +197 -0
- package/src/__tests__/types.test.ts +309 -0
- package/src/createResource.ts +172 -0
- package/src/extractCollection.ts +34 -0
- package/src/getArrayPath.ts +20 -0
- package/src/hookifyResource.ts +36 -0
- package/src/index.ts +42 -0
- package/src/mapCollection.ts +54 -0
- package/src/next/OptionsToFunction.ts +41 -0
- package/src/next/RestEndpoint.d.ts +542 -0
- package/src/next/RestEndpoint.js +262 -0
- package/src/next/__tests__/RestEndpoint.ts +1333 -0
- package/src/next/__tests__/__snapshots__/RestEndpoint.ts.snap +18 -0
- package/src/next/__tests__/types.test.ts +429 -0
- package/src/next/createResource.ts +194 -0
- package/src/next/index.ts +5 -0
- package/src/next/resourceTypes.ts +32 -0
- package/src/package.json +1 -0
- package/src/paginatedCollections.ts +32 -0
- package/src/paginationUpdate.ts +49 -0
- package/src/paramsToString.native.ts +10 -0
- package/src/paramsToString.ts +7 -0
- package/src/pathTypes.ts +58 -0
- package/src/test-fixtures.ts +128 -0
- package/src/utiltypes.ts +25 -0
- package/ts4.0/NetworkError.d.ts +11 -0
- package/ts4.0/OptionsToFunction.d.ts +16 -0
- package/ts4.0/RestEndpoint.d.ts +240 -0
- package/ts4.0/RestEndpointTypeHelp.d.ts +10 -0
- package/ts4.0/RestHelpers.d.ts +7 -0
- package/ts4.0/createResource.d.ts +66 -0
- package/ts4.0/extractCollection.d.ts +11 -0
- package/ts4.0/getArrayPath.d.ts +3 -0
- package/ts4.0/hookifyResource.d.ts +19 -0
- package/ts4.0/index.d.ts +12 -0
- package/ts4.0/mapCollection.d.ts +16 -0
- package/ts4.0/next/OptionsToFunction.d.ts +16 -0
- package/ts4.0/next/RestEndpoint.d.ts +226 -0
- package/ts4.0/next/createResource.d.ts +94 -0
- package/ts4.0/next/index.d.ts +6 -0
- package/ts4.0/next/resourceTypes.d.ts +31 -0
- package/ts4.0/paginatedCollections.d.ts +3 -0
- package/ts4.0/paginationUpdate.d.ts +10 -0
- package/ts4.0/paramsToString.d.ts +2 -0
- package/ts4.0/paramsToString.native.d.ts +2 -0
- package/ts4.0/pathTypes.d.ts +20 -0
- package/ts4.0/test-fixtures.d.ts +141 -0
- 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];
|