ketting
Version:
Opiniated HATEAOS / Rest client.
318 lines • 10.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Resource = void 0;
const state_1 = require("./state");
const uri_1 = require("./util/uri");
const follow_promise_1 = require("./follow-promise");
const link_1 = require("./link");
const events_1 = require("events");
const fetch_body_helper_1 = require("./util/fetch-body-helper");
/**
* A 'resource' represents an endpoint on a server.
*
* A resource has a uri, methods that correspond to HTTP methods,
* and events to subscribe to state changes.
*/
class Resource extends events_1.EventEmitter {
/**
* Create the resource.
*
* This is usually done by the Client.
*/
constructor(client, uri) {
super();
this.client = client;
this.uri = uri;
this.activeRefresh = null;
}
/**
* Gets the current state of the resource.
*
* This function will return a State object.
*/
get(getOptions) {
const state = this.getCache();
if (!state) {
return this.refresh(getOptions);
}
return Promise.resolve(state);
}
/**
* Does a HEAD request and returns a HeadState object.
*
* If there was a valid existing cache for a GET request, it will
* still return that.
*/
async head(headOptions) {
let state = this.client.cache.get(this.uri);
if (state) {
return state;
}
const response = await this.fetchOrThrow(optionsToRequestInit('HEAD', headOptions));
state = await state_1.headStateFactory(this.uri, response);
return state;
}
/**
* Gets the current state of the resource, skipping
* the cache.
*
* This function will return a State object.
*/
refresh(getOptions) {
const params = {
cache: 'reload',
};
if ((getOptions === null || getOptions === void 0 ? void 0 : getOptions.getContentHeaders) && !(getOptions === null || getOptions === void 0 ? void 0 : getOptions.headers)) {
params.headers = getOptions.getContentHeaders();
}
else if (!(getOptions === null || getOptions === void 0 ? void 0 : getOptions.getContentHeaders) && (getOptions === null || getOptions === void 0 ? void 0 : getOptions.headers)) {
params.headers = getOptions.headers;
}
else if ((getOptions === null || getOptions === void 0 ? void 0 : getOptions.getContentHeaders) && (getOptions === null || getOptions === void 0 ? void 0 : getOptions.headers)) {
params.headers = getOptions.getContentHeaders();
params.headers = Object.assign(Object.assign({}, getOptions.headers), params.headers);
}
if (!this.activeRefresh) {
this.activeRefresh = (async () => {
try {
const response = await this.fetchOrThrow(params);
const state = await this.client.getStateForResponse(this.uri, response);
this.updateCache(state);
return state;
}
finally {
this.activeRefresh = null;
}
})();
}
return this.activeRefresh;
}
/**
* Updates the server state with a PUT request
*/
async put(options) {
const requestInit = optionsToRequestInit('PUT', options);
/**
* If we got a 'State' object passed, it means we don't need to emit a
* stale event, as the passed object is the new
* state.
*
* We're gonna track that with a custom header that will be removed
* later in the fetch pipeline.
*/
if (state_1.isState(options)) {
requestInit.headers.set('X-KETTING-NO-STALE', '1');
}
await this.fetchOrThrow(requestInit);
if (state_1.isState(options)) {
this.updateCache(options);
}
}
/**
* Deletes the resource
*/
async delete() {
await this.fetchOrThrow({ method: 'DELETE' });
}
/**
* Sends a POST request to the resource.
*
* See the documentation for PostRequestOptions for more details.
* This function is used for RPC-like endpoints and form submissions.
*
* This function will return the response as a State object.
*/
async post(options) {
const response = await this.fetchOrThrow(optionsToRequestInit('POST', options));
return this.client.getStateForResponse(this.uri, response);
}
/**
* Sends a POST request, and follows to the next resource.
*
* If a server responds with a 201 Status code and a Location header,
* it will automatically return the newly created resource.
*
* If the server responded with a 204 or 205, this function will return
* `this`.
*/
async postFollow(options) {
const response = await this.fetchOrThrow(optionsToRequestInit('POST', options));
switch (response.status) {
case 201:
if (response.headers.has('location')) {
return this.go(response.headers.get('location'));
}
throw new Error('Could not follow after a 201 request, because the server did not reply with a Location header. If you sent a Location header, check if your service is returning "Access-Control-Expose-Headers: Location".');
case 204:
case 205:
return this;
default:
throw new Error('Did not receive a 201, 204 or 205 status code so we could not follow to the next resource');
}
}
/**
* Sends a PATCH request to the resource.
*
* This function defaults to a application/json content-type header.
*
* If the server responds with 200 Status code this will return a State object
*/
async patch(options) {
const response = await this.fetchOrThrow(optionsToRequestInit('PATCH', options));
if (response.status === 200) {
return await this.client.getStateForResponse(this.uri, response);
}
}
/**
* 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) {
return new follow_promise_1.FollowPromiseOne(this, rel, variables);
}
/**
* 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 new follow_promise_1.FollowPromiseMany(this, rel);
}
/**
* Resolves a new resource based on a relative uri.
*
* Use this function to manually get a Resource object via a uri. The uri
* will be resolved based on the uri of the current resource.
*
* This function doesn't do any HTTP requests.
*/
go(uri) {
uri = uri_1.resolve(this.uri, uri);
return this.client.go(uri);
}
/**
* Does a HTTP request on the current resource URI
*/
fetch(init) {
return this.client.fetcher.fetch(this.uri, init);
}
/**
* Does a HTTP request on the current resource URI.
*
* If the response was a 4XX or 5XX, this function will throw
* an exception.
*/
fetchOrThrow(init) {
return this.client.fetcher.fetchOrThrow(this.uri, init);
}
/**
* Updates the state cache, and emits events.
*
* This will update the local state but *not* update the server
*/
updateCache(state) {
if (state.uri !== this.uri) {
throw new Error('When calling updateCache on a resource, the uri of the State object must match the uri of the Resource');
}
this.client.cacheState(state);
}
/**
* Clears the state cache for this resource.
*/
clearCache() {
this.client.cache.delete(this.uri);
}
/**
* Retrieves the current cached resource state, and return `null` if it's
* not available.
*/
getCache() {
return this.client.cache.get(this.uri);
}
/**
* Returns a Link object, by its REL.
*
* If the link does not exist, a LinkNotFound error will be thrown.
*
* @deprecated
*/
async link(rel) {
const state = await this.get();
const link = state.links.get(rel);
if (!link) {
throw new link_1.LinkNotFound(`Link with rel: ${rel} not found on ${this.uri}`);
}
return link;
}
/**
* Returns all links defined on this object.
*
* @deprecated
*/
async links(rel) {
const state = await this.get();
if (!rel) {
return state.links.getAll();
}
else {
return state.links.getMany(rel);
}
}
/**
*
* Returns true or false depending on if a link with the specified relation
* type exists.
*
* @deprecated
*/
async hasLink(rel) {
const state = await this.get();
return state.links.has(rel);
}
}
exports.Resource = Resource;
exports.default = Resource;
function optionsToRequestInit(method, options) {
if (!options) {
return {
method,
headers: new Headers(),
};
}
let headers;
if (options.getContentHeaders) {
headers = new Headers(options.getContentHeaders());
}
else if (options.headers) {
headers = new Headers(options.headers);
}
else {
headers = new Headers();
}
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
let body;
if (options.serializeBody !== undefined) {
body = options.serializeBody();
}
else if (options.data) {
body = options.data;
if (fetch_body_helper_1.needsJsonStringify(body)) {
body = JSON.stringify(body);
}
}
else {
body = null;
}
return {
method,
body,
headers,
};
}
//# sourceMappingURL=resource.js.map