UNPKG

@data-client/rest

Version:

Quickly define typed REST resources and endpoints

312 lines (284 loc) 9.28 kB
import { schema, Entity, Schema } from '@data-client/endpoint'; import { useController } from '@data-client/react'; import { useSuspense } from '@data-client/react'; import { CacheProvider } from '@data-client/react'; import nock from 'nock'; import { makeRenderDataClient } from '../../../test'; import { ResourcePath } from '../pathTypes'; import resource from '../resource'; import RestEndpoint from '../RestEndpoint'; import { payload, createPayload, users, nested, moreNested, paginatedFirstPage, paginatedSecondPage, } from '../test-fixtures'; const { Collection } = schema; export class User extends Entity { readonly id: number | undefined = undefined; readonly username: string = ''; readonly email: string = ''; readonly isAdmin: boolean = false; } export const UserResource = resource({ path: 'http\\://test.com/user/:id', schema: User, }); export class PaginatedArticle extends Entity { readonly id: number | undefined = undefined; readonly title: string = ''; readonly content: string = ''; readonly author: number | null = null; readonly tags: string[] = []; static schema = { author: User, }; } function createPaginatableResource<U extends ResourcePath, S extends Schema>({ path, schema, Endpoint = RestEndpoint, }: { readonly path: U; readonly schema: S; readonly Endpoint?: typeof RestEndpoint; }) { const baseResource = resource({ path, schema, Endpoint }); const getList = baseResource.getList.extend({ path: 'http\\://test.com/article-paginated', schema: { nextPage: '', data: { results: new Collection([PaginatedArticle]) }, }, }); const getNextPage = getList.paginated((v: { cursor: string | number }) => []); return { ...baseResource, getList, getNextPage, }; } const PaginatedArticleResource = createPaginatableResource({ path: 'http\\://test.com/article-paginated/:id', schema: PaginatedArticle, }); export class UrlArticle extends PaginatedArticle { readonly url: string = 'happy.com'; } describe('resource()', () => { const renderDataClient: ReturnType<typeof makeRenderDataClient> = makeRenderDataClient(CacheProvider); let mynock: nock.Scope; beforeEach(() => { nock(/.*/) .persist() .defaultReplyHeaders({ 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json', }) .options(/.*/) .reply(200) .get(`/article-cooler/${payload.id}`) .reply(200, payload) .delete(`/article-cooler/${payload.id}`) .reply(204, '') .delete(`/article/${payload.id}`) .reply(200, {}) .get(`/article-cooler/0`) .reply(403, {}) .get(`/article-cooler/666`) .reply(200, '') .get(`/article-cooler`) .reply(200, nested) .post(`/article-cooler`) .reply(200, createPayload) .get(`/user`) .reply(200, users); mynock = nock(/.*/).defaultReplyHeaders({ 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json', }); }); afterEach(() => { nock.cleanAll(); }); it('should handle simple urls', () => { expect(UserResource.get.url({ id: '5' })).toBe('http://test.com/user/5'); expect(UserResource.get.url({ id: '100' })).toBe( 'http://test.com/user/100', ); /*expect(UserResource.getList.url({ bob: '100' })).toBe( 'http://test.com/user?bob=100', ); expect(UserResource.create.url({ bob: '100' })).toBe( 'http://test.com/user', );*/ expect( UserResource.update.url({ id: '100' }, { id: 100, username: 'bob' }), ).toBe('http://test.com/user/100'); // @ts-expect-error () => UserResource.get.url({ sdf: '5' }); }); it('should handle multiarg urls', () => { const MyUserResource = resource({ path: 'http\\://test.com/groups/:group/users/:id', schema: User, }); expect(MyUserResource.get.url({ group: 'big', id: '5' })).toBe( 'http://test.com/groups/big/users/5', ); expect(MyUserResource.get.url({ group: 'big', id: '100' })).toBe( 'http://test.com/groups/big/users/100', ); /*expect(MyUserResource.getList.url({ group: 'big', bob: '100' })).toBe( 'http://test.com/groups/big/users?bob=100', );*/ expect( MyUserResource.create.url({ group: 'big' }, { username: '100' }), ).toBe('http://test.com/groups/big/users'); expect( MyUserResource.update.url( { group: 'big', id: '100' }, { id: 100, username: 'bob' }, ), ).toBe('http://test.com/groups/big/users/100'); // missing required expect(() => // @ts-expect-error MyUserResource.get.url({ id: '5' }), ).toThrow(); // extra fields () => MyUserResource.get.url({ group: 'mygroup', id: '5', // @ts-expect-error notexisting: 'hi', }); // @ts-expect-error () => useSuspense(MyUserResource.get, { id: '5' }); // @ts-expect-error () => useSuspense(MyUserResource.get); () => useSuspense(MyUserResource.get, { group: 'yay', id: '5' }); }); it('should automatically name methods', () => { expect(PaginatedArticleResource.get.name).toBe('PaginatedArticle.get'); expect(PaginatedArticleResource.create.name).toBe( 'PaginatedArticle.create', ); expect(PaginatedArticleResource.getList.name).toBe( 'PaginatedArticle.getList', ); expect(PaginatedArticleResource.delete.name).toBe( 'PaginatedArticle.delete', ); }); it('should update on get for a paginated resource', async () => { mynock.get(`/article-paginated`).reply(200, paginatedFirstPage); mynock.get(`/article-paginated?cursor=2`).reply(200, paginatedSecondPage); const { result, waitForNextUpdate } = renderDataClient(() => { const { fetch } = useController(); const { data: { results: articles }, nextPage, } = useSuspense(PaginatedArticleResource.getList); return { articles, nextPage, fetch }; }); await waitForNextUpdate(); () => // @ts-expect-error result.current.fetch(PaginatedArticleResource.getNextPage); await result.current.fetch(PaginatedArticleResource.getNextPage, { cursor: 2, }); expect(result.current.articles.map(({ id }) => id)).toEqual([5, 3, 7, 8]); }); it('should deduplicate results', async () => { mynock.get(`/article-paginated`).reply(200, paginatedFirstPage); mynock.get(`/article-paginated?cursor=2`).reply(200, { ...paginatedSecondPage, results: [nested[nested.length - 1], ...moreNested], }); const { result, waitForNextUpdate } = renderDataClient(() => { const { fetch } = useController(); const { data: { results: articles }, nextPage, } = useSuspense(PaginatedArticleResource.getList); return { articles, nextPage, fetch }; }); await waitForNextUpdate(); await result.current.fetch(PaginatedArticleResource.getNextPage, { cursor: 2, }); expect(result.current.articles.map(({ id }) => id)).toEqual([5, 3, 7, 8]); }); it('should not deep-merge deeply defined entities', async () => { interface Complex { firstvalue: number; secondthing: { arg?: number; other?: string; }; } class ComplexEntity extends Entity { readonly id: string = ''; readonly complexThing?: Complex = undefined; readonly extra: string = ''; pk() { return this.id; } } const ComplexResource = resource({ path: '/complex-thing/:id', schema: ComplexEntity, }); const firstResponse = { id: '5', complexThing: { firstvalue: 233, secondthing: { arg: 88 }, }, extra: 'hi', }; mynock.get(`/complex-thing/5`).reply(200, firstResponse); const { result, waitForNextUpdate } = renderDataClient(() => { const { fetch } = useController(); const article = useSuspense(ComplexResource.get, { id: '5' }); return { article, fetch }; }); await waitForNextUpdate(); expect(result.current.article).toEqual(firstResponse); const secondResponse = { id: '5', complexThing: { firstvalue: 5, secondthing: { other: 'hi' }, }, }; mynock.get(`/complex-thing/5`).reply(200, secondResponse); await result.current.fetch(ComplexResource.get, { id: '5', }); expect(result.current.article).toEqual({ ...secondResponse, extra: 'hi' }); }); it('delete() should fallback to params when response is empty object', async () => { mynock.delete(`/article-paginated/500`).reply(200, {}); const res = await PaginatedArticleResource.delete({ id: 500 }); expect(res).toEqual({ id: 500 }); }); it('delete() should fallback to params when response is undefined', async () => { mynock.delete(`/article-paginated/500`).reply(204, undefined); const res = await PaginatedArticleResource.delete({ id: 500 }); expect(res).toEqual({ id: 500 }); }); it('should spread `url` member', () => { const entity = UrlArticle.fromJS({ url: 'five' }); const spread = { ...entity }; expect(spread.url).toBe('five'); expect(Object.hasOwn(entity, 'url')).toBeTruthy(); }); });