UNPKG

ketting

Version:

Opinionated HATEOAS / Rest client.

237 lines (191 loc) 4.46 kB
import { LinkHints } from 'hal-types'; import { resolve } from './util/uri.js'; export type Link = { /** * Target URI */ href: string; /** * Context URI. * * Used to resolve relative URIs */ context: string; /** * Relation type */ rel: string; /** * Link title */ title?: string; /** * Content type hint of the target resource */ type?: string; /** * Anchor. * * This describes where the link is linked from, from for example * a fragment in the current document */ anchor?: string; /** * Language of the target resource */ hreflang?: string; /** * HTML5 media attribute */ media?: string; /** * If templated is set to true, the href is a templated URI. */ templated?: boolean; /** * Link hints, as defined in draft-nottingham-link-hint */ hints?: LinkHints; /** * Link name * * This is sometimes used as a machine-readable secondary key for links. * * This is at least used in HAL, but there may be other formats: * * @see https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-06#section-5.5 */ name?: string; } type NewLink = Omit<Link, 'context'>; /** * Links container, providing an easy way to manage a set of links. */ export class Links { private store: Map<string, Link[]>; constructor(public defaultContext: string, links?: Link[] | Links) { this.store = new Map(); if (links) { if (links instanceof Links) { this.add(...links.getAll()); } else { for (const link of links) { this.add(link); } } } } /** * Adds a link to the list */ add(...links: (Link | NewLink)[]): void add(rel: string, href: string): void add(...args: any[]): void { let links: Link[]; if (typeof args[0] === 'string') { links = [{ rel: args[0], href: args[1], context: this.defaultContext, }]; } else { links = args.map( link => { return { context: this.defaultContext, ...link };} ); } for(const link of links) { if (this.store.has(link.rel)) { this.store.get(link.rel)!.push(link); } else { this.store.set(link.rel, [link]); } } } /** * Set a link * * If a link with the provided 'rel' already existed, it will be overwritten. */ set(link: Link | NewLink): void set(rel: string, href: string): void set(arg1: any, arg2?: any): void { let link: Link; if (typeof arg1 === 'string') { link = { rel: arg1, href: arg2, context: this.defaultContext, }; } else { link = { context: this.defaultContext, ...arg1, }; } this.store.set(link.rel, [link]); } /** * Return a single link by its 'rel'. * * If the link does not exist, undefined is returned. */ get(rel: string): Link|undefined { const links = this.store.get(rel); if (!links || links.length < 0) { return undefined; } return links[0]; } /** * Delete all links with the given 'rel'. * * If the second argument is provided, only links that match the href will * be removed. */ delete(rel: string, href?: string): void { if (href===undefined) { this.store.delete(rel); return; } const uris = this.store.get(rel); if (!uris) return; this.store.delete(rel); const absHref = resolve(this.defaultContext, href); this.store.set(rel, uris.filter(uri => resolve(uri) !== absHref) ); } /** * Return all links that have a given rel. * * If no links with the rel were found, an empty array is returned. */ getMany(rel: string): Link[] { return this.store.get(rel) || []; } /** * Return all links. */ getAll(): Link[] { const result = []; for(const links of this.store.values()) { result.push(...links); } return result; } /** * Returns true if at least 1 link with the given rel exists. */ has(rel: string): boolean { return this.store.has(rel); } } /** * The LinkNotFound error gets thrown whenever something tries to follow a * link by its rel, that doesn't exist */ export class LinkNotFound extends Error {} /** * A key->value map of variables to place in a templated link */ export type LinkVariables = { [key: string]: string | number | string[] | number[]; };