alcaeus
Version:
Hydra Core hypermedia-aware client library
154 lines (153 loc) • 6.95 kB
JavaScript
import { hydra } from '@tpluscode/rdf-ns-builders';
import fromStream from 'rdf-dataset-ext/fromStream.js';
import FetchUtil from './FetchUtil.js';
import { merge } from './helpers/MergeHeaders.js';
import { getAbsoluteUri } from './helpers/uri.js';
import * as DefaultCacheStrategy from './ResourceCacheStrategy.js';
export * from 'alcaeus-core';
function byInProperties(left, right) {
return left.in().terms.length - right.in().terms.length;
}
export class Alcaeus {
constructor(init, fetchUtil) {
this.baseUri = undefined;
this.defaultHeaders = {};
this.defaultRequestInit = {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.log = () => {
};
this.cacheStrategy = { ...DefaultCacheStrategy };
const { resources, environment, rootSelectors, fetch } = init;
this.rootSelectors = rootSelectors;
this.resources = resources;
this.environment = environment;
this._headers = init.Headers;
this._fetch = fetchUtil || FetchUtil(fetch, this._headers);
this.__apiDocumentations = environment.termMap();
}
get apiDocumentations() {
return [...this.__apiDocumentations.values()];
}
async loadResource(id, headers = {}, requestInit, dereferenceApiDocumentation = true) {
const term = typeof id === 'string' ? this.environment.namedNode(id) : id;
let requestHeaders = new this._headers(headers);
const previousResource = this.resources.get(term);
if (previousResource) {
if (!this.cacheStrategy.shouldLoad(previousResource)) {
return previousResource;
}
const cacheHeadersInit = this.cacheStrategy.requestCacheHeaders(previousResource);
if (cacheHeadersInit) {
const cacheHeaders = new this._headers(cacheHeadersInit);
requestHeaders = merge(requestHeaders, cacheHeaders, this._headers);
}
}
const uri = getAbsoluteUri(term.value, this.baseUri);
const defaultRequestInit = typeof this.defaultRequestInit === 'function'
? await this.defaultRequestInit({ uri })
: this.defaultRequestInit;
const response = await this._fetch.resource(uri, {
parsers: this.environment.formats.parsers,
headers: await this.__mergeHeaders(requestHeaders, { uri }),
...{ ...defaultRequestInit, ...requestInit },
});
if (previousResource) {
const etag = response.xhr.headers.get('etag');
const previousEtag = previousResource.response.xhr.headers.get('etag');
const etagsEqual = etag && etag === previousEtag;
if (response.xhr.status === 304 || etagsEqual) {
return previousResource;
}
}
const stream = await response.quadStream();
if (stream) {
const dataset = await fromStream(this.environment.dataset(), stream);
const rootResource = this.__findRootResource(dataset, response);
if (dereferenceApiDocumentation) {
await this.__getApiDocumentation(response, headers);
}
const resources = response.xhr.ok ? this.resources : this.resources.clone();
await resources.set(term, {
response,
dataset,
rootResource,
});
return resources.get(term);
}
return {
response,
};
}
async loadDocumentation(id, headers = {}, requestInit = {}) {
const term = typeof id === 'string' ? this.environment.namedNode(id) : id;
const resource = await this.loadResource(term, headers, requestInit, false);
if (resource && resource.representation) {
this.__apiDocumentations.set(term, resource.representation);
const [apiDocs] = resource.representation.ofType(hydra.ApiDocumentation);
if (apiDocs) {
return apiDocs;
}
}
return null;
}
async invokeOperation(operation, headers = {}, body, requestInit = {}) {
const uri = getAbsoluteUri(operation.target.id.value, this.baseUri);
const mergedHeaders = await this.__mergeHeaders(new this._headers(headers), { uri });
if (!operation.method) {
throw new Error('Cannot invoke operation without a hydra:method');
}
const defaultRequestInit = typeof this.defaultRequestInit === 'function'
? await this.defaultRequestInit({ uri })
: this.defaultRequestInit;
const response = await this._fetch.operation(operation.method, uri, {
parsers: this.environment.formats.parsers,
headers: mergedHeaders,
body,
...{ ...defaultRequestInit, ...requestInit },
});
await this.__getApiDocumentation(response, headers);
const responseStream = await response.quadStream();
if (responseStream) {
const dataset = await fromStream(this.environment.dataset(), responseStream);
const rootResource = this.__findRootResource(dataset, response);
let resources = this.resources;
if (operation.method?.toUpperCase() !== 'GET') {
resources = this.resources.clone();
}
await resources.set(this.environment.namedNode(response.resourceUri), {
dataset,
rootResource,
response,
});
return resources.get(this.environment.namedNode(response.resourceUri));
}
return {
response,
};
}
async __getApiDocumentation(response, headers) {
if (!response.apiDocumentationLink) {
this.log(`Resource ${response.requestedUri} does not expose API Documentation link`);
return;
}
await this.loadDocumentation(response.apiDocumentationLink, headers);
}
async __mergeHeaders(headers, { uri }) {
const defaultHeaders = typeof this.defaultHeaders === 'function' ? await this.defaultHeaders({ uri }) : this.defaultHeaders;
return merge(new this._headers(defaultHeaders), headers, this._headers);
}
__findRootResource(dataset, response) {
const candidateNodes = this.rootSelectors.reduce((candidates, [, getCandidate]) => {
const candidate = getCandidate(this.environment, response, dataset);
if (candidate && dataset.match(candidate).size) {
candidates.add(candidate);
}
return candidates;
}, this.environment.termSet());
if (!candidateNodes.size) {
return undefined;
}
const candidates = [...candidateNodes].map(term => this.environment.clownface({ dataset, term }));
return candidates.sort(byInProperties)[0].term;
}
}