rb-data-provider-json-server
Version:
A Restboard data provider for JSON REST server
270 lines (253 loc) • 6.62 kB
JavaScript
import { RbDataProvider } from "rb-core-module";
import { defaultClient, renderQuerystring, retryCodes } from "./http.js";
class RbDataProviderJsonServer extends RbDataProvider {
constructor(
apiURL,
{
timeout = 5000,
retries = 3,
backoff = 500,
client = defaultClient,
tokenGetter = null,
contentTypeParser = (data) => "application/json; charset=UTF-8",
responseDataParser = (payload) => payload?.data || payload,
responseMetaParser = (payload) => payload?.meta || {},
responseErrorParser = (res) => res?.statusText || res?.status,
querystringRenderer = renderQuerystring,
idempotentUpdate = false,
cache = null,
} = {}
) {
super();
this.apiURL = apiURL;
this.timeout = timeout;
this.retries = retries;
this.backoff = backoff;
this.getToken = tokenGetter;
this.parseContentType = contentTypeParser;
this.parseData = responseDataParser;
this.parseMeta = responseMetaParser;
this.parseError = responseErrorParser;
this.renderQuerystring = querystringRenderer;
this.client = client;
this.runningReqs = new Map();
this.idempotentUpdate = idempotentUpdate;
this.cache = cache;
}
async getMany(
resource,
{
filters = {},
sort = "",
order = "",
offset = 0,
limit = null,
abort = null,
} = {}
) {
const base = `${this.apiURL}/${resource}`;
const qs = this.renderQuerystring(filters, sort, order, offset, limit);
const url = [base, qs].filter((v) => v).join("?");
const res = await this._performRequest(
url,
{
method: "GET",
abort,
},
this.retries
);
return {
data: this.parseData(res),
meta: this.parseMeta(res),
};
}
async getOne(resource, key, { filters = {}, abort = null } = {}) {
let url = `${this.apiURL}/${resource}/${key}`;
const qs = this.renderQuerystring(filters);
if (qs) {
url += `?${qs}`;
}
const res = await this._performRequest(
url,
{
method: "GET",
abort,
},
this.retries
);
return {
data: this.parseData(res),
meta: this.parseMeta(res),
};
}
async createOne(resource, data, { filters = {}, abort = null } = {}) {
let url = `${this.apiURL}/${resource}`;
const ct = this.parseContentType(data);
const qs = this.renderQuerystring(filters);
if (qs) {
url += `?${qs}`;
}
const res = await this._performRequest(
url,
{
method: "POST",
body: data,
headers: {
"Content-Type": ct,
},
abort,
},
this.retries
);
return {
data: this.parseData(res),
meta: this.parseMeta(res),
};
}
async updateOne(resource, key, data, { filters = {}, abort = null } = {}) {
let url = `${this.apiURL}/${resource}/${key}`;
const ct = this.parseContentType(data);
const qs = this.renderQuerystring(filters);
if (qs) {
url += `?${qs}`;
}
const res = await this._performRequest(
url,
{
method: this.idempotentUpdate ? "PUT" : "PATCH",
body: data,
headers: {
"Content-Type": ct,
},
abort,
},
this.retries
);
return {
data: this.parseData(res),
meta: this.parseMeta(res),
};
}
async updateMany(resource, data, { filters = {}, abort = null } = {}) {
let url = `${this.apiURL}/${resource}`;
const ct = this.parseContentType(data);
const qs = this.renderQuerystring(filters);
if (qs) {
url += `?${qs}`;
}
const res = await this._performRequest(
url,
{
method: this.idempotentUpdate ? "PUT" : "PATCH",
body: data,
headers: {
"Content-Type": ct,
},
abort,
},
this.retries
);
return {
data: this.parseData(res),
meta: this.parseMeta(res),
};
}
async deleteOne(resource, key, { filters = {}, abort = null } = {}) {
let url = `${this.apiURL}/${resource}/${key}`;
const qs = this.renderQuerystring(filters);
if (qs) {
url += `?${qs}`;
}
await this._performRequest(
url,
{
method: "DELETE",
abort,
},
this.retries
);
return {
data: key,
};
}
async deleteMany(resource, keys, { filters = {}, abort = null } = {}) {
let url = `${this.apiURL}/${resource}`;
const qs = this.renderQuerystring(filters);
if (qs) {
url += `?${qs}`;
}
await this._performRequest(
url,
{
method: "DELETE",
body: keys,
abort,
},
this.retries
);
return {
data: keys,
};
}
async _performRequest(url, options, retries, backoff) {
const { abort, headers, ...opts } = options;
const _backoff = backoff || this.backoff;
const _token = this.getToken ? await this.getToken() : undefined;
const _headers = {
Authorization: _token && `Bearer ${_token}`,
...headers,
};
const _reqOpts = {
timeout: this.timeout,
...opts,
headers: _headers,
};
const _reqId = JSON.stringify({
..._reqOpts,
url,
});
_reqOpts.abort = abort;
// Caching
if (this.cache && (await this.cache.has(_reqId))) {
return this.cache.get(_reqId);
}
// Batching
if (this.runningReqs.has(_reqId)) {
return this.runningReqs.get(_reqId);
}
const req = this.client(url, _reqOpts)
.finally(() => this.runningReqs.delete(_reqId))
.then((res) => {
if (res.ok) {
return res.json();
}
return new Promise((resolve, reject) => {
if (retries > 1 && retryCodes.includes(res.status)) {
// Retry on failure
const retryRequest = () => {
this._performRequest(url, options, retries - 1, _backoff * 2)
.then(resolve)
.catch(reject);
};
setTimeout(retryRequest, _backoff);
} else {
// Handle final error
const err = this.parseError(res);
return !err?.then ? reject(err) : err.then(reject).catch(reject);
}
});
})
.then((res) => {
if (this.cache) {
this.cache.set(_reqId, res);
}
return res;
});
this.runningReqs.set(_reqId, req);
return req;
}
}
function createProvider(apiURL, opts) {
return new RbDataProviderJsonServer(apiURL, opts);
}
export default createProvider;