ketting
Version:
Opiniated HATEAOS / Rest client.
361 lines • 12.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.factory = exports.HalState = void 0;
const base_state_1 = require("./base-state");
const util_1 = require("../http/util");
const link_1 = require("../link");
const uri_1 = require("../util/uri");
/**
* Represents a resource state in the HAL format
*/
class HalState extends base_state_1.BaseState {
serializeBody() {
return JSON.stringify({
_links: this.serializeLinks(),
...this.data
});
}
serializeLinks() {
const links = {
self: { href: this.uri },
};
for (const link of this.links.getAll()) {
const { rel, context, ...attributes } = link;
if (rel === 'self') {
// skip
continue;
}
if (links[rel] === undefined) {
// First link of its kind
links[rel] = attributes;
}
else if (Array.isArray(links[rel])) {
// Add link to link array.
links[rel].push(attributes);
}
else {
// 1 link with this rel existed, so we will transform it to an array.
links[rel] = [links[rel], attributes];
}
}
return links;
}
clone() {
return new HalState({
client: this.client,
uri: this.uri,
data: this.data,
headers: new Headers(this.headers),
links: new link_1.Links(this.links.defaultContext, this.links.getAll()),
actions: this.actionInfo,
});
}
}
exports.HalState = HalState;
/**
* Turns a HTTP response into a HalState
*/
const factory = async (client, uri, response) => {
const body = await response.json();
const links = (0, util_1.parseLink)(uri, response.headers.get('Link'));
// The HAL factory is also respondible for plain JSON, which might be an
// array.
if (Array.isArray(body)) {
return new HalState({
client,
uri,
data: body,
headers: response.headers,
links,
});
}
links.add(...parseHalLinks(uri, body));
// Remove _links and _embedded from body
const { _embedded, _links, _templates, ...newBody } = body;
return new HalState({
client,
uri: uri,
data: newBody,
headers: response.headers,
links: links,
embedded: parseHalEmbedded(client, uri, body, response.headers),
actions: parseHalForms(uri, body),
});
};
exports.factory = factory;
/**
* Parse the Hal _links object and populate the 'links' property.
*/
function parseHalLinks(context, body) {
if (body._links === undefined) {
return [];
}
const result = [];
/**
* We're capturing all rel-link pairs so we don't duplicate them if they
* re-appear in _embedded.
*
* Links that are embedded _should_ appear in both lists, but not everyone
* does this.
*/
const foundLinks = new Set();
for (const [relType, links] of Object.entries(body._links)) {
const linkList = Array.isArray(links) ? links : [links];
for (const link of linkList) {
foundLinks.add(relType + ';' + link.href);
}
result.push(...parseHalLink(context, relType, linkList));
}
if (body._embedded) {
// eslint-disable-next-line prefer-const
for (let [rel, innerBodies] of Object.entries(body._embedded)) {
for (const innerBody of Array.isArray(innerBodies) ? innerBodies : [innerBodies]) {
const href = innerBody?._links?.self?.href;
if (!href) {
continue;
}
if (foundLinks.has(rel + ';' + href)) {
continue;
}
result.push({
rel: rel,
href: href,
context: context,
});
}
}
}
return result;
}
/**
* Parses a single HAL link from a _links object
*/
function parseHalLink(context, rel, links) {
const result = [];
for (const link of links) {
result.push({
rel,
context,
...link,
});
}
return result;
}
/**
* Parse the HAL _embedded object. Right now we're just grabbing the
* information from _embedded and turn it into links.
*/
function parseHalEmbedded(client, context, body, headers) {
if (body._embedded === undefined || !body._embedded) {
return [];
}
const result = [];
for (const embedded of Object.values(body._embedded)) {
let embeddedList;
if (!Array.isArray(embedded)) {
embeddedList = [embedded];
}
else {
embeddedList = embedded;
}
for (const embeddedItem of embeddedList) {
if (embeddedItem._links?.self?.href === undefined) {
// eslint-disable-next-line no-console
console.warn('An item in _embedded was ignored. Each item must have a single "self" link');
continue;
}
const embeddedSelf = (0, uri_1.resolve)(context, embeddedItem._links?.self?.href);
// Remove _links and _embedded from body
const { _embedded, _links, ...newBody } = embeddedItem;
result.push(new HalState({
client,
uri: embeddedSelf,
data: newBody,
headers: new Headers({
'Content-Type': headers.get('Content-Type'),
}),
links: new link_1.Links(embeddedSelf, parseHalLinks(context, embeddedItem)),
// Parsing nested embedded items. Note that we assume that the base url is relative to
// the outermost parent, not relative to the embedded item. HAL is not clear on this.
embedded: parseHalEmbedded(client, embeddedSelf, embeddedItem, headers),
actions: parseHalForms(embeddedSelf, embeddedItem)
}));
}
}
return result;
}
function parseHalForms(context, body) {
if (!body._templates)
return [];
return Object.entries(body._templates).map(([key, hf]) => {
return {
uri: (0, uri_1.resolve)(context, hf.target || ''),
name: key,
title: hf.title,
method: hf.method,
contentType: hf.contentType || 'application/json',
fields: hf.properties ? hf.properties.map(prop => parseHalField(prop)) : [],
};
});
}
function parseHalField(halField) {
switch (halField.type) {
case undefined:
case 'text':
case 'search':
case 'tel':
case 'url':
case 'email':
if (halField.options) {
const baseField = {
name: halField.name,
type: 'select',
label: halField.prompt,
required: halField.required || false,
readOnly: halField.readOnly || false,
multiple: halField.options.multiple,
value: (halField.options.selectedValues || halField.value)
};
const labelField = halField.options.promptField || 'prompt';
const valueField = halField.options.valueField || 'value';
if (isInlineOptions(halField.options)) {
const options = {};
for (const entry of halField.options.inline) {
if (typeof entry === 'string') {
options[entry] = entry;
}
else {
options[entry[valueField]] = entry[labelField];
}
}
return {
...baseField,
options
};
}
else {
return {
...baseField,
dataSource: {
href: halField.options.link.href,
type: halField.options.link.type,
labelField,
valueField,
}
};
}
}
else {
return {
name: halField.name,
type: halField.type ?? 'text',
required: halField.required || false,
readOnly: halField.readOnly || false,
value: halField.value,
pattern: halField.regex ? new RegExp(halField.regex) : undefined,
label: halField.prompt,
placeholder: halField.placeholder,
minLength: halField.minLength,
maxLength: halField.maxLength,
};
}
case 'hidden':
return {
name: halField.name,
type: 'hidden',
required: halField.required || false,
readOnly: halField.readOnly || false,
value: halField.value,
label: halField.prompt,
placeholder: halField.placeholder,
};
case 'textarea':
return {
name: halField.name,
type: halField.type,
required: halField.required || false,
readOnly: halField.readOnly || false,
value: halField.value,
label: halField.prompt,
placeholder: halField.placeholder,
cols: halField.cols,
rows: halField.rows,
minLength: halField.minLength,
maxLength: halField.maxLength,
};
case 'password':
return {
name: halField.name,
type: halField.type,
required: halField.required || false,
readOnly: halField.readOnly || false,
label: halField.prompt,
placeholder: halField.placeholder,
minLength: halField.minLength,
maxLength: halField.maxLength,
};
case 'date':
case 'month':
case 'week':
case 'time':
return {
name: halField.name,
type: halField.type,
value: halField.value,
required: halField.required || false,
readOnly: halField.readOnly || false,
label: halField.prompt,
min: halField.min,
max: halField.max,
step: halField.step,
};
case 'number':
case 'range':
return {
name: halField.name,
type: halField.type,
value: halField.value ? +halField.value : undefined,
required: halField.required || false,
readOnly: halField.readOnly || false,
label: halField.prompt,
min: halField.min,
max: halField.max,
step: halField.step,
};
case 'datetime-local':
return {
name: halField.name,
type: halField.type,
value: halField.value ? new Date(halField.value) : undefined,
required: halField.required || false,
readOnly: halField.readOnly || false,
label: halField.prompt,
min: halField.min,
max: halField.max,
step: halField.step,
};
case 'color':
return {
name: halField.name,
type: halField.type,
required: halField.required || false,
readOnly: halField.readOnly || false,
label: halField.prompt,
value: halField.value,
};
case 'radio':
case 'checkbox':
return {
name: halField.name,
type: halField.type,
required: halField.required || false,
readOnly: halField.readOnly || false,
label: halField.prompt,
value: !!halField.value,
};
}
}
function isInlineOptions(options) {
return options.inline !== undefined;
}
//# sourceMappingURL=hal.js.map