ketting
Version:
Opinionated HATEOAS / Rest client.
197 lines • 6.19 kB
JavaScript
import { Links, LinkNotFound } from '../link.js';
import { ActionNotFound, SimpleAction } from '../action.js';
import { resolve } from '../util/uri.js';
import { expand } from '../util/uri-template.js';
import { entityHeaderNames } from '../http/util.js';
import { serializeBody } from '#state-serialized-body';
/**
* Implements a State object for HEAD responses
*/
export class BaseHeadState {
uri;
/**
* Timestamp of when the State was first generated
*/
timestamp;
/**
* The full list of HTTP headers that were sent with the response.
*/
headers;
/**
* All links associated with the resource.
*/
links;
/**
* Reference to main client that created this state
*/
client;
constructor(init) {
this.client = init.client;
this.uri = init.uri;
this.headers = init.headers;
this.timestamp = Date.now();
this.links = init.links;
}
/**
* Follows a relationship, based on its reltype. For example, this might be
* 'alternate', 'item', 'edit' or a custom url-based one.
*
* This function can also follow templated uris. You can specify uri
* variables in the optional variables argument.
*/
follow(rel, variables) {
const link = this.links.get(rel);
if (!link)
throw new LinkNotFound(`Link with rel ${rel} on ${this.uri} not found`);
let href;
if (link.templated) {
href = expand(link, variables || {});
}
else {
href = resolve(link);
}
if (link.hints?.status === 'deprecated') {
/* eslint-disable-next-line no-console */
console.warn(`[ketting] The ${link.rel} link on ${this.uri} is marked deprecated.`, link);
}
return this.client.go(href);
}
/**
* Follows a relationship based on its reltype. This function returns a
* Promise that resolves to an array of Resource objects.
*
* If no resources were found, the array will be empty.
*/
followAll(rel) {
return this.links.getMany(rel).map(link => {
if (link.hints?.status === 'deprecated') {
/* eslint-disable-next-line no-console */
console.warn(`[ketting] The ${link.rel} link on ${this.uri} is marked deprecated.`, link);
}
const href = resolve(link);
return this.client.go(href);
});
}
/**
* Content-headers are a subset of HTTP headers that related directly
* to the content. The obvious ones are Content-Type.
*
* This set of headers will be sent by the server along with a GET
* response, but will also be sent back to the server in a PUT
* request.
*/
contentHeaders() {
const result = {};
for (const contentHeader of entityHeaderNames) {
if (this.headers.has(contentHeader)) {
result[contentHeader] = this.headers.get(contentHeader);
}
}
return new Headers(result);
}
}
/**
* The Base State provides a convenient way to implement a new State type.
*/
export class BaseState extends BaseHeadState {
data;
embedded;
actionInfo;
constructor(init) {
super(init);
this.data = init.data;
this.actionInfo = init.actions || [];
this.embedded = init.embedded || [];
}
/**
* Return an action by name.
*
* If no name is given, the first action is returned. This is useful for
* formats that only supply 1 action, and no name.
*/
action(name) {
const actionSearchResult = this.doFindAction(name);
if (actionSearchResult === 'NO_ACTION_DEFINED') {
throw new ActionNotFound('This State does not define any actions');
}
if (actionSearchResult === 'NO_ACTION_FOR_THE_PROVIDED_NAME') {
throw new ActionNotFound('This State defines no action');
}
return actionSearchResult;
}
findAction(name) {
const actionSearchResult = this.doFindAction(name);
if (typeof actionSearchResult !== 'object') {
return undefined;
}
return actionSearchResult;
}
doFindAction(name) {
if (!this.actionInfo.length) {
return 'NO_ACTION_DEFINED';
}
if (name === undefined) {
return new SimpleAction(this.client, this.actionInfo[0]);
}
for (const action of this.actionInfo) {
if (action.name === name) {
return new SimpleAction(this.client, action);
}
}
return 'NO_ACTION_FOR_THE_PROVIDED_NAME';
}
/**
* Returns all actions
*/
actions() {
return this.actionInfo.map(action => new SimpleAction(this.client, action));
}
/**
* Checks if the specified action exists.
*
* If no name is given, checks if _any_ action exists.
*/
hasAction(name) {
if (name === undefined)
return this.actionInfo.length > 0;
for (const action of this.actionInfo) {
if (name === action.name) {
return true;
}
}
return false;
}
/**
* Returns a serialization of the state that can be used in a HTTP
* response.
*
* For example, a JSON object might simply serialize using
* JSON.serialize().
*/
serializeBody() {
return serializeBody(this.data);
}
/**
* Certain formats can embed other resources, identified by their
* own URI.
*
* When a format has embedded resources, we will use these to warm
* the cache.
*
* This method returns every embedded resource.
*/
getEmbedded() {
return this.embedded;
}
clone() {
return new BaseState({
client: this.client,
uri: this.uri,
data: this.data,
headers: new Headers(this.headers),
links: new Links(this.links.defaultContext, this.links.getAll()),
actions: this.actionInfo,
});
}
}
//# sourceMappingURL=base-state.js.map