@data-client/rest 0.16.4 → 0.16.6
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 +65 -0
- package/dist/index.js +59 -60
- package/dist/index.umd.min.js +1 -1
- package/legacy/RestEndpoint.js +55 -27
- package/legacy/RestEndpointTypes.js +1 -1
- package/legacy/RestHelpers.js +2 -13
- package/legacy/index.js +1 -1
- package/legacy/pathToRegexp.js +2 -0
- package/lib/RestEndpoint.js +55 -27
- package/lib/RestEndpointTypes.d.ts +39 -19
- package/lib/RestEndpointTypes.d.ts.map +1 -1
- package/lib/RestEndpointTypes.js +1 -1
- package/lib/RestHelpers.d.ts +1 -1
- package/lib/RestHelpers.d.ts.map +1 -1
- package/lib/RestHelpers.js +2 -13
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/pathToRegexp.d.ts +3 -0
- package/lib/pathToRegexp.d.ts.map +1 -0
- package/lib/pathToRegexp.js +342 -0
- package/package.json +5 -4
- package/src/RestEndpoint.js +85 -34
- package/src/RestEndpointTypes.ts +71 -22
- package/src/RestHelpers.ts +8 -9
- package/src/__tests__/RestEndpoint.ts +485 -1
- package/src/__tests__/resource-construction.test.ts +47 -0
- package/src/index.ts +2 -0
- package/src/pathToRegexp.ts +2 -0
- package/ts4.0/RestEndpointTypes.d.ts +39 -19
- package/ts4.0/RestHelpers.d.ts +1 -1
- package/ts4.0/index.d.ts +2 -1
- package/ts4.0/pathToRegexp.d.ts +3 -0
- package/ts4.1/RestEndpointTypes.d.ts +39 -19
- package/ts4.1/RestHelpers.d.ts +1 -1
- package/ts4.1/index.d.ts +2 -1
- package/ts4.1/pathToRegexp.d.ts +3 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Entity, schema, Collection } from '@data-client/endpoint';
|
|
1
|
+
import { Entity, schema, Collection, Values } from '@data-client/endpoint';
|
|
2
2
|
import { useController, useCache } from '@data-client/react';
|
|
3
3
|
import { useSuspense } from '@data-client/react';
|
|
4
4
|
import { CacheProvider } from '@data-client/react';
|
|
@@ -145,6 +145,13 @@ const getNextPage3 = getArticleList3.getPage;
|
|
|
145
145
|
flat.move.schema satisfies number;
|
|
146
146
|
// @ts-expect-error - push schema should not be any
|
|
147
147
|
flat.push.schema satisfies number;
|
|
148
|
+
// body should be derived from collection's entity schema, not any
|
|
149
|
+
// @ts-expect-error
|
|
150
|
+
flat.push.body satisfies number;
|
|
151
|
+
// @ts-expect-error
|
|
152
|
+
flat.move.body satisfies number;
|
|
153
|
+
// @ts-expect-error
|
|
154
|
+
flat.remove.body satisfies number;
|
|
148
155
|
|
|
149
156
|
// nested Collection schema should also be typed
|
|
150
157
|
const nested = new RestEndpoint({
|
|
@@ -161,6 +168,63 @@ const getNextPage3 = getArticleList3.getPage;
|
|
|
161
168
|
nested.move.schema satisfies number;
|
|
162
169
|
// @ts-expect-error - nested push schema should not be any
|
|
163
170
|
nested.push.schema satisfies number;
|
|
171
|
+
// @ts-expect-error
|
|
172
|
+
nested.push.body satisfies number;
|
|
173
|
+
// @ts-expect-error
|
|
174
|
+
nested.move.body satisfies number;
|
|
175
|
+
// @ts-expect-error
|
|
176
|
+
nested.remove.body satisfies number;
|
|
177
|
+
|
|
178
|
+
// With explicit body: PATCH extenders should have Partial body
|
|
179
|
+
type Body = { title: string; content: string };
|
|
180
|
+
const withBody = new RestEndpoint({
|
|
181
|
+
urlPrefix: 'http://test.com',
|
|
182
|
+
path: '/articles/:id',
|
|
183
|
+
schema: new Collection([PaginatedArticle]),
|
|
184
|
+
method: 'GET',
|
|
185
|
+
body: {} as Body,
|
|
186
|
+
});
|
|
187
|
+
// POST extenders keep full body type
|
|
188
|
+
// @ts-expect-error - push body should not be any
|
|
189
|
+
withBody.push.body satisfies number;
|
|
190
|
+
withBody.push.body satisfies Body | Body[] | FormData | undefined;
|
|
191
|
+
// @ts-expect-error - unshift body should not be any
|
|
192
|
+
withBody.unshift.body satisfies number;
|
|
193
|
+
// PATCH extenders get Partial body (like partialUpdate)
|
|
194
|
+
// @ts-expect-error - move body should not be any
|
|
195
|
+
withBody.move.body satisfies number;
|
|
196
|
+
withBody.move.body satisfies Partial<Body> | FormData | undefined;
|
|
197
|
+
// @ts-expect-error - remove body should not be any
|
|
198
|
+
withBody.remove.body satisfies number;
|
|
199
|
+
withBody.remove.body satisfies
|
|
200
|
+
| Partial<Body>
|
|
201
|
+
| Partial<Body>[]
|
|
202
|
+
| FormData
|
|
203
|
+
| undefined;
|
|
204
|
+
// push body requires full Body (not partial)
|
|
205
|
+
() => withBody.push({ id: 'a' }, { title: 'hi', content: 'there' });
|
|
206
|
+
// @ts-expect-error - push rejects partial body
|
|
207
|
+
() => withBody.push({ id: 'a' }, { title: 'hi' });
|
|
208
|
+
// move body accepts partial Body (PATCH semantics)
|
|
209
|
+
() => withBody.move({ id: 'a' }, { title: 'hi' });
|
|
210
|
+
() => withBody.move({ id: 'a' }, { content: 'there' });
|
|
211
|
+
// @ts-expect-error - move rejects invalid fields
|
|
212
|
+
() => withBody.move({ id: 'a' }, { invalid: 'field' });
|
|
213
|
+
|
|
214
|
+
// Values Collection: assign body should accept Record<string, entity>
|
|
215
|
+
const valuesEndpoint = new RestEndpoint({
|
|
216
|
+
urlPrefix: 'http://test.com',
|
|
217
|
+
path: '/articles',
|
|
218
|
+
schema: new Collection(new Values(PaginatedArticle)),
|
|
219
|
+
method: 'GET',
|
|
220
|
+
});
|
|
221
|
+
// @ts-expect-error - assign body should not be any
|
|
222
|
+
valuesEndpoint.assign.body satisfies number;
|
|
223
|
+
valuesEndpoint.assign.body satisfies
|
|
224
|
+
| Record<string, Partial<PaginatedArticle>>
|
|
225
|
+
| FormData
|
|
226
|
+
| undefined;
|
|
227
|
+
() => valuesEndpoint.assign({ myKey: { id: 5, title: 'hi', content: 'ho' } });
|
|
164
228
|
};
|
|
165
229
|
|
|
166
230
|
describe('RestEndpoint', () => {
|
|
@@ -1736,6 +1800,426 @@ describe('RestEndpoint.fetch()', () => {
|
|
|
1736
1800
|
expect(() => noColletionEndpoint.getPage).toThrowErrorMatchingSnapshot();
|
|
1737
1801
|
});
|
|
1738
1802
|
});
|
|
1803
|
+
|
|
1804
|
+
describe('content property', () => {
|
|
1805
|
+
const binaryData = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
|
1806
|
+
|
|
1807
|
+
it("content: 'blob' calls response.blob()", async () => {
|
|
1808
|
+
nock(/.*/)
|
|
1809
|
+
.defaultReplyHeaders({
|
|
1810
|
+
'Access-Control-Allow-Origin': '*',
|
|
1811
|
+
'Content-Type': 'application/octet-stream',
|
|
1812
|
+
})
|
|
1813
|
+
.get('/files/1')
|
|
1814
|
+
.reply(200, binaryData);
|
|
1815
|
+
|
|
1816
|
+
const ep = new RestEndpoint({
|
|
1817
|
+
path: 'http\\://test.com/files/:id',
|
|
1818
|
+
content: 'blob',
|
|
1819
|
+
});
|
|
1820
|
+
const result = await ep({ id: 1 });
|
|
1821
|
+
expect(result).toBeInstanceOf(Blob);
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
it("content: 'text' calls response.text()", async () => {
|
|
1825
|
+
nock(/.*/)
|
|
1826
|
+
.defaultReplyHeaders({
|
|
1827
|
+
'Access-Control-Allow-Origin': '*',
|
|
1828
|
+
'Content-Type': 'text/plain',
|
|
1829
|
+
})
|
|
1830
|
+
.get('/files/1')
|
|
1831
|
+
.reply(200, 'hello world');
|
|
1832
|
+
|
|
1833
|
+
const ep = new RestEndpoint({
|
|
1834
|
+
path: 'http\\://test.com/files/:id',
|
|
1835
|
+
content: 'text',
|
|
1836
|
+
});
|
|
1837
|
+
const result = await ep({ id: 1 });
|
|
1838
|
+
expect(result).toBe('hello world');
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
it("content: 'json' calls response.json() strictly", async () => {
|
|
1842
|
+
nock(/.*/)
|
|
1843
|
+
.defaultReplyHeaders({
|
|
1844
|
+
'Access-Control-Allow-Origin': '*',
|
|
1845
|
+
'Content-Type': 'text/plain',
|
|
1846
|
+
})
|
|
1847
|
+
.get('/files/1')
|
|
1848
|
+
.reply(200, { ok: true });
|
|
1849
|
+
|
|
1850
|
+
const ep = new RestEndpoint({
|
|
1851
|
+
path: 'http\\://test.com/files/:id',
|
|
1852
|
+
content: 'json',
|
|
1853
|
+
});
|
|
1854
|
+
const result = await ep({ id: 1 });
|
|
1855
|
+
expect(result).toEqual({ ok: true });
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
it('content: blob with schema set throws', async () => {
|
|
1859
|
+
nock(/.*/)
|
|
1860
|
+
.defaultReplyHeaders({
|
|
1861
|
+
'Access-Control-Allow-Origin': '*',
|
|
1862
|
+
'Content-Type': 'application/octet-stream',
|
|
1863
|
+
})
|
|
1864
|
+
.get('/files/1')
|
|
1865
|
+
.reply(200, binaryData);
|
|
1866
|
+
|
|
1867
|
+
const ep = new RestEndpoint({
|
|
1868
|
+
path: 'http\\://test.com/files/:id',
|
|
1869
|
+
content: 'blob',
|
|
1870
|
+
schema: User,
|
|
1871
|
+
} as any);
|
|
1872
|
+
await expect(async () => await ep({ id: 1 })).rejects.toMatchObject({
|
|
1873
|
+
status: 400,
|
|
1874
|
+
message: expect.stringContaining('incompatible with schema'),
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
it.each(['blob', 'text', 'arrayBuffer', 'stream'] as const)(
|
|
1879
|
+
'content: %s with schema warns at construction',
|
|
1880
|
+
content => {
|
|
1881
|
+
const errorSpy = jest
|
|
1882
|
+
.spyOn(console, 'error')
|
|
1883
|
+
.mockImplementation(() => {});
|
|
1884
|
+
try {
|
|
1885
|
+
new RestEndpoint({
|
|
1886
|
+
path: 'http\\://test.com/files/:id',
|
|
1887
|
+
content,
|
|
1888
|
+
schema: User,
|
|
1889
|
+
} as any);
|
|
1890
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1891
|
+
expect.stringContaining('incompatible with schema'),
|
|
1892
|
+
);
|
|
1893
|
+
} finally {
|
|
1894
|
+
errorSpy.mockRestore();
|
|
1895
|
+
}
|
|
1896
|
+
},
|
|
1897
|
+
);
|
|
1898
|
+
|
|
1899
|
+
it('204 with content: blob returns null', async () => {
|
|
1900
|
+
nock(/.*/)
|
|
1901
|
+
.defaultReplyHeaders({
|
|
1902
|
+
'Access-Control-Allow-Origin': '*',
|
|
1903
|
+
})
|
|
1904
|
+
.get('/files/1')
|
|
1905
|
+
.reply(204);
|
|
1906
|
+
|
|
1907
|
+
const ep = new RestEndpoint({
|
|
1908
|
+
path: 'http\\://test.com/files/:id',
|
|
1909
|
+
content: 'blob',
|
|
1910
|
+
});
|
|
1911
|
+
const result = await ep({ id: 1 });
|
|
1912
|
+
expect(result).toBe(null);
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
it("extend({ content: 'blob' }) propagates", async () => {
|
|
1916
|
+
nock(/.*/)
|
|
1917
|
+
.defaultReplyHeaders({
|
|
1918
|
+
'Access-Control-Allow-Origin': '*',
|
|
1919
|
+
'Content-Type': 'application/octet-stream',
|
|
1920
|
+
})
|
|
1921
|
+
.get('/files/1')
|
|
1922
|
+
.reply(200, binaryData);
|
|
1923
|
+
|
|
1924
|
+
const base = new RestEndpoint({
|
|
1925
|
+
path: 'http\\://test.com/files/:id',
|
|
1926
|
+
});
|
|
1927
|
+
const blobEp = base.extend({ content: 'blob' });
|
|
1928
|
+
expect(blobEp.content).toBe('blob');
|
|
1929
|
+
const result = await blobEp({ id: 1 });
|
|
1930
|
+
expect(result).toBeInstanceOf(Blob);
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
it('subclass with content = blob works', async () => {
|
|
1934
|
+
nock(/.*/)
|
|
1935
|
+
.defaultReplyHeaders({
|
|
1936
|
+
'Access-Control-Allow-Origin': '*',
|
|
1937
|
+
'Content-Type': 'application/octet-stream',
|
|
1938
|
+
})
|
|
1939
|
+
.get('/files/1')
|
|
1940
|
+
.reply(200, binaryData);
|
|
1941
|
+
|
|
1942
|
+
class BlobEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
|
|
1943
|
+
content = 'blob' as const;
|
|
1944
|
+
}
|
|
1945
|
+
const ep = new BlobEndpoint({ path: 'http\\://test.com/files/:id' });
|
|
1946
|
+
expect(ep.content).toBe('blob');
|
|
1947
|
+
const result = await ep({ id: 1 });
|
|
1948
|
+
expect(result).toBeInstanceOf(Blob);
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
it("content: 'arrayBuffer' calls response.arrayBuffer()", async () => {
|
|
1952
|
+
nock(/.*/)
|
|
1953
|
+
.defaultReplyHeaders({
|
|
1954
|
+
'Access-Control-Allow-Origin': '*',
|
|
1955
|
+
'Content-Type': 'application/octet-stream',
|
|
1956
|
+
})
|
|
1957
|
+
.get('/files/1')
|
|
1958
|
+
.reply(200, Buffer.from([1, 2, 3, 4]));
|
|
1959
|
+
|
|
1960
|
+
const ep = new RestEndpoint({
|
|
1961
|
+
path: 'http\\://test.com/files/:id',
|
|
1962
|
+
content: 'arrayBuffer',
|
|
1963
|
+
});
|
|
1964
|
+
const result = await ep({ id: 1 });
|
|
1965
|
+
expect(result).toBeInstanceOf(ArrayBuffer);
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
it("content: 'stream' returns response.body", async () => {
|
|
1969
|
+
const mockBody = {
|
|
1970
|
+
getReader() {
|
|
1971
|
+
return {};
|
|
1972
|
+
},
|
|
1973
|
+
};
|
|
1974
|
+
const mockResponse = {
|
|
1975
|
+
ok: true,
|
|
1976
|
+
status: 200,
|
|
1977
|
+
body: mockBody,
|
|
1978
|
+
headers: new Headers({ 'Content-Type': 'application/octet-stream' }),
|
|
1979
|
+
} as unknown as Response;
|
|
1980
|
+
|
|
1981
|
+
const ep = new RestEndpoint({
|
|
1982
|
+
path: 'http\\://test.com/files/:id',
|
|
1983
|
+
content: 'stream',
|
|
1984
|
+
fetchResponse() {
|
|
1985
|
+
return Promise.resolve(mockResponse);
|
|
1986
|
+
},
|
|
1987
|
+
});
|
|
1988
|
+
const result = await ep({ id: 1 });
|
|
1989
|
+
expect(result).toBe(mockBody);
|
|
1990
|
+
});
|
|
1991
|
+
});
|
|
1992
|
+
|
|
1993
|
+
describe('auto-detection (no content)', () => {
|
|
1994
|
+
it('Content-Type: image/png returns blob', async () => {
|
|
1995
|
+
const imgData = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
|
1996
|
+
nock(/.*/)
|
|
1997
|
+
.defaultReplyHeaders({
|
|
1998
|
+
'Access-Control-Allow-Origin': '*',
|
|
1999
|
+
'Content-Type': 'image/png',
|
|
2000
|
+
})
|
|
2001
|
+
.get('/files/1')
|
|
2002
|
+
.reply(200, imgData);
|
|
2003
|
+
|
|
2004
|
+
const ep = new RestEndpoint({
|
|
2005
|
+
path: 'http\\://test.com/files/:id',
|
|
2006
|
+
});
|
|
2007
|
+
const result = await ep({ id: 1 });
|
|
2008
|
+
expect(result).toBeInstanceOf(Blob);
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
it('Content-Type: application/octet-stream returns blob', async () => {
|
|
2012
|
+
nock(/.*/)
|
|
2013
|
+
.defaultReplyHeaders({
|
|
2014
|
+
'Access-Control-Allow-Origin': '*',
|
|
2015
|
+
'Content-Type': 'application/octet-stream',
|
|
2016
|
+
})
|
|
2017
|
+
.get('/files/1')
|
|
2018
|
+
.reply(200, Buffer.from([1, 2, 3]));
|
|
2019
|
+
|
|
2020
|
+
const ep = new RestEndpoint({
|
|
2021
|
+
path: 'http\\://test.com/files/:id',
|
|
2022
|
+
});
|
|
2023
|
+
const result = await ep({ id: 1 });
|
|
2024
|
+
expect(result).toBeInstanceOf(Blob);
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
it('Content-Type: application/pdf returns blob', async () => {
|
|
2028
|
+
nock(/.*/)
|
|
2029
|
+
.defaultReplyHeaders({
|
|
2030
|
+
'Access-Control-Allow-Origin': '*',
|
|
2031
|
+
'Content-Type': 'application/pdf',
|
|
2032
|
+
})
|
|
2033
|
+
.get('/files/1')
|
|
2034
|
+
.reply(200, Buffer.from([0x25, 0x50, 0x44, 0x46]));
|
|
2035
|
+
|
|
2036
|
+
const ep = new RestEndpoint({
|
|
2037
|
+
path: 'http\\://test.com/files/:id',
|
|
2038
|
+
});
|
|
2039
|
+
const result = await ep({ id: 1 });
|
|
2040
|
+
expect(result).toBeInstanceOf(Blob);
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
it('Content-Type: text/plain returns text (unchanged)', async () => {
|
|
2044
|
+
nock(/.*/)
|
|
2045
|
+
.defaultReplyHeaders({
|
|
2046
|
+
'Access-Control-Allow-Origin': '*',
|
|
2047
|
+
'Content-Type': 'text/plain',
|
|
2048
|
+
})
|
|
2049
|
+
.get('/files/1')
|
|
2050
|
+
.reply(200, 'plain text');
|
|
2051
|
+
|
|
2052
|
+
const ep = new RestEndpoint({
|
|
2053
|
+
path: 'http\\://test.com/files/:id',
|
|
2054
|
+
});
|
|
2055
|
+
const result = await ep({ id: 1 });
|
|
2056
|
+
expect(result).toBe('plain text');
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
it('Content-Type: application/json returns JSON (unchanged)', async () => {
|
|
2060
|
+
nock(/.*/)
|
|
2061
|
+
.defaultReplyHeaders({
|
|
2062
|
+
'Access-Control-Allow-Origin': '*',
|
|
2063
|
+
'Content-Type': 'application/json',
|
|
2064
|
+
})
|
|
2065
|
+
.get('/files/1')
|
|
2066
|
+
.reply(200, { id: 1, name: 'test' });
|
|
2067
|
+
|
|
2068
|
+
const ep = new RestEndpoint({
|
|
2069
|
+
path: 'http\\://test.com/files/:id',
|
|
2070
|
+
});
|
|
2071
|
+
const result = await ep({ id: 1 });
|
|
2072
|
+
expect(result).toEqual({ id: 1, name: 'test' });
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
it('Content-Type: application/xml returns text (text-like)', async () => {
|
|
2076
|
+
nock(/.*/)
|
|
2077
|
+
.defaultReplyHeaders({
|
|
2078
|
+
'Access-Control-Allow-Origin': '*',
|
|
2079
|
+
'Content-Type': 'application/xml',
|
|
2080
|
+
})
|
|
2081
|
+
.get('/files/1')
|
|
2082
|
+
.reply(200, '<root>hi</root>');
|
|
2083
|
+
|
|
2084
|
+
const ep = new RestEndpoint({
|
|
2085
|
+
path: 'http\\://test.com/files/:id',
|
|
2086
|
+
});
|
|
2087
|
+
const result = await ep({ id: 1 });
|
|
2088
|
+
expect(result).toBe('<root>hi</root>');
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
it('Content-Type: audio/mpeg returns blob', async () => {
|
|
2092
|
+
nock(/.*/)
|
|
2093
|
+
.defaultReplyHeaders({
|
|
2094
|
+
'Access-Control-Allow-Origin': '*',
|
|
2095
|
+
'Content-Type': 'audio/mpeg',
|
|
2096
|
+
})
|
|
2097
|
+
.get('/files/1')
|
|
2098
|
+
.reply(200, Buffer.from([0xff, 0xfb]));
|
|
2099
|
+
|
|
2100
|
+
const ep = new RestEndpoint({
|
|
2101
|
+
path: 'http\\://test.com/files/:id',
|
|
2102
|
+
});
|
|
2103
|
+
const result = await ep({ id: 1 });
|
|
2104
|
+
expect(result).toBeInstanceOf(Blob);
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
it('Content-Type: video/mp4 returns blob', async () => {
|
|
2108
|
+
nock(/.*/)
|
|
2109
|
+
.defaultReplyHeaders({
|
|
2110
|
+
'Access-Control-Allow-Origin': '*',
|
|
2111
|
+
'Content-Type': 'video/mp4',
|
|
2112
|
+
})
|
|
2113
|
+
.get('/files/1')
|
|
2114
|
+
.reply(200, Buffer.from([0x00, 0x00]));
|
|
2115
|
+
|
|
2116
|
+
const ep = new RestEndpoint({
|
|
2117
|
+
path: 'http\\://test.com/files/:id',
|
|
2118
|
+
});
|
|
2119
|
+
const result = await ep({ id: 1 });
|
|
2120
|
+
expect(result).toBeInstanceOf(Blob);
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
it('Content-Type: font/woff2 returns blob', async () => {
|
|
2124
|
+
nock(/.*/)
|
|
2125
|
+
.defaultReplyHeaders({
|
|
2126
|
+
'Access-Control-Allow-Origin': '*',
|
|
2127
|
+
'Content-Type': 'font/woff2',
|
|
2128
|
+
})
|
|
2129
|
+
.get('/files/1')
|
|
2130
|
+
.reply(200, Buffer.from([0x77, 0x4f]));
|
|
2131
|
+
|
|
2132
|
+
const ep = new RestEndpoint({
|
|
2133
|
+
path: 'http\\://test.com/files/:id',
|
|
2134
|
+
});
|
|
2135
|
+
const result = await ep({ id: 1 });
|
|
2136
|
+
expect(result).toBeInstanceOf(Blob);
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
it('Content-Type: XLSX (openxmlformats) returns blob', async () => {
|
|
2140
|
+
nock(/.*/)
|
|
2141
|
+
.defaultReplyHeaders({
|
|
2142
|
+
'Access-Control-Allow-Origin': '*',
|
|
2143
|
+
'Content-Type':
|
|
2144
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
2145
|
+
})
|
|
2146
|
+
.get('/files/1')
|
|
2147
|
+
.reply(200, Buffer.from([0x50, 0x4b, 0x03, 0x04]));
|
|
2148
|
+
|
|
2149
|
+
const ep = new RestEndpoint({
|
|
2150
|
+
path: 'http\\://test.com/files/:id',
|
|
2151
|
+
});
|
|
2152
|
+
const result = await ep({ id: 1 });
|
|
2153
|
+
expect(result).toBeInstanceOf(Blob);
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
it('Content-Type: DOCX (openxmlformats) returns blob', async () => {
|
|
2157
|
+
nock(/.*/)
|
|
2158
|
+
.defaultReplyHeaders({
|
|
2159
|
+
'Access-Control-Allow-Origin': '*',
|
|
2160
|
+
'Content-Type':
|
|
2161
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
2162
|
+
})
|
|
2163
|
+
.get('/files/1')
|
|
2164
|
+
.reply(200, Buffer.from([0x50, 0x4b, 0x03, 0x04]));
|
|
2165
|
+
|
|
2166
|
+
const ep = new RestEndpoint({
|
|
2167
|
+
path: 'http\\://test.com/files/:id',
|
|
2168
|
+
});
|
|
2169
|
+
const result = await ep({ id: 1 });
|
|
2170
|
+
expect(result).toBeInstanceOf(Blob);
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
it('Content-Type: PPTX (openxmlformats) returns blob', async () => {
|
|
2174
|
+
nock(/.*/)
|
|
2175
|
+
.defaultReplyHeaders({
|
|
2176
|
+
'Access-Control-Allow-Origin': '*',
|
|
2177
|
+
'Content-Type':
|
|
2178
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
2179
|
+
})
|
|
2180
|
+
.get('/files/1')
|
|
2181
|
+
.reply(200, Buffer.from([0x50, 0x4b, 0x03, 0x04]));
|
|
2182
|
+
|
|
2183
|
+
const ep = new RestEndpoint({
|
|
2184
|
+
path: 'http\\://test.com/files/:id',
|
|
2185
|
+
});
|
|
2186
|
+
const result = await ep({ id: 1 });
|
|
2187
|
+
expect(result).toBeInstanceOf(Blob);
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
it('binary content-type with normalizable schema throws', async () => {
|
|
2191
|
+
nock(/.*/)
|
|
2192
|
+
.defaultReplyHeaders({
|
|
2193
|
+
'Access-Control-Allow-Origin': '*',
|
|
2194
|
+
'Content-Type': 'application/octet-stream',
|
|
2195
|
+
})
|
|
2196
|
+
.get('/files/1')
|
|
2197
|
+
.reply(200, Buffer.from([1, 2, 3]));
|
|
2198
|
+
|
|
2199
|
+
const ep = new RestEndpoint({
|
|
2200
|
+
path: 'http\\://test.com/files/:id',
|
|
2201
|
+
schema: User,
|
|
2202
|
+
});
|
|
2203
|
+
await expect(async () => await ep({ id: 1 })).rejects.toMatchObject({
|
|
2204
|
+
status: 400,
|
|
2205
|
+
});
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
it('No Content-Type returns text (backward compat)', async () => {
|
|
2209
|
+
nock(/.*/)
|
|
2210
|
+
.defaultReplyHeaders({
|
|
2211
|
+
'Access-Control-Allow-Origin': '*',
|
|
2212
|
+
})
|
|
2213
|
+
.get('/files/1')
|
|
2214
|
+
.reply(200, 'no content type');
|
|
2215
|
+
|
|
2216
|
+
const ep = new RestEndpoint({
|
|
2217
|
+
path: 'http\\://test.com/files/:id',
|
|
2218
|
+
});
|
|
2219
|
+
const result = await ep({ id: 1 });
|
|
2220
|
+
expect(result).toBe('no content type');
|
|
2221
|
+
});
|
|
2222
|
+
});
|
|
1739
2223
|
const proto = Object.prototype;
|
|
1740
2224
|
const gpo = Object.getPrototypeOf;
|
|
1741
2225
|
|
|
@@ -1361,6 +1361,53 @@ describe('resource()', () => {
|
|
|
1361
1361
|
expect(result.current.task3?.status).toEqual('in-progress');
|
|
1362
1362
|
});
|
|
1363
1363
|
|
|
1364
|
+
it('extender body types should match mutation endpoint semantics', () => {
|
|
1365
|
+
// resource without explicit body: extenders derive from Partial<Denormalize<schema>>
|
|
1366
|
+
type PushBody = (typeof UserResource)['getList']['push']['body'];
|
|
1367
|
+
type MoveBody = (typeof UserResource)['getList']['move']['body'];
|
|
1368
|
+
type RemoveBody = (typeof UserResource)['getList']['remove']['body'];
|
|
1369
|
+
|
|
1370
|
+
// push (POST) body should not be any
|
|
1371
|
+
// @ts-expect-error
|
|
1372
|
+
({}) as PushBody satisfies number;
|
|
1373
|
+
// move (PATCH) body should not be any
|
|
1374
|
+
// @ts-expect-error
|
|
1375
|
+
({}) as MoveBody satisfies number;
|
|
1376
|
+
// remove (PATCH) body should not be any
|
|
1377
|
+
// @ts-expect-error
|
|
1378
|
+
({}) as RemoveBody satisfies number;
|
|
1379
|
+
|
|
1380
|
+
// resource with explicit body: PATCH extenders should accept partial body
|
|
1381
|
+
interface MyBody {
|
|
1382
|
+
title: string;
|
|
1383
|
+
content: string;
|
|
1384
|
+
}
|
|
1385
|
+
const TypedResource = resource({
|
|
1386
|
+
path: 'http\\://test.com/articles/:id',
|
|
1387
|
+
schema: User,
|
|
1388
|
+
body: {} as MyBody,
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
// POST extenders: push requires full body
|
|
1392
|
+
() => TypedResource.getList.push({ title: 'hi', content: 'there' });
|
|
1393
|
+
// @ts-expect-error - push requires full body, not partial
|
|
1394
|
+
() => TypedResource.getList.push({ title: 'hi' });
|
|
1395
|
+
|
|
1396
|
+
// PATCH extenders: move accepts partial body (like partialUpdate)
|
|
1397
|
+
() => TypedResource.getList.move({ id: '1' }, { title: 'hi' });
|
|
1398
|
+
() => TypedResource.getList.move({ id: '1' }, { content: 'there' });
|
|
1399
|
+
// @ts-expect-error - move rejects invalid fields
|
|
1400
|
+
() => TypedResource.getList.move({ id: '1' }, { invalid: 'field' });
|
|
1401
|
+
|
|
1402
|
+
// PATCH extenders: remove accepts partial body
|
|
1403
|
+
() => TypedResource.getList.remove({ group: 'a' }, { title: 'hi' });
|
|
1404
|
+
// @ts-expect-error - remove rejects invalid fields
|
|
1405
|
+
() => TypedResource.getList.remove({ group: 'a' }, { invalid: 'field' });
|
|
1406
|
+
|
|
1407
|
+
// partialUpdate also accepts partial body
|
|
1408
|
+
() => TypedResource.partialUpdate({ id: '1' }, { title: 'hi' });
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1364
1411
|
it.each([
|
|
1365
1412
|
{
|
|
1366
1413
|
response: {
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type {
|
|
|
14
14
|
AddEndpoint,
|
|
15
15
|
PaginationFieldEndpoint,
|
|
16
16
|
PaginationEndpoint,
|
|
17
|
+
ContentType,
|
|
17
18
|
} from './RestEndpoint.js';
|
|
18
19
|
export type { RestEndpoint as IRestEndpoint } from './RestEndpointTypes.js';
|
|
19
20
|
export { getUrlBase, getUrlTokens } from './RestHelpers.js';
|
|
@@ -46,5 +47,6 @@ export type {
|
|
|
46
47
|
PathArgsAndSearch,
|
|
47
48
|
} from './pathTypes.js';
|
|
48
49
|
|
|
50
|
+
export type { CollectionOptions } from '@data-client/endpoint';
|
|
49
51
|
export * from '@data-client/endpoint';
|
|
50
52
|
export * from './RestEndpoint.js';
|