UNPKG

alcaeus

Version:

Hydra Core hypermedia-aware client library

154 lines (153 loc) 6.95 kB
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; } }