@foxy.io/sdk
Version:
Universal SDK for a full server-side and a limited in-browser access to Foxy hAPI.
229 lines (228 loc) • 10.5 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { storageV8N, v8n } from '../v8n.js';
import consola from 'consola';
import { Request } from 'cross-fetch';
import { ResolutionError } from './ResolutionError.js';
import { Response } from './Response.js';
/**
* Serializes object zoom definition using hAPI format.
*
* @param prefix Curie prefix.
* @param zoom Zoom definition as object.
* @returns Serialized zoom parameter value.
*/
function stringifyZoom(prefix, zoom) {
const scope = prefix === '' ? '' : prefix + ':';
if (typeof zoom === 'string')
return scope + zoom;
if (Array.isArray(zoom))
return zoom.map(v => stringifyZoom(prefix, v)).join();
return Object.entries(zoom)
.map(([key, value]) => stringifyZoom(scope + key, value))
.join();
}
/**
* Serializes object order definition using hAPI format.
*
* @param order Order definition as object.
* @returns Serialized order parameter value.
*/
function stringifyOrder(order) {
if (typeof order === 'string')
return order;
if (Array.isArray(order)) {
return order.map(item => stringifyOrder(item)).join();
}
return Object.entries(order)
.map(([key, value]) => `${key} ${value}`)
.join();
}
/**
* Base class representing a resource node that can be fetched,
* created, updated or deleted. You shouldn't need to create instances
* of this class unless you're building a custom API client with our SDK.
*/
export class Node {
constructor(init) {
Node.v8n.classConstructor.check(init);
this._path = init.path;
this._fetch = init.fetch;
this._cache = init.cache;
this._console = init.console;
}
/**
* Resolves the URL of this node and sends a GET request
* using provided parameters.
*
* @param query Query parameters such as zoom, fields etc.
* @returns Instance of {@link APIResponse} representing this resource.
*/
get(query) {
return __awaiter(this, void 0, void 0, function* () {
Node.v8n.get.check(query);
const url = yield this._resolve();
const { filters, fields, offset, limit, order, zoom } = query !== null && query !== void 0 ? query : {};
if (filters !== undefined) {
filters.forEach((filter) => {
const [key, value = ''] = filter.split('=');
if (key)
url.searchParams.append(key, value);
});
}
if (fields !== undefined)
url.searchParams.set('fields', fields.join(','));
if (offset !== undefined)
url.searchParams.set('offset', String(offset));
if (limit !== undefined)
url.searchParams.set('limit', String(limit));
if (order !== undefined)
url.searchParams.set('order', stringifyOrder(order));
if (zoom !== undefined)
url.searchParams.set('zoom', stringifyZoom('', zoom));
const response = yield this._fetch(new Request(url.toString()));
const config = { cache: this._cache, console: this._console, fetch: this._fetch };
return new Response(Object.assign(Object.assign(Object.assign({}, config), response), { body: yield response.text() }));
});
}
/**
* Resolves the URL of this node and sends a PUT request
* with provided properties, replacing the existing resource.
*
* @param body Complete resource object.
* @returns Instance of {@link APIResponse} representing this resource.
*/
put(body) {
return __awaiter(this, void 0, void 0, function* () {
const url = yield this._resolve();
const request = new Request(url.toString(), { body: JSON.stringify(body), method: 'PUT' });
const response = yield this._fetch(request);
const config = { cache: this._cache, console: this._console, fetch: this._fetch };
return new Response(Object.assign(Object.assign(Object.assign({}, config), response), { body: yield response.text() }));
});
}
/**
* Resolves the URL of this node and sends a POST request
* with provided properties, creating a resource or triggering an action.
*
* @param body Complete resource object.
* @returns Instance of {@link APIResponse} representing this resource.
*/
post(body) {
return __awaiter(this, void 0, void 0, function* () {
const url = yield this._resolve();
const request = new Request(url.toString(), { body: JSON.stringify(body), method: 'POST' });
const response = yield this._fetch(request);
const config = { cache: this._cache, console: this._console, fetch: this._fetch };
return new Response(Object.assign(Object.assign(Object.assign({}, config), response), { body: yield response.text() }));
});
}
/**
* Resolves the URL of this node and sends a PATCH request
* with provided properties, updating this resource.
*
* @param body Partial resource object.
* @returns Instance of {@link APIResponse} representing this resource.
*/
patch(body) {
return __awaiter(this, void 0, void 0, function* () {
const url = yield this._resolve();
const request = new Request(url.toString(), { body: JSON.stringify(body), method: 'PATCH' });
const response = yield this._fetch(request);
const config = { cache: this._cache, console: this._console, fetch: this._fetch };
return new Response(Object.assign(Object.assign(Object.assign({}, config), response), { body: yield response.text() }));
});
}
/**
* Resolves the URL of this node and sends a DELETE request,
* removing this resource.
*
* @returns Instance of {@link APIResponse} representing this resource.
*/
delete() {
return __awaiter(this, void 0, void 0, function* () {
const url = yield this._resolve();
const request = new Request(url.toString(), { method: 'DELETE' });
const response = yield this._fetch(request);
const config = { cache: this._cache, console: this._console, fetch: this._fetch };
return new Response(Object.assign(Object.assign(Object.assign({}, config), response), { body: yield response.text() }));
});
}
/**
* Resource path builder. Calling this method instructs our
* SDK to find the provided curie in this resource's links and
* navigate to its location on request.
*
* @param curie Curie to follow.
* @returns Instance of {@link APINode} representing the resource at curie location.
*/
follow(curie) {
Node.v8n.follow.check(curie);
const config = { cache: this._cache, console: this._console, fetch: this._fetch };
const path = this._path.concat(curie);
return new Node(Object.assign(Object.assign({}, config), { path }));
}
/**
* Resolves resource URL from a curie chain. The first element in the path
* must be a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL).
*
* @returns Resolved URL.
* @throws Throws {@link APIResolutionError} when once of the resources can't be reached.
*/
_resolve() {
return __awaiter(this, void 0, void 0, function* () {
if (this._path.length === 1)
return new URL(this._path[0].toString());
const [baseURL, curie] = this._path;
const key = `${baseURL.toString()} > ${curie}`;
const config = { cache: this._cache, console: this._console, fetch: this._fetch };
this._console.trace(`Trying to resolve ${key}...`);
const cachedURL = this._cache.getItem(key);
if (cachedURL) {
this._console.success(`Resolved ${key} to ${cachedURL.toString()} using cache.`);
const reducedPath = [new URL(cachedURL), ...this._path.slice(2)];
return new Node(Object.assign(Object.assign({}, config), { path: reducedPath }))._resolve();
}
const response = yield this._fetch(baseURL.toString());
if (response.ok) {
const json = yield response.clone().json();
if (json._links[curie]) {
const url = new URL(json._links[curie].href);
const reducedPath = [url, ...this._path.slice(2)];
this._cache.setItem(key, url.toString());
this._console.trace(`Cached ${url.toString()} for ${key}.`);
this._console.success(`Resolved ${key} to ${url.toString()} online.`);
return new Node(Object.assign(Object.assign({}, config), { path: reducedPath }))._resolve();
}
}
this._console.error(`Failed to resolve ${key}.`);
throw new ResolutionError(response);
});
}
}
Node.v8n = {
classConstructor: v8n().schema({
cache: storageV8N,
console: v8n().instanceOf(consola.constructor),
fetch: v8n().typeOf('function'),
path: v8n().curieChain(),
}),
follow: v8n().string(),
get: v8n().optional(v8n().schema({
fields: v8n().optional(v8n().array().every.string()),
filters: v8n().optional(v8n().array().every.string()),
limit: v8n().optional(v8n().number()),
offset: v8n().optional(v8n().number()),
order: v8n().optional(v8n().passesAnyOf(v8n().string(), v8n().object(), v8n().array())),
zoom: v8n().optional(v8n().passesAnyOf(v8n().string(), v8n().object(), v8n().array())),
})),
};
Node.ResolutionError = ResolutionError;
Node.Response = Response;