@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,192 lines (1,043 loc) • 28.5 kB
JavaScript
/**
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact Volker Schukai.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { addAttributeToken } from "../../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
import { Datasource, dataSourceSymbol } from "../datasource.mjs";
import { DatasourceStyleSheet } from "../stylesheet/datasource.mjs";
import { instanceSymbol } from "../../../constants.mjs";
import {
assembleMethodSymbol,
registerCustomElement,
} from "../../../dom/customelement.mjs";
import { RestAPI } from "../../../data/datasource/server/restapi.mjs";
import { DataFetchError } from "../../../data/datasource/server/restapi/data-fetch-error.mjs";
import { Formatter } from "../../../text/formatter.mjs";
import { clone } from "../../../util/clone.mjs";
import { validateBoolean } from "../../../types/validate.mjs";
import { findElementWithIdUpwards } from "../../../dom/util.mjs";
import { Observer } from "../../../types/observer.mjs";
import { Pathfinder } from "../../../data/pathfinder.mjs";
import { fireCustomEvent } from "../../../dom/events.mjs";
import { isArray, isFunction, isObject, isString } from "../../../types/is.mjs";
export { Rest };
/**
* @private
* @type {symbol}
*/
const intersectionObserverHandlerSymbol = Symbol("intersectionObserverHandler");
/**
* @private
* Original at source/components/datatable/datasource/rest.mjs
* @type {symbol}
*/
const rawDataSymbol = Symbol.for(
"@schukai/monster/data/datasource/server/restapi/rawdata",
);
/**
* @private
* @type {symbol}
*/
const intersectionObserverObserverSymbol = Symbol(
"intersectionObserverObserver",
);
/**
* @private
* @type {symbol}
*/
const filterObserverSymbol = Symbol("filterObserver");
const readRequestIdSymbol = Symbol("readRequestId");
const writeRequestIdSymbol = Symbol("writeRequestId");
const lookupCacheSymbol = Symbol("lookupCache");
const lookupPendingSymbol = Symbol("lookupPending");
const lookupPendingFetchSymbol = Symbol("lookupPendingFetch");
/**
* A rest api datasource
*
* @fragments /fragments/components/datatable/datasource/rest
*
* @example /examples/components/datatable/datasource-rest-simple Simple Rest datasource
* @example /examples/components/datatable/datasource-rest-auto-init Auto init
* @example /examples/components/datatable/datasource-rest-do-fetch Rest datasource with fetch
*
* @issue https://localhost.alvine.dev:8440/development/issues/closed/272.html
*
* @copyright Volker Schukai
* @summary A rest api datasource for the datatable or other components
*/
class Rest extends Datasource {
/**
* the constructor of the class
*/
constructor() {
super();
this[dataSourceSymbol] = new RestAPI();
}
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/datasource/rest@@instance");
}
/**
* To set the options via the HTML tag, the attribute `data-monster-options` must be used.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* The individual configuration values can be found in the table.
*
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {Object} features Feature definitions
* @property {boolean} features.autoInit If true, the component is initialized automatically
* @property {boolean} features.filter If true, the filter.id is used to attach a filter control
* @property {Object} autoInit Auto init definitions
* @property {boolean} autoInit.intersectionObserver If true, the intersection observer is initialized automatically
* @property {boolean} autoInit.oneTime If true, the intersection observer is initialized only once
* @property {Object} filter Filter definitions
* @property {string} filter.id The id of the filter control
* @property {Object} response Response definitions
* @property {Object} response.path Path definitions (changed in 3.56.0)
* @property {string} response.path.message Path to the message (changed in 3.56.0)
* @property {Object} read Read configuration
* @property {string} read.url The url of the rest api
* @property {string} read.method The method of the rest api
* @property {Object} read.parameters The parameters of the rest api
* @property {Object} read.parameters.filter The filter of the rest api
* @property {Object} read.parameters.order The order by of the rest api
* @property {Object} read.parameters.page The page of the rest api
* @property {string} read.mapping.currentPage The current page
* @property {Object} write Write configuration
* @property {string} write.url The url of the rest api
* @property {string} write.method The method of the rest api
* @property {Object} write Write configuration
*/
get defaults() {
const restOptions = new RestAPI().defaults;
restOptions.read.parameters = {
filter: null,
order: null,
page: "1",
};
restOptions.read.mapping.currentPage = "sys.pagination.currentPage";
return Object.assign({}, super.defaults, restOptions, {
templates: {
main: getTemplate(),
},
features: {
autoInit: false,
filter: false,
},
autoInit: {
intersectionObserver: false,
oneTime: true,
},
filter: {
id: null,
},
/*datatable: {
id: undefined, // not used?
}, */
response: {
path: {
message: "sys.message",
code: "sys.code",
validationErrors: null,
validationMessage: null,
},
},
validation: {
map: {},
},
lookups: {
enabled: false,
debug: false,
sourcePath: "dataset",
request: {
idsParam: "ids",
idsSeparator: ",",
},
response: {
path: "dataset",
id: "id",
},
format: {
template: "${name}",
marker: {
open: ["${"],
close: ["}"],
},
},
columns: {},
},
});
}
/**
* With this method, you can set the parameters for the rest api. The parameters are
* used for building the url.
*
* @param {string} page
* @param {string} query
* @param {string} order
* @return {Rest}
*/
setParameters({ page, query, order }) {
const parameters = this.getOption("read.parameters");
if (query !== undefined) {
parameters.query = `${query}`;
parameters.page = "1";
}
// after a query the page is set to 1, so if the page is not set, it is set to 1
if (page !== undefined) parameters.page = `${page}`;
if (order !== undefined) parameters.order = `${order}`;
this.setOption("read.parameters", parameters);
return this;
}
/**
* @private
* @return {void}
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initEventHandler.call(this);
}
/**
* This method reloads the data from the rest api, this method is deprecated.
* You should use the method `read` instead.
*
* @deprecated 2023-06-25
* @return {Promise<never>|*}
*/
reload() {
return this.read();
}
/**
* Fetches the data from the rest api, this method is deprecated.
* You should use the method `read` instead.
*
* @deprecated 2024-12-24
* @return {Promise<never>|*}
*/
fetch() {
return this.read();
}
/**
*
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [DatasourceStyleSheet];
}
/**
* @private
* @return {string}
*/
static getTag() {
return "monster-datasource-rest";
}
/**
* This method activates the intersection observer manually.
* For this purpose, the option `autoInit.intersectionObserver` must be set to `false`.
*
* @return {Rest}
*/
initIntersectionObserver() {
initIntersectionObserver.call(this);
return this;
}
/**
* @private
*/
connectedCallback() {
super.connectedCallback();
queueMicrotask(() => {
if (this.getOption("features.filter", false) === true) {
initFilter.call(this);
if (!this[filterObserverSymbol]) {
initAutoInit.call(this);
}
} else {
initAutoInit.call(this);
}
});
}
/**
* @private
*/
disconnectedCallback() {
super.disconnectedCallback();
removeFilter.call(this);
}
/**
* This method reads the data from the rest api.
* The data is stored in the internal dataset object.
*
* @return {Promise}
* @fires monster-datasource-fetch
* @fires monster-datasource-fetched
* @fires monster-datasource-error
*/
read() {
const opt = clone(this.getOption("read"));
const requestId = (this[readRequestIdSymbol] || 0) + 1;
this[readRequestIdSymbol] = requestId;
const responseCallback = opt?.responseCallback;
opt.responseCallback = (obj) => {
if (this[readRequestIdSymbol] !== requestId) {
return;
}
if (typeof responseCallback === "function") {
responseCallback(obj);
return;
}
const transformedPayload = this[
dataSourceSymbol
].transformServerPayload.call(this[dataSourceSymbol], obj);
if (typeof transformedPayload === "undefined") {
return;
}
this[dataSourceSymbol].set(transformedPayload);
applyLookups.call(this, requestId).catch(() => {});
};
this[dataSourceSymbol].setOption("read", opt);
let url = this.getOption("read.url");
if (!url) {
return Promise.reject(new Error("No url defined"));
}
const param = this.getOption("read.parameters", {});
if (param.query === null || param.query === undefined) {
param.query = "";
}
if (param.page === null || param.page === undefined) {
param.page = "1";
}
if (param.order === null || param.order === undefined) {
param.order = "";
}
const formatter = new Formatter(param);
url = formatter.format(url);
this[dataSourceSymbol].setOption("read.url", url);
return new Promise((resolve, reject) => {
fireCustomEvent(this, "monster-datasource-fetch", {
datasource: this,
});
queueMicrotask(() => {
this[dataSourceSymbol]
.read()
.then((response) => {
if (this[readRequestIdSymbol] === requestId) {
fireCustomEvent(this, "monster-datasource-fetched", {
datasource: this,
});
}
resolve(response);
})
.catch((error) => {
handleValidationError.call(this, error);
if (this[readRequestIdSymbol] === requestId) {
fireCustomEvent(this, "monster-datasource-error", {
error: error,
});
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
}
reject(error);
});
});
});
}
/**
* Writes the data to the rest api.
* @return {Promise}
*/
write() {
const opt = clone(this.getOption("write"));
const requestId = (this[writeRequestIdSymbol] || 0) + 1;
this[writeRequestIdSymbol] = requestId;
const responseCallback = opt?.responseCallback;
opt.responseCallback = (obj) => {
if (this[writeRequestIdSymbol] !== requestId) {
return;
}
if (typeof responseCallback === "function") {
responseCallback(obj);
}
};
this[dataSourceSymbol].setOption("write", opt);
let url = this.getOption("write.url");
const formatter = new Formatter(this.getOption("write.parameters"));
if (!url) {
return Promise.reject(new Error("No url defined"));
}
url = formatter.format(url);
this[dataSourceSymbol].setOption("write.url", url);
return new Promise((resolve, reject) => {
fireCustomEvent(this, "monster-datasource-fetch", {
datasource: this,
});
queueMicrotask(() => {
this[dataSourceSymbol]
.write()
.then((response) => {
if (this[writeRequestIdSymbol] === requestId) {
fireCustomEvent(this, "monster-datasource-fetched", {
datasource: this,
});
}
resolve(response);
})
.catch((error) => {
handleValidationError.call(this, error);
if (this[writeRequestIdSymbol] === requestId) {
fireCustomEvent(this, "monster-datasource-error", {
error: error,
});
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
}
reject(error);
});
});
});
}
}
/**
* @private
*/
function removeFilter() {
const filterID = this.getOption("filter.id", undefined);
if (!filterID) return;
const filterControl = findElementWithIdUpwards(this, filterID);
if (filterControl && this[filterObserverSymbol]) {
filterControl?.detachObserver(this[filterObserverSymbol]);
}
}
/**
* @private
*/
function initFilter() {
const filterID = this.getOption("filter.id", undefined);
if (!filterID)
throw new Error("filter feature is enabled but no filter id is defined");
const filterControl = findElementWithIdUpwards(this, filterID);
if (!filterControl) {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
"filter feature is enabled but no filter control with id " +
filterID +
" is found",
);
return;
}
this[filterObserverSymbol] = new Observer(() => {
const query = filterControl.getOption("query");
if (query === undefined || query === null) {
return;
}
this.setParameters({ query: query });
this.read()
.then((response) => {
if (!(response instanceof Response)) {
return Promise.reject(new Error("response is not a Response object"));
}
if (response?.ok === true) {
this.dispatchEvent(new CustomEvent("reload", { bubbles: true }));
filterControl?.showSuccess();
}
if (response.bodyUsed === true) {
return handleIntersectionObserver.call(
this,
response[rawDataSymbol],
response,
filterControl,
);
}
response
.text()
.then((jsonAsText) => {
let json;
try {
json = JSON.parse(jsonAsText);
} catch (e) {
const message = e instanceof Error ? e.message : `${e}`;
filterControl?.showFailureMessage(message);
return Promise.reject(e);
}
return handleIntersectionObserver.call(
this,
json,
response,
filterControl,
);
})
.catch((e) => {
filterControl?.showFailureMessage(e.message);
});
})
.catch((e) => {
this.dispatchEvent(
new CustomEvent("error", { bubbles: true, detail: e }),
);
if (!(e instanceof Error)) {
e = new Error(e);
}
filterControl?.showFailureMessage(e.message);
return Promise.reject(e);
});
});
filterControl.attachObserver(this[filterObserverSymbol]);
}
/**
* @private
* @param json
* @param response
* @param filterControl
* @returns {Promise<never>|Promise<Awaited<unknown>>}
*/
function handleIntersectionObserver(json, response, filterControl) {
const path = new Pathfinder(json);
const codePath = this.getOption("response.path.code");
if (path.exists(codePath)) {
const code = `${path.getVia(codePath)}`;
if (code && code === "200") {
filterControl?.showSuccess();
return Promise.resolve(response);
}
const messagePath = this.getOption("response.path.message");
if (path.exists(messagePath)) {
const message = path.getVia(messagePath);
filterControl?.showFailureMessage(message);
return Promise.reject(new Error(message));
}
return Promise.reject(new Error("Response code is not 200"));
}
}
/**
* @private
*/
function initAutoInit() {
const autoInit = this.getOption("features.autoInit");
validateBoolean(autoInit);
if (autoInit !== true) return;
if (this.getOption("autoInit.intersectionObserver") === true) {
initIntersectionObserver.call(this);
return;
}
queueMicrotask(() => {
this.read().catch(() => {});
});
}
/**
* @private
*/
function initEventHandler() {
this[intersectionObserverHandlerSymbol] = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (entry.intersectionRatio > 0) {
this.read();
}
// only load once
if (
this.getOption("autoInit.oneTime") === true &&
this[intersectionObserverObserverSymbol] !== undefined
) {
this[intersectionObserverObserverSymbol].unobserve(this);
}
}
});
};
}
/**
* @private
*/
function initIntersectionObserver() {
this.classList.add("intersection-observer");
const options = {
root: null,
rootMargin: "0px",
threshold: 0.1,
};
this[intersectionObserverObserverSymbol] = new IntersectionObserver(
this[intersectionObserverHandlerSymbol],
options,
);
this[intersectionObserverObserverSymbol].observe(this);
}
/**
* @private
* @param {Error} error
* @return {Promise<void>}
*/
function handleValidationError(error) {
const path = this.getOption("response.path.validationErrors");
if (!path) {
return Promise.resolve();
}
if (!(error instanceof DataFetchError)) {
return Promise.resolve();
}
const messagePath = this.getOption("response.path.validationMessage");
return error
.getBody()
.then((body) => {
let json = body;
if (typeof body === "string") {
try {
json = JSON.parse(body);
} catch {
return;
}
}
if (!isObject(json)) {
return;
}
const pf = new Pathfinder(json);
if (!pf.exists(path)) {
return;
}
const rawErrors = pf.getVia(path);
if (!isObject(rawErrors)) {
return;
}
const normalizedErrors = {};
for (const [key, value] of Object.entries(rawErrors)) {
if (isObject(value)) {
normalizedErrors[key] = {
code: value.code || null,
message: value.message || `${value}`,
};
} else {
normalizedErrors[key] = { code: null, message: `${value}` };
}
}
let message = null;
if (messagePath && pf.exists(messagePath)) {
message = pf.getVia(messagePath);
}
this.dispatchEvent(
new CustomEvent("monster-datasource-validation", {
bubbles: true,
detail: {
errors: normalizedErrors,
raw: rawErrors,
message,
payload: json,
},
}),
);
})
.catch(() => {});
}
/**
* @private
* @return {boolean}
*/
function lookupDebugEnabled() {
return this.getOption("lookups.debug", false) === true;
}
/**
* @private
* @param {number} requestId
* @return {Promise<void>}
*/
async function applyLookups(requestId) {
if (!this.getOption("lookups.enabled", false)) {
return;
}
const columns = this.getOption("lookups.columns", {});
if (!isObject(columns)) {
return;
}
const columnEntries = Object.entries(columns).filter(([, cfg]) =>
isObject(cfg),
);
if (columnEntries.length === 0) {
return;
}
const resolverColumns = [];
const remoteColumns = [];
for (const [name, cfg] of columnEntries) {
if (isFunction(cfg.resolve)) {
resolverColumns.push([name, cfg]);
} else if (isString(cfg.url) && cfg.url !== "") {
remoteColumns.push([name, cfg]);
}
}
if (resolverColumns.length > 0) {
updateLookupRows.call(this, requestId, (rows) => {
for (const [, cfg] of resolverColumns) {
const key = cfg.key;
const target = cfg.target;
if (!isString(key) || !isString(target)) {
continue;
}
for (const row of rows) {
const entry = cfg.resolve(row[key], row);
if (!entry) {
continue;
}
row[target] = formatLookupValue.call(this, cfg, entry, row);
if (isString(cfg.loadingKey)) {
row[cfg.loadingKey] = false;
}
}
}
});
}
if (remoteColumns.length === 0) {
return;
}
await Promise.all(
remoteColumns.map(([name, cfg]) =>
resolveRemoteLookup.call(this, requestId, name, cfg),
),
);
}
/**
* @private
* @param {number} requestId
* @param {string} name
* @param {object} cfg
* @return {Promise<void>}
*/
async function resolveRemoteLookup(requestId, name, cfg) {
const key = cfg.key;
const target = cfg.target;
if (!isString(key) || !isString(target)) {
return;
}
const cache = getLookupCache.call(this, name);
const pending = getLookupPending.call(this, name);
const pendingFetches = getLookupPendingFetches.call(this, name);
const ids = collectLookupIds.call(
this,
key,
this.getOption("lookups.sourcePath", "dataset"),
);
const missingIds = ids.filter((id) => !cache.has(id) && !pending.has(id));
if (missingIds.length === 0) {
if (pending.size > 0) {
updateLookupRows.call(this, requestId, (rows) => {
if (!isString(cfg.loadingKey)) return;
for (const row of rows) {
if (pending.has(String(row[key]))) {
row[cfg.loadingKey] = true;
}
}
});
if (pendingFetches.size > 0) {
const fetches = Array.from(pendingFetches);
await Promise.all(fetches.map((promise) => promise.catch(() => null)));
}
}
applyLookupCache.call(this, requestId, cfg, cache);
return;
}
updateLookupRows.call(this, requestId, (rows) => {
if (!isString(cfg.loadingKey)) return;
for (const row of rows) {
if (
missingIds.includes(String(row[key])) ||
pending.has(String(row[key]))
) {
row[cfg.loadingKey] = true;
}
}
});
missingIds.forEach((id) => pending.add(id));
const fetchPromise = fetchLookupEntries.call(this, cfg, missingIds);
pendingFetches.add(fetchPromise);
let response;
try {
response = await fetchPromise;
} finally {
pendingFetches.delete(fetchPromise);
}
response.forEach((entry, id) => cache.set(id, entry));
missingIds.forEach((id) => pending.delete(id));
applyLookupCache.call(this, requestId, cfg, cache);
}
/**
* @private
* @param {number} requestId
* @param {object} cfg
* @param {Map} cache
*/
function applyLookupCache(requestId, cfg, cache) {
const key = cfg.key;
const target = cfg.target;
updateLookupRows.call(this, requestId, (rows) => {
for (const row of rows) {
const entry = cache.get(String(row[key]));
if (entry) {
row[target] = formatLookupValue.call(this, cfg, entry, row);
}
if (isString(cfg.loadingKey)) {
row[cfg.loadingKey] = false;
}
}
});
}
/**
* @private
* @param {string} key
* @return {string[]}
*/
function collectLookupIds(key, sourcePath) {
const rows = resolveLookupRows.call(this, undefined, sourcePath);
if (!isArray(rows)) {
return [];
}
const ids = new Set();
for (const row of rows) {
const value = row?.[key];
if (value === undefined || value === null || value === "") {
continue;
}
if (isArray(value)) {
value.forEach((entry) => ids.add(String(entry)));
} else {
ids.add(String(value));
}
}
return Array.from(ids);
}
/**
* @private
* @param {object} cfg
* @param {string[]} ids
* @return {Promise<Map<string, object>>}
*/
async function fetchLookupEntries(cfg, ids) {
const requestDefaults = this.getOption("lookups.request", {});
const request = {
...requestDefaults,
...(cfg.request || {}),
};
const init = isObject(request.init) ? request.init : {};
const url = buildLookupUrl(cfg.url, ids, request);
const debug = lookupDebugEnabled.call(this);
if (debug) {
console.debug("[monster-datasource-rest] lookup fetch", {
url,
ids,
});
}
const response = await fetch(url, init);
if (!response.ok) {
if (debug) {
console.debug("[monster-datasource-rest] lookup failed", {
url,
status: response.status,
});
}
return new Map();
}
let payload;
try {
payload = await response.json();
} catch (_error) {
if (debug) {
console.debug("[monster-datasource-rest] lookup invalid json", { url });
}
return new Map();
}
const responseDefaults = this.getOption("lookups.response", {});
const responseConfig = {
...responseDefaults,
...(cfg.response || {}),
};
let entries = payload;
if (isString(responseConfig.path) && responseConfig.path !== "") {
entries = new Pathfinder(payload).getVia(responseConfig.path);
}
if (!isArray(entries)) {
if (debug) {
console.debug("[monster-datasource-rest] lookup no entries", {
url,
path: responseConfig.path,
});
}
return new Map();
}
const idKey = responseConfig.id || "id";
const result = new Map();
for (const entry of entries) {
if (!entry || entry[idKey] === undefined || entry[idKey] === null) {
continue;
}
result.set(String(entry[idKey]), entry);
}
if (debug) {
console.debug("[monster-datasource-rest] lookup resolved", {
url,
entries: result.size,
});
}
return result;
}
/**
* @private
* @param {string} url
* @param {string[]} ids
* @param {object} request
* @return {string}
*/
function buildLookupUrl(url, ids, request) {
const idsParam = request.idsParam || "ids";
const idsValue = buildLookupIdsValue(ids, request);
if (url.includes("${")) {
const formatter = new Formatter({ ids: idsValue });
return formatter.format(url);
}
const separator = url.includes("?") ? "&" : "?";
return `${url}${separator}${encodeURIComponent(idsParam)}=${encodeURIComponent(
idsValue,
)}`;
}
/**
* @private
* @param {string[]} ids
* @param {object} request
* @return {string}
*/
function buildLookupIdsValue(ids, request) {
if (isFunction(request.queryBuilder)) {
return request.queryBuilder(ids);
}
const idsTemplate = request.idsTemplate;
const idsSeparator = request.idsSeparator || ",";
const wrapOpen = request.wrapOpen || "";
const wrapClose = request.wrapClose || "";
let values = ids;
if (isString(idsTemplate) && idsTemplate !== "") {
values = ids.map((id) => {
const formatter = new Formatter({ id });
return formatter.format(idsTemplate);
});
}
return `${wrapOpen}${values.join(idsSeparator)}${wrapClose}`;
}
/**
* @private
* @param {object} cfg
* @param {object} entry
* @param {object} row
* @return {string}
*/
function formatLookupValue(cfg, entry, row) {
if (isFunction(cfg.format)) {
return cfg.format(entry, row);
}
const formatDefaults = this.getOption("lookups.format", {});
const format = isObject(cfg.format) ? cfg.format : {};
const template = format.template || formatDefaults.template || "${name}";
if (!isString(template)) {
return "";
}
const formatter = new Formatter({ ...entry, row });
const marker = format.marker || formatDefaults.marker;
if (marker?.open) {
formatter.setMarker(marker.open, marker.close);
}
return formatter.format(template);
}
/**
* @private
* @param {number} requestId
* @param {Function} update
*/
function updateLookupRows(requestId, update) {
if (this[readRequestIdSymbol] !== requestId) {
return;
}
const data = this[dataSourceSymbol].get();
const sourcePath = this.getOption("lookups.sourcePath", "dataset");
const next = clone(data);
const rows = resolveLookupRows.call(this, next, sourcePath);
if (!isArray(rows)) {
return;
}
update(rows);
this[dataSourceSymbol].set(next);
}
/**
* @private
* @param {object} [data]
* @param {string} [sourcePath]
* @return {Array|undefined}
*/
function resolveLookupRows(data, sourcePath) {
const source = data || this[dataSourceSymbol].get();
if (isString(sourcePath) && sourcePath !== "") {
return new Pathfinder(source).getVia(sourcePath);
}
if (isArray(source)) {
return source;
}
if (isObject(source) && isArray(source.dataset)) {
return source.dataset;
}
return undefined;
}
/**
* @private
* @param {string} name
* @return {Map<string, object>}
*/
function getLookupCache(name) {
if (!this[lookupCacheSymbol]) {
this[lookupCacheSymbol] = new Map();
}
if (!this[lookupCacheSymbol].has(name)) {
this[lookupCacheSymbol].set(name, new Map());
}
return this[lookupCacheSymbol].get(name);
}
/**
* @private
* @param {string} name
* @return {Set<string>}
*/
function getLookupPending(name) {
if (!this[lookupPendingSymbol]) {
this[lookupPendingSymbol] = new Map();
}
if (!this[lookupPendingSymbol].has(name)) {
this[lookupPendingSymbol].set(name, new Set());
}
return this[lookupPendingSymbol].get(name);
}
/**
* @private
* @param {string} name
* @return {Set<Promise<Map<string, object>>>}
*/
function getLookupPendingFetches(name) {
if (!this[lookupPendingFetchSymbol]) {
this[lookupPendingFetchSymbol] = new Map();
}
if (!this[lookupPendingFetchSymbol].has(name)) {
this[lookupPendingFetchSymbol].set(name, new Set());
}
return this[lookupPendingFetchSymbol].get(name);
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `<slot></slot>`;
}
registerCustomElement(Rest);