ketting
Version:
Opinionated HATEOAS / Rest client.
237 lines (191 loc) • 4.46 kB
text/typescript
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 = {
[]: string | number | string[] | number[];
};