@data-client/rest 0.15.6 → 0.16.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/CHANGELOG.md +118 -0
- package/dist/index.js +61 -18
- package/dist/index.umd.min.js +1 -1
- package/legacy/NetworkError.js +16 -1
- package/legacy/RestEndpoint.js +17 -4
- package/legacy/RestEndpointTypes.js +1 -1
- package/legacy/RestHelpers.js +29 -16
- package/legacy/pathTypes.js +1 -1
- package/legacy/resource.js +3 -1
- package/legacy/resourceTypes.js +1 -1
- package/lib/NetworkError.d.ts +12 -0
- package/lib/NetworkError.d.ts.map +1 -1
- package/lib/NetworkError.js +16 -1
- package/lib/RestEndpoint.js +17 -4
- package/lib/RestEndpointTypes.d.ts +43 -9
- package/lib/RestEndpointTypes.d.ts.map +1 -1
- package/lib/RestEndpointTypes.js +1 -1
- package/lib/RestHelpers.d.ts +2 -2
- package/lib/RestHelpers.d.ts.map +1 -1
- package/lib/RestHelpers.js +29 -16
- package/lib/pathTypes.d.ts +14 -13
- package/lib/pathTypes.d.ts.map +1 -1
- package/lib/pathTypes.js +1 -1
- package/lib/resource.d.ts.map +1 -1
- package/lib/resource.js +3 -1
- package/lib/resourceTypes.d.ts +5 -1
- package/lib/resourceTypes.d.ts.map +1 -1
- package/lib/resourceTypes.js +1 -1
- package/package.json +5 -5
- package/src/NetworkError.ts +15 -0
- package/src/RestEndpoint.js +21 -3
- package/src/RestEndpointTypes.ts +33 -9
- package/src/RestHelpers.ts +34 -26
- package/src/__tests__/Resource.test.ts +131 -0
- package/src/__tests__/RestEndpoint.ts +212 -4
- package/src/__tests__/__snapshots__/RestEndpoint.ts.snap +2 -0
- package/src/__tests__/__snapshots__/resource-construction.test.ts.snap +1 -1
- package/src/__tests__/hookifyResource.ts +7 -0
- package/src/__tests__/resource-construction.test.ts +543 -0
- package/src/pathTypes.ts +35 -23
- package/src/resource.ts +2 -0
- package/src/resourceTypes.ts +3 -1
- package/ts4.0/NetworkError.d.ts +12 -0
- package/ts4.0/RestEndpointTypes.d.ts +61 -9
- package/ts4.0/RestHelpers.d.ts +2 -2
- package/ts4.0/pathTypes.d.ts +1 -1
- package/ts4.0/resourceExtendable.d.ts +1 -1
- package/ts4.0/resourceTypes.d.ts +7 -1
- package/ts4.1/NetworkError.d.ts +12 -0
- package/ts4.1/RestEndpointTypes.d.ts +61 -9
- package/ts4.1/RestHelpers.d.ts +2 -2
- package/ts4.1/pathTypes.d.ts +14 -13
- package/ts4.1/resourceExtendable.d.ts +1 -1
- package/ts4.1/resourceTypes.d.ts +7 -1
package/src/RestHelpers.ts
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
|
-
import { compile, PathFunction, parse } from 'path-to-regexp';
|
|
1
|
+
import { compile, PathFunction, parse, Token, ParamData } from 'path-to-regexp';
|
|
2
2
|
|
|
3
3
|
import { ShortenPath } from './pathTypes.js';
|
|
4
4
|
|
|
5
|
-
const urlBaseCache:
|
|
6
|
-
export function getUrlBase(path: string): PathFunction {
|
|
7
|
-
if (!
|
|
8
|
-
urlBaseCache
|
|
9
|
-
path,
|
|
10
|
-
compile(path, {
|
|
11
|
-
encode: encodeURIComponent,
|
|
12
|
-
validate: false,
|
|
13
|
-
}),
|
|
14
|
-
);
|
|
5
|
+
const urlBaseCache: Record<string, PathFunction<object>> = Object.create(null);
|
|
6
|
+
export function getUrlBase(path: string): PathFunction<ParamData> {
|
|
7
|
+
if (!(path in urlBaseCache)) {
|
|
8
|
+
urlBaseCache[path] = compile(path);
|
|
15
9
|
}
|
|
16
|
-
return urlBaseCache
|
|
10
|
+
return urlBaseCache[path];
|
|
17
11
|
}
|
|
18
12
|
|
|
19
|
-
const urlTokensCache:
|
|
13
|
+
const urlTokensCache: Record<string, Set<string>> = Object.create(null);
|
|
20
14
|
export function getUrlTokens(path: string): Set<string> {
|
|
21
|
-
if (!
|
|
22
|
-
urlTokensCache.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
if (!(path in urlTokensCache)) {
|
|
16
|
+
urlTokensCache[path] = tokenMap(parse(path).tokens);
|
|
17
|
+
}
|
|
18
|
+
return urlTokensCache[path];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tokenMap(tokens: Token[]): Set<string> {
|
|
22
|
+
const tokenNames = new Set<string>();
|
|
23
|
+
for (const token of tokens) {
|
|
24
|
+
switch (token.type) {
|
|
25
|
+
case 'param':
|
|
26
|
+
case 'wildcard':
|
|
27
|
+
tokenNames.add(token.name);
|
|
28
|
+
break;
|
|
29
|
+
case 'group':
|
|
30
|
+
for (const name of tokenMap(token.tokens)) {
|
|
31
|
+
tokenNames.add(name);
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
28
35
|
}
|
|
29
|
-
return
|
|
36
|
+
return tokenNames;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
const proto = Object.prototype;
|
|
@@ -40,11 +47,12 @@ export function isPojo(obj: unknown): obj is Record<string, any> {
|
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
export function shortenPath<S extends string>(path: S): ShortenPath<S> {
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
throw new Error(
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
const lastTokenIndex = Math.max(path.lastIndexOf(':'), path.lastIndexOf('*'));
|
|
51
|
+
if (lastTokenIndex === -1)
|
|
52
|
+
throw new Error(
|
|
53
|
+
'Resource path requires at least one :parameter or *wildcard',
|
|
54
|
+
);
|
|
55
|
+
let shortUrlRoot: ShortenPath<S> = path.substring(0, lastTokenIndex) as any;
|
|
48
56
|
if (shortUrlRoot[shortUrlRoot.length - 1] === '/')
|
|
49
57
|
shortUrlRoot = shortUrlRoot.substring(
|
|
50
58
|
0,
|
|
@@ -5,6 +5,7 @@ import { CacheProvider } from '@data-client/react';
|
|
|
5
5
|
import nock from 'nock';
|
|
6
6
|
|
|
7
7
|
import { makeRenderDataClient } from '../../../test';
|
|
8
|
+
import NetworkError from '../NetworkError';
|
|
8
9
|
import { ResourcePath } from '../pathTypes';
|
|
9
10
|
import resource from '../resource';
|
|
10
11
|
import RestEndpoint from '../RestEndpoint';
|
|
@@ -133,6 +134,18 @@ describe('resource()', () => {
|
|
|
133
134
|
() => UserResource.get.url({ sdf: '5' });
|
|
134
135
|
});
|
|
135
136
|
|
|
137
|
+
it('should omit optional path params when undefined', () => {
|
|
138
|
+
const ep = new RestEndpoint({ path: '/users{/:id}' });
|
|
139
|
+
expect(ep.url({ id: undefined })).toBe('/users');
|
|
140
|
+
expect(ep.url({ id: '5' })).toBe('/users/5');
|
|
141
|
+
expect(ep.url({})).toBe('/users');
|
|
142
|
+
|
|
143
|
+
const ep2 = new RestEndpoint({ path: '/users{/:id}{/:group}' });
|
|
144
|
+
expect(ep2.url({ id: undefined, group: undefined })).toBe('/users');
|
|
145
|
+
expect(ep2.url({ id: '5', group: undefined })).toBe('/users/5');
|
|
146
|
+
expect(ep2.url({ id: undefined })).toBe('/users');
|
|
147
|
+
});
|
|
148
|
+
|
|
136
149
|
it('should handle multiarg urls', () => {
|
|
137
150
|
const MyUserResource = resource({
|
|
138
151
|
path: 'http\\://test.com/groups/:group/users/:id',
|
|
@@ -179,6 +192,50 @@ describe('resource()', () => {
|
|
|
179
192
|
() => useSuspense(MyUserResource.get, { group: 'yay', id: '5' });
|
|
180
193
|
});
|
|
181
194
|
|
|
195
|
+
it('should shorten wildcard (*) paths for getList', () => {
|
|
196
|
+
const FileResource = resource({
|
|
197
|
+
path: '/repos/:owner/*path',
|
|
198
|
+
schema: User,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// get uses the full path with wildcard
|
|
202
|
+
expect(
|
|
203
|
+
FileResource.get.url({ owner: 'john', path: ['src', 'index.ts'] }),
|
|
204
|
+
).toBe('/repos/john/src/index.ts');
|
|
205
|
+
// getList strips the last *wildcard token
|
|
206
|
+
expect(FileResource.getList.url({ owner: 'john' })).toBe('/repos/john');
|
|
207
|
+
// create (push) also uses shortened path
|
|
208
|
+
expect(FileResource.create.url({ owner: 'john' }, {} as any)).toBe(
|
|
209
|
+
'/repos/john',
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// type checks: get requires both owner and path (array)
|
|
213
|
+
// @ts-expect-error - missing path
|
|
214
|
+
() => FileResource.get.url({ owner: 'john' });
|
|
215
|
+
// @ts-expect-error - path must be array
|
|
216
|
+
() => FileResource.get.url({ owner: 'john', path: 'src/index.ts' });
|
|
217
|
+
// getList only requires owner (extra props become search params)
|
|
218
|
+
() => FileResource.getList({ owner: 'john' });
|
|
219
|
+
// @ts-expect-error - missing required owner param
|
|
220
|
+
() => FileResource.getList({});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should shorten path when wildcard comes after multiple params', () => {
|
|
224
|
+
const DeepResource = resource({
|
|
225
|
+
path: '/api/:version/files/*path',
|
|
226
|
+
schema: User,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(
|
|
230
|
+
DeepResource.get.url({ version: 'v2', path: ['docs', 'readme.md'] }),
|
|
231
|
+
).toBe('/api/v2/files/docs/readme.md');
|
|
232
|
+
expect(DeepResource.getList.url({ version: 'v2' })).toBe('/api/v2/files');
|
|
233
|
+
|
|
234
|
+
// @ts-expect-error - missing version
|
|
235
|
+
() => DeepResource.getList({});
|
|
236
|
+
() => DeepResource.getList({ version: 'v2' });
|
|
237
|
+
});
|
|
238
|
+
|
|
182
239
|
it('should automatically name methods', () => {
|
|
183
240
|
expect(PaginatedArticleResource.get.name).toBe('PaginatedArticle.get');
|
|
184
241
|
expect(PaginatedArticleResource.create.name).toBe(
|
|
@@ -302,6 +359,44 @@ describe('resource()', () => {
|
|
|
302
359
|
expect(res).toEqual({ id: 500 });
|
|
303
360
|
});
|
|
304
361
|
|
|
362
|
+
it('getList.move should have similar properties to update', () => {
|
|
363
|
+
expect(UserResource.getList.move.method).toBe('PATCH');
|
|
364
|
+
// move uses the full entity path (same as update), not the shortened list path
|
|
365
|
+
expect(UserResource.getList.move.url({ id: '5' }, {})).toBe(
|
|
366
|
+
'http://test.com/user/5',
|
|
367
|
+
);
|
|
368
|
+
expect(UserResource.update.url({ id: '5' }, {})).toBe(
|
|
369
|
+
'http://test.com/user/5',
|
|
370
|
+
);
|
|
371
|
+
// schema is the Collection.move variant (not the raw Entity like update)
|
|
372
|
+
expect(UserResource.getList.move.schema).toBeDefined();
|
|
373
|
+
expect(UserResource.getList.move.schema).not.toBe(
|
|
374
|
+
UserResource.update.schema,
|
|
375
|
+
);
|
|
376
|
+
// inherits getOptimisticResponse from getList (which has optimistic via extraMutateOptions)
|
|
377
|
+
expect(typeof UserResource.getList.move.getOptimisticResponse).toBe(
|
|
378
|
+
typeof UserResource.update.getOptimisticResponse,
|
|
379
|
+
);
|
|
380
|
+
// name distinguishes it from update
|
|
381
|
+
expect(UserResource.getList.move.name).toBe('User.getList.partialUpdate');
|
|
382
|
+
// searchParams are removed (move targets a specific entity, not a filtered list)
|
|
383
|
+
expect(UserResource.getList.move.searchParams).toBeUndefined();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('getList.move should handle multiarg urls', () => {
|
|
387
|
+
const MyUserResource = resource({
|
|
388
|
+
path: 'http\\://test.com/groups/:group/users/:id',
|
|
389
|
+
schema: User,
|
|
390
|
+
});
|
|
391
|
+
expect(MyUserResource.getList.move.url({ group: 'big', id: '5' }, {})).toBe(
|
|
392
|
+
'http://test.com/groups/big/users/5',
|
|
393
|
+
);
|
|
394
|
+
// same as update path
|
|
395
|
+
expect(MyUserResource.update.url({ group: 'big', id: '5' }, {})).toBe(
|
|
396
|
+
'http://test.com/groups/big/users/5',
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
305
400
|
it('should spread `url` member', () => {
|
|
306
401
|
const entity = UrlArticle.fromJS({ url: 'five' });
|
|
307
402
|
const spread = { ...entity };
|
|
@@ -309,3 +404,39 @@ describe('resource()', () => {
|
|
|
309
404
|
expect(Object.hasOwn(entity, 'url')).toBeTruthy();
|
|
310
405
|
});
|
|
311
406
|
});
|
|
407
|
+
|
|
408
|
+
describe('NetworkError', () => {
|
|
409
|
+
it('toJSON() should serialize error with status, message, and url', () => {
|
|
410
|
+
const mockResponse = {
|
|
411
|
+
status: 404,
|
|
412
|
+
statusText: 'Not Found',
|
|
413
|
+
url: 'http://test.com/api/missing',
|
|
414
|
+
} as Response;
|
|
415
|
+
|
|
416
|
+
const error = new NetworkError(mockResponse);
|
|
417
|
+
const json = error.toJSON();
|
|
418
|
+
|
|
419
|
+
expect(json).toEqual({
|
|
420
|
+
name: 'NetworkError',
|
|
421
|
+
status: 404,
|
|
422
|
+
message: 'http://test.com/api/missing: Not Found',
|
|
423
|
+
url: 'http://test.com/api/missing',
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('toJSON() output should be JSON.stringify-able', () => {
|
|
428
|
+
const mockResponse = {
|
|
429
|
+
status: 500,
|
|
430
|
+
statusText: 'Internal Server Error',
|
|
431
|
+
url: 'http://test.com/api/broken',
|
|
432
|
+
} as Response;
|
|
433
|
+
|
|
434
|
+
const error = new NetworkError(mockResponse);
|
|
435
|
+
const serialized = JSON.stringify(error);
|
|
436
|
+
const parsed = JSON.parse(serialized);
|
|
437
|
+
|
|
438
|
+
expect(parsed.name).toBe('NetworkError');
|
|
439
|
+
expect(parsed.status).toBe(500);
|
|
440
|
+
expect(parsed.url).toBe('http://test.com/api/broken');
|
|
441
|
+
});
|
|
442
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Entity, schema, Collection } from '@data-client/endpoint';
|
|
2
|
-
import { useController } from '@data-client/react';
|
|
2
|
+
import { useController, useCache } from '@data-client/react';
|
|
3
3
|
import { useSuspense } from '@data-client/react';
|
|
4
4
|
import { CacheProvider } from '@data-client/react';
|
|
5
5
|
import { CoolerArticle, CoolerArticleResource } from '__tests__/new';
|
|
6
6
|
import nock from 'nock';
|
|
7
7
|
|
|
8
|
-
import { makeRenderDataClient } from '../../../test';
|
|
8
|
+
import { makeRenderDataClient, act } from '../../../test';
|
|
9
9
|
import RestEndpoint, {
|
|
10
10
|
Defaults,
|
|
11
11
|
RestEndpointConstructorOptions,
|
|
@@ -132,6 +132,35 @@ const getNextPage3 = getArticleList3.getPage;
|
|
|
132
132
|
a.getPage({ cursor: 'hi', blob: 'ho' });
|
|
133
133
|
// @ts-expect-error
|
|
134
134
|
a.getPage({ blob: 'ho' });
|
|
135
|
+
|
|
136
|
+
// Compare flat vs nested Collection schema type inference
|
|
137
|
+
const flat = new RestEndpoint({
|
|
138
|
+
urlPrefix: 'http://test.com',
|
|
139
|
+
path: '/articles/:id',
|
|
140
|
+
schema: new Collection([PaginatedArticle]),
|
|
141
|
+
method: 'GET',
|
|
142
|
+
});
|
|
143
|
+
// flat Collection schema should be typed
|
|
144
|
+
// @ts-expect-error - schema should not be any
|
|
145
|
+
flat.move.schema satisfies number;
|
|
146
|
+
// @ts-expect-error - push schema should not be any
|
|
147
|
+
flat.push.schema satisfies number;
|
|
148
|
+
|
|
149
|
+
// nested Collection schema should also be typed
|
|
150
|
+
const nested = new RestEndpoint({
|
|
151
|
+
urlPrefix: 'http://test.com',
|
|
152
|
+
path: '/articles/:id',
|
|
153
|
+
schema: {
|
|
154
|
+
nextPage: '',
|
|
155
|
+
data: { results: new Collection([PaginatedArticle]) },
|
|
156
|
+
},
|
|
157
|
+
method: 'GET',
|
|
158
|
+
searchParams: {} as { group: string },
|
|
159
|
+
});
|
|
160
|
+
// @ts-expect-error - nested move schema should not be any
|
|
161
|
+
nested.move.schema satisfies number;
|
|
162
|
+
// @ts-expect-error - nested push schema should not be any
|
|
163
|
+
nested.push.schema satisfies number;
|
|
135
164
|
};
|
|
136
165
|
|
|
137
166
|
describe('RestEndpoint', () => {
|
|
@@ -208,9 +237,9 @@ describe('RestEndpoint', () => {
|
|
|
208
237
|
});
|
|
209
238
|
|
|
210
239
|
it('only optional path means the arg is not required', () => {
|
|
211
|
-
const ep = new RestEndpoint({ path: '/users/:id
|
|
240
|
+
const ep = new RestEndpoint({ path: '/users{/:id}{/:group}' });
|
|
212
241
|
const epbody = new RestEndpoint({
|
|
213
|
-
path: '/users/:id
|
|
242
|
+
path: '/users{/:id}{/:group}',
|
|
214
243
|
body: { title: '' },
|
|
215
244
|
method: 'POST',
|
|
216
245
|
});
|
|
@@ -226,6 +255,64 @@ describe('RestEndpoint', () => {
|
|
|
226
255
|
() => epbody({ title: 'hi' }, { title: 'hi' });
|
|
227
256
|
});
|
|
228
257
|
|
|
258
|
+
it('should omit optional path params when undefined', () => {
|
|
259
|
+
const ep = new RestEndpoint({ path: '/users{/:id}' });
|
|
260
|
+
expect(ep.url({ id: undefined })).toBe('/users');
|
|
261
|
+
expect(ep.url({ id: '5' })).toBe('/users/5');
|
|
262
|
+
expect(ep.url({})).toBe('/users');
|
|
263
|
+
|
|
264
|
+
const ep2 = new RestEndpoint({ path: '/users{/:id}{/:group}' });
|
|
265
|
+
expect(ep2.url({ id: undefined, group: undefined })).toBe('/users');
|
|
266
|
+
expect(ep2.url({ id: '5', group: undefined })).toBe('/users/5');
|
|
267
|
+
expect(ep2.url({ id: undefined })).toBe('/users');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should handle wildcard (*) path params as arrays', () => {
|
|
271
|
+
const ep = new RestEndpoint({ path: '/files/*path' });
|
|
272
|
+
expect(ep.url({ path: ['a', 'b', 'c'] })).toBe('/files/a/b/c');
|
|
273
|
+
expect(ep.url({ path: ['single'] })).toBe('/files/single');
|
|
274
|
+
|
|
275
|
+
// @ts-expect-error - path must be an array, not a string
|
|
276
|
+
() => ep.url({ path: 'a/b/c' });
|
|
277
|
+
// @ts-expect-error - missing required wildcard param
|
|
278
|
+
() => ep.url({});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should handle wildcard with regular params', () => {
|
|
282
|
+
const ep = new RestEndpoint({ path: '/users/:id/*rest' });
|
|
283
|
+
expect(ep.url({ id: '5', rest: ['docs', 'file'] })).toBe(
|
|
284
|
+
'/users/5/docs/file',
|
|
285
|
+
);
|
|
286
|
+
expect(ep.url({ id: '42', rest: ['a'] })).toBe('/users/42/a');
|
|
287
|
+
|
|
288
|
+
// @ts-expect-error - missing id
|
|
289
|
+
() => ep.url({ rest: ['a'] });
|
|
290
|
+
// @ts-expect-error - missing rest
|
|
291
|
+
() => ep.url({ id: '5' });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should handle optional wildcard params', () => {
|
|
295
|
+
const ep = new RestEndpoint({ path: '/files{/*path}' });
|
|
296
|
+
expect(ep.url({ path: ['a', 'b'] })).toBe('/files/a/b');
|
|
297
|
+
expect(ep.url({})).toBe('/files');
|
|
298
|
+
expect(ep.url({ path: undefined })).toBe('/files');
|
|
299
|
+
() => ep();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should handle optional wildcard with required param', () => {
|
|
303
|
+
const ep = new RestEndpoint({ path: '/users/:id{/*rest}' });
|
|
304
|
+
expect(ep.url({ id: '5', rest: ['docs', 'file'] })).toBe(
|
|
305
|
+
'/users/5/docs/file',
|
|
306
|
+
);
|
|
307
|
+
expect(ep.url({ id: '5' })).toBe('/users/5');
|
|
308
|
+
expect(ep.url({ id: '5', rest: undefined })).toBe('/users/5');
|
|
309
|
+
|
|
310
|
+
// @ts-expect-error - missing required param id
|
|
311
|
+
() => ep.url({});
|
|
312
|
+
// @ts-expect-error - rest should be array, not string
|
|
313
|
+
() => ep.url({ id: '5', rest: 'docs/file' });
|
|
314
|
+
});
|
|
315
|
+
|
|
229
316
|
it('should allow sideEffect overrides', () => {
|
|
230
317
|
const weirdGetUser = new RestEndpoint({
|
|
231
318
|
path: 'http\\://test.com/user/:id',
|
|
@@ -364,6 +451,113 @@ describe('RestEndpoint', () => {
|
|
|
364
451
|
expect(getArticleList3.remove.name).toMatchSnapshot();
|
|
365
452
|
});
|
|
366
453
|
|
|
454
|
+
it('move: should extend name of parent endpoint', () => {
|
|
455
|
+
expect(getArticleList3.move.name).toMatchSnapshot();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('move should work with deeply nested Collection', async () => {
|
|
459
|
+
class GroupedArticle extends Entity {
|
|
460
|
+
readonly id: number | undefined = undefined;
|
|
461
|
+
readonly title: string = '';
|
|
462
|
+
readonly group: string = '';
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const getGroupedArticles = new RestEndpoint({
|
|
466
|
+
urlPrefix: 'http://test.com',
|
|
467
|
+
path: '/grouped-articles',
|
|
468
|
+
movePath: '/grouped-articles/:id',
|
|
469
|
+
schema: {
|
|
470
|
+
nextPage: '',
|
|
471
|
+
data: { results: new Collection([GroupedArticle]) },
|
|
472
|
+
},
|
|
473
|
+
method: 'GET',
|
|
474
|
+
searchParams: {} as { group: string },
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Verify schema is correctly extracted from nested structure
|
|
478
|
+
expect(getGroupedArticles.move.schema).toBeTruthy();
|
|
479
|
+
expect(getGroupedArticles.move.method).toBe('PATCH');
|
|
480
|
+
|
|
481
|
+
// Diagnose schema type - should not be any
|
|
482
|
+
// @ts-expect-error - schema should not be any
|
|
483
|
+
getGroupedArticles.move.schema satisfies number;
|
|
484
|
+
|
|
485
|
+
mynock.patch(/grouped-articles/).reply(200, (uri, body: any) => ({
|
|
486
|
+
id: 2,
|
|
487
|
+
title: 'Article 2',
|
|
488
|
+
group: 'beta',
|
|
489
|
+
...(typeof body === 'string' ? JSON.parse(body) : body),
|
|
490
|
+
}));
|
|
491
|
+
|
|
492
|
+
const { result, controller } = renderDataClient(
|
|
493
|
+
() => {
|
|
494
|
+
return {
|
|
495
|
+
alpha: useCache(getGroupedArticles, { group: 'alpha' }),
|
|
496
|
+
beta: useCache(getGroupedArticles, { group: 'beta' }),
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
initialFixtures: [
|
|
501
|
+
{
|
|
502
|
+
endpoint: getGroupedArticles,
|
|
503
|
+
args: [{ group: 'alpha' }],
|
|
504
|
+
response: {
|
|
505
|
+
nextPage: '2',
|
|
506
|
+
data: {
|
|
507
|
+
results: [
|
|
508
|
+
{ id: 1, title: 'Article 1', group: 'alpha' },
|
|
509
|
+
{ id: 2, title: 'Article 2', group: 'alpha' },
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
endpoint: getGroupedArticles,
|
|
516
|
+
args: [{ group: 'beta' }],
|
|
517
|
+
response: {
|
|
518
|
+
nextPage: '2',
|
|
519
|
+
data: {
|
|
520
|
+
results: [{ id: 3, title: 'Article 3', group: 'beta' }],
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
expect(result.current.alpha?.data.results).toHaveLength(2);
|
|
529
|
+
expect(result.current.beta?.data.results).toHaveLength(1);
|
|
530
|
+
|
|
531
|
+
// Move article 2 from alpha to beta
|
|
532
|
+
await act(async () => {
|
|
533
|
+
const response = await controller.fetch(
|
|
534
|
+
getGroupedArticles.move,
|
|
535
|
+
{ id: 2 },
|
|
536
|
+
{
|
|
537
|
+
id: 2,
|
|
538
|
+
title: 'Article 2',
|
|
539
|
+
group: 'beta',
|
|
540
|
+
},
|
|
541
|
+
);
|
|
542
|
+
// Type test: response should be a single GroupedArticle, not an array
|
|
543
|
+
response.title satisfies string;
|
|
544
|
+
expect(response.title).toBe('Article 2');
|
|
545
|
+
expect(response.group).toBe('beta');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// article should be removed from alpha
|
|
549
|
+
expect(result.current.alpha?.data.results).toHaveLength(1);
|
|
550
|
+
const alphaResults = result.current.alpha!.data.results!;
|
|
551
|
+
expect(alphaResults[0]?.title).toBe('Article 1');
|
|
552
|
+
|
|
553
|
+
// article should be added to beta
|
|
554
|
+
const betaResults = result.current.beta!.data.results!;
|
|
555
|
+
expect(betaResults).toHaveLength(2);
|
|
556
|
+
expect(betaResults.map((a: any) => a.title)).toEqual(
|
|
557
|
+
expect.arrayContaining(['Article 3', 'Article 2']),
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
|
|
367
561
|
// TODO: but we need a Values collection
|
|
368
562
|
// it('assign: should extend name of parent endpoint', () => {
|
|
369
563
|
// expect(getArticleList3.assign.name).toMatchSnapshot();
|
|
@@ -1500,6 +1694,20 @@ describe('RestEndpoint.fetch()', () => {
|
|
|
1500
1694
|
expect(noColletionEndpoint.remove.schema).toBeFalsy();
|
|
1501
1695
|
});
|
|
1502
1696
|
|
|
1697
|
+
it('without Collection in schema - endpoint.move schema should be null', () => {
|
|
1698
|
+
const noColletionEndpoint = new RestEndpoint({
|
|
1699
|
+
urlPrefix: 'http://test.com/article-paginated/',
|
|
1700
|
+
path: ':group',
|
|
1701
|
+
name: 'get',
|
|
1702
|
+
schema: {
|
|
1703
|
+
nextPage: '',
|
|
1704
|
+
data: { results: [PaginatedArticle] },
|
|
1705
|
+
},
|
|
1706
|
+
method: 'GET',
|
|
1707
|
+
});
|
|
1708
|
+
expect(noColletionEndpoint.move.schema).toBeFalsy();
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1503
1711
|
it('without Collection in schema - endpoint.getPage should throw', () => {
|
|
1504
1712
|
const noColletionEndpoint = new RestEndpoint({
|
|
1505
1713
|
urlPrefix: 'http://test.com/article-paginated/',
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`RestEndpoint getPage: should extend name of parent endpoint 1`] = `"http://test.com/article-paginated.getPage"`;
|
|
4
4
|
|
|
5
|
+
exports[`RestEndpoint move: should extend name of parent endpoint 1`] = `"http://test.com/article-paginated.partialUpdate"`;
|
|
6
|
+
|
|
5
7
|
exports[`RestEndpoint push: should extend name of parent endpoint 1`] = `"http://test.com/article-paginated.create"`;
|
|
6
8
|
|
|
7
9
|
exports[`RestEndpoint remove: should extend name of parent endpoint 1`] = `"http://test.com/article-paginated.remove"`;
|
|
@@ -4,7 +4,7 @@ exports[`resource() UserResource.delete should work with 1`] = `"not found"`;
|
|
|
4
4
|
|
|
5
5
|
exports[`resource() UserResource.delete should work with {"id": 5} 1`] = `"not found"`;
|
|
6
6
|
|
|
7
|
-
exports[`resource() should not allow paths without at least one argument 1`] = `"Resource path requires at least one :parameter"`;
|
|
7
|
+
exports[`resource() should not allow paths without at least one argument 1`] = `"Resource path requires at least one :parameter or *wildcard"`;
|
|
8
8
|
|
|
9
9
|
exports[`resource() warnings should warn when mis-capitalizing options 1`] = `
|
|
10
10
|
[
|
|
@@ -63,7 +63,14 @@ describe('hookifyResource()', () => {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
let renderDataClient: ReturnType<typeof makeRenderDataClient>;
|
|
66
|
+
let errorSpy: jest.SpyInstance;
|
|
66
67
|
|
|
68
|
+
beforeAll(() => {
|
|
69
|
+
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
70
|
+
});
|
|
71
|
+
afterAll(() => {
|
|
72
|
+
errorSpy.mockRestore();
|
|
73
|
+
});
|
|
67
74
|
beforeEach(() => {
|
|
68
75
|
renderDataClient = makeRenderDataClient(CacheProvider);
|
|
69
76
|
|