UNPKG

ketting

Version:

Opiniated HATEAOS / Rest client.

293 lines (224 loc) 7.89 kB
import Resource from './resource'; import { LinkVariables, LinkNotFound } from './link'; import { resolve } from './util/uri'; import { expand } from './util/uri-template'; /** * Base interface for both FollowOne and FollowAll */ abstract class FollowPromise<T> implements PromiseLike<T> { protected prefetchEnabled: boolean; protected preferPushEnabled: boolean; protected preferTranscludeEnabled: boolean; protected useHeadEnabled: boolean; constructor() { this.prefetchEnabled = false; this.preferPushEnabled = false; this.preferTranscludeEnabled = false; this.useHeadEnabled = false; } preFetch(): this { this.prefetchEnabled = true; return this; } preferPush(): this { this.preferPushEnabled = true; return this; } preferTransclude(): this { this.preferTranscludeEnabled = true; return this; } /** * Use a HTTP HEAD request to fetch the links. * * This is useful when interacting with servers that embed links in Link * Headers. */ useHead(): this { this.useHeadEnabled = true; return this; } abstract then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>; abstract catch<TResult1 = T, TResult2 = never>(onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>; } /** * The FollowPromise class is what's being returned from follow() functions. * * It's 'PromiseLike', which means you can treat it like a Promise, and it * can be awaited. When used as a Promise, it resolves to the Resource object * that was followed. * * In addition to being a Promise<Resource> stand-in, it also exposes other * functions, namely: * * * `follow()` to allow a user to chain several follow() functions to do * several 'hops' all at once. * * `followAll()`, allowing a user to call `followAll()` at the end of a * chain. */ export class FollowPromiseOne<T = any> extends FollowPromise<Resource<T>> { private resource: Resource | Promise<Resource>; private rel: string; private variables?: LinkVariables; constructor(resource: Resource | Promise<Resource>, rel: string, variables?: LinkVariables) { super(); this.resource = resource; this.rel = rel; this.variables = variables; } /** * This 'then' function behaves like a Promise then() function. * * This method signature is pretty crazy, but trust that it's pretty much * like any then() method on a promise. */ then<TResult1 = Resource<T>, TResult2 = never>( onfulfilled?: ((value: Resource<T>) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: Error) => TResult2 | PromiseLike<TResult2>) | null | undefined ): Promise<TResult1 | TResult2> { return this.fetchLinkedResource().then(onfulfilled, onrejected); } /** * This 'catch' function behaves like a Promise catch() function. */ catch<TResult1 = any, TResult2 = never>(onrejected?: ((reason: Error) => TResult2 | PromiseLike<TResult2>) | null | undefined): Promise<TResult1 | TResult2> { return this.fetchLinkedResource().then(undefined, onrejected); } /** * Implementation of a Promise.finally function */ finally<TResult1 = any>(onfinally: () => TResult1 | PromiseLike<TResult1>): Promise<TResult1> { return this.then( () => onfinally(), () => onfinally() ); } /** * Follow another link immediately after following this link. * * This allows you to follow several hops of links in one go. * * For example: resource.follow('foo').follow('bar'); */ follow<TNested = any>(rel: string, variables?: LinkVariables): FollowPromiseOne<TNested> { return new FollowPromiseOne(this.fetchLinkedResource(), rel, variables); } /** * Follows a set of links immediately after following this link. * * For example: resource.follow('foo').followAll('item'); */ followAll<TNested = any>(rel: string): FollowPromiseMany<TNested> { return new FollowPromiseMany(this.fetchLinkedResource(), rel); } /** * This function does the actual fetching of the linked * resource. */ private async fetchLinkedResource(): Promise<Resource<T>> { const resource = await this.resource; const headers: { [name: string]: string } = {}; if (this.preferPushEnabled) { headers['Prefer-Push'] = this.rel; } if (!this.useHeadEnabled && this.preferTranscludeEnabled) { headers.Prefer = 'transclude=' + this.rel; } let state; if (this.useHeadEnabled) { state = await resource.head({headers}); } else { state = await resource.get({ headers }); } const link = state.links.get(this.rel); if (!link) throw new LinkNotFound(`Link with rel ${this.rel} on ${state.uri} not found`); let href; if (link.templated) { href = expand(link, this.variables || {}); } else { href = resolve(link); } const newResource = resource.go(href); if (this.prefetchEnabled) { newResource.get().catch( err => { // eslint-disable-next-line no-console console.warn('Error while prefetching linked resource', err); }); } return newResource; } } /** */ export class FollowPromiseMany<T = any> extends FollowPromise<Resource<T>[]> { private resource: Resource | Promise<Resource>; private rel: string; constructor(resource: Resource | Promise<Resource>, rel: string) { super(); this.resource = resource; this.rel = rel; } /** * This 'then' function behaves like a Promise then() function. */ then<TResult1 = Resource<T>[], TResult2 = never>( onfulfilled?: ((value: Resource<T>[]) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: Error) => TResult2 | PromiseLike<TResult2>) | null | undefined ): Promise<TResult1 | TResult2> { return this.fetchLinkedResources().then(onfulfilled, onrejected); } /** * This 'catch' function behaves like a Promise catch() function. */ catch<TResult1 = any, TResult2 = never>(onrejected?: ((reason: Error) => TResult2 | PromiseLike<TResult2>) | null | undefined): Promise<TResult1 | TResult2> { return this.fetchLinkedResources().then(undefined, onrejected); } /** * Implementation of a Promise.finally function */ finally<TResult1 = any>(onfinally: () => TResult1 | PromiseLike<TResult1>): Promise<TResult1> { return this.then( () => onfinally(), () => onfinally() ); } /** * This function does the actual fetching, to obtained the url * of the linked resource. It returns the Resource object. */ private async fetchLinkedResources(): Promise<Resource<T>[]> { const resource = await this.resource; const headers: { [name: string]: string } = {}; if (this.preferPushEnabled) { headers['Prefer-Push'] = this.rel; } if (!this.useHeadEnabled && this.preferTranscludeEnabled) { headers.Prefer = 'transclude=' + this.rel; } let state; if (this.useHeadEnabled) { state = await resource.head({headers}); } else { state = await resource.get({ headers }); } const links = state.links.getMany(this.rel); let href; const result: Resource<T>[] = []; for (const link of links) { href = resolve(link); const newResource = resource.go(href); result.push(newResource); if (this.prefetchEnabled) { newResource.get().catch( err => { // eslint-disable-next-line no-console console.warn('Error while prefetching linked resource', err); }); } } return result; } }