@tmagic/data-source
Version:
812 lines (801 loc) • 24.9 kB
JavaScript
import EventEmitter$1, { EventEmitter } from 'events';
import { cloneDeep, union } from 'lodash-es';
import { setValueByKeyPath, getValueByKeyPath, getDefaultValueFromFields, traverseNode, Watcher, Target, DSL_NODE_KEY_COPY_PREFIX, isDataSourceTarget, isDataSourceCondTarget, compiledCond, NODE_CONDS_KEY, isPage, isPageFragment, replaceChildNode, DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, dataSourceTemplateRegExp, compiledNode, NODE_DISABLE_DATA_SOURCE_KEY, getNodes, getDepNodeIds } from '@tmagic/core';
import State from 'deep-state-observer';
class ObservedData {
}
class SimpleObservedData extends ObservedData {
data = {};
event = new EventEmitter();
constructor(initialData) {
super();
this.data = initialData;
}
update(data, path) {
if (path) {
setValueByKeyPath(path, data, this.data);
} else {
this.data = data;
}
const changeEvent = {
updateData: data,
path: path ?? ""
};
if (path) {
this.event.emit(path, changeEvent);
}
this.event.emit("", changeEvent);
}
on(path, callback) {
this.event.on(path, callback);
}
off(path, callback) {
this.event.off(path, callback);
}
getData(path) {
return path ? getValueByKeyPath(path, this.data) : this.data;
}
destroy() {
}
}
class DataSource extends EventEmitter$1 {
isInit = false;
/** @tmagic/core 实例 */
app;
mockData;
#type = "base";
#id;
#schema;
#observedData;
/** 数据源自定义字段配置 */
#fields = [];
/** 数据源自定义方法配置 */
#methods = [];
constructor(options) {
super();
this.#id = options.schema.id;
this.#schema = options.schema;
this.app = options.app;
this.setFields(options.schema.fields);
this.setMethods(options.schema.methods || []);
let data = options.initialData;
const ObservedDataClass = options.ObservedDataClass || SimpleObservedData;
if (this.app.platform === "editor") {
const mocks = cloneDeep(options.schema.mocks || []);
this.mockData = mocks.find((mock) => mock.useInEditor)?.data || this.getDefaultData();
data = cloneDeep(this.mockData);
} else if (typeof options.useMock === "boolean" && options.useMock) {
const mocks = cloneDeep(options.schema.mocks || []);
this.mockData = mocks.find((mock) => mock.enable)?.data;
data = cloneDeep(this.mockData) || this.getDefaultData();
} else if (!options.initialData) {
data = this.getDefaultData();
} else {
this.#observedData = new ObservedDataClass(options.initialData ?? {});
this.isInit = true;
return;
}
this.#observedData = new ObservedDataClass(data ?? {});
}
get id() {
return this.#id;
}
get type() {
return this.#type;
}
get schema() {
return this.#schema;
}
get fields() {
return this.#fields;
}
get methods() {
return this.#methods;
}
setFields(fields) {
this.#fields = fields;
}
setMethods(methods) {
this.#methods = methods;
}
get data() {
return this.#observedData.getData("");
}
setData(data, path) {
this.#observedData.update(data, path);
const changeEvent = {
updateData: data,
path
};
this.emit("change", changeEvent);
}
setValue(path, data) {
return this.setData(data, path);
}
onDataChange(path, callback) {
this.#observedData.on(path, callback);
}
offDataChange(path, callback) {
this.#observedData.off(path, callback);
}
getDefaultData() {
return getDefaultValueFromFields(this.#fields);
}
async init() {
this.isInit = true;
}
destroy() {
this.#fields = [];
this.removeAllListeners();
this.#observedData.destroy();
}
}
const urlencoded = (data) => Object.entries(data).reduce((prev, [key, value]) => {
let v = value;
if (typeof value === "object") {
v = JSON.stringify(value);
}
if (typeof value !== "undefined") {
return `${prev}${prev ? "&" : ""}${globalThis.encodeURIComponent(key)}=${globalThis.encodeURIComponent(`${v}`)}`;
}
return prev;
}, "");
const webRequest = async (options) => {
const { url, method = "GET", headers = {}, params = {}, data = {}, ...config } = options;
const query = urlencoded(params);
let body = JSON.stringify(data);
if (headers["Content-Type"]?.includes("application/x-www-form-urlencoded")) {
body = urlencoded(data);
}
const response = await globalThis.fetch(query ? `${url}?${query}` : url, {
method,
headers,
body: method === "GET" ? void 0 : body,
...config
});
return response.json();
};
class HttpDataSource extends DataSource {
/** 是否正在发起请求 */
isLoading = false;
error;
/** 请求配置 */
httpOptions;
/** 请求函数 */
#fetch;
/** 请求前需要执行的函数队列 */
#beforeRequest = [];
/** 请求后需要执行的函数队列 */
#afterRequest = [];
#type = "http";
constructor(options) {
const { options: httpOptions } = options.schema;
super(options);
this.httpOptions = httpOptions;
if (typeof options.request === "function") {
this.#fetch = options.request;
} else if (typeof globalThis.fetch === "function") {
this.#fetch = webRequest;
}
this.methods.forEach((method) => {
if (typeof method.content !== "function") return;
if (method.timing === "beforeRequest") {
this.#beforeRequest.push(method.content);
}
if (method.timing === "afterRequest") {
this.#afterRequest.push(method.content);
}
});
}
get type() {
return this.#type;
}
async init() {
if (this.schema.autoFetch) {
await this.request();
}
super.init();
}
async request(options = {}) {
this.isLoading = true;
const { url, params, data, headers, ...otherHttpOptions } = this.httpOptions;
let reqOptions = {
url: typeof url === "function" ? url({ app: this.app, dataSource: this }) : url,
params: typeof params === "function" ? params({ app: this.app, dataSource: this }) : params,
data: typeof data === "function" ? data({ app: this.app, dataSource: this }) : data,
headers: typeof headers === "function" ? headers({ app: this.app, dataSource: this }) : headers,
...otherHttpOptions,
...options
};
try {
for (const method of this.#beforeRequest) {
await method({ options: reqOptions, params: {}, dataSource: this, app: this.app });
}
if (typeof this.schema.beforeRequest === "function") {
reqOptions = await this.schema.beforeRequest(reqOptions, { app: this.app, dataSource: this });
}
if (this.mockData) {
this.setData(this.mockData);
} else {
let res = await this.#fetch?.(reqOptions);
for (const method of this.#afterRequest) {
await method({ res, options: reqOptions, params: {}, dataSource: this, app: this.app });
}
if (typeof this.schema.afterResponse === "function") {
res = await this.schema.afterResponse(res, { app: this.app, dataSource: this, options: reqOptions });
}
if (this.schema.responseOptions?.dataPath) {
const data2 = getValueByKeyPath(this.schema.responseOptions.dataPath, res);
this.setData(data2);
} else {
this.setData(res);
}
}
this.error = void 0;
} catch (error) {
this.error = {
msg: error.message
};
this.emit("error", error);
}
this.isLoading = false;
}
get(options) {
return this.request({
...options,
method: "GET"
});
}
post(options) {
return this.request({
...options,
method: "POST"
});
}
}
const cache = /* @__PURE__ */ new Map();
const getDeps = (ds, nodes, inEditor) => {
let cacheKey;
if (inEditor) {
const ids = [];
nodes.forEach((node) => {
traverseNode(node, (node2) => {
ids.push(node2.id);
});
});
cacheKey = `${ds.id}:${ids.join(":")}`;
} else {
cacheKey = `${ds.id}:${nodes.map((node) => node.id).join(":")}`;
}
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const watcher = new Watcher();
watcher.addTarget(
new Target({
id: ds.id,
type: "data-source",
isTarget: (key, value) => {
if (`${key}`.includes(DSL_NODE_KEY_COPY_PREFIX)) {
return false;
}
return isDataSourceTarget(ds, key, value, true);
}
})
);
watcher.addTarget(
new Target({
id: ds.id,
type: "cond",
isTarget: (key, value) => isDataSourceCondTarget(ds, key, value, true)
})
);
watcher.collect(nodes, {}, true);
const { deps } = watcher.getTarget(ds.id, "data-source");
const { deps: condDeps } = watcher.getTarget(ds.id, "cond");
const result = { deps, condDeps };
cache.set(cacheKey, result);
return result;
};
const compiledCondition = (cond, data) => {
let result = true;
for (const { op, value, range, field } of cond) {
const [sourceId, ...fields] = field;
const dsData = data[sourceId];
if (!dsData || !fields.length) {
break;
}
try {
const fieldValue = getValueByKeyPath(fields.join("."), dsData);
if (!compiledCond(op, fieldValue, value, range)) {
result = false;
break;
}
} catch (e) {
console.warn(e);
}
}
return result;
};
const compliedConditions = (node, data) => {
if (!node[NODE_CONDS_KEY] || !Array.isArray(node[NODE_CONDS_KEY]) || !node[NODE_CONDS_KEY].length) return true;
for (const { cond } of node[NODE_CONDS_KEY]) {
if (!cond) continue;
if (compiledCondition(cond, data)) {
return true;
}
}
return false;
};
const updateNode = (node, dsl) => {
if (isPage(node) || isPageFragment(node)) {
const index = dsl.items?.findIndex((child) => child.id === node.id);
dsl.items.splice(index, 1, node);
} else {
replaceChildNode(node, dsl.items);
}
};
const createIteratorContentData = (itemData, dsId, fields = [], dsData = {}) => {
const data = {
...dsData,
[dsId]: {}
};
let rawData = cloneDeep(dsData[dsId]);
let obj = data[dsId];
fields.forEach((key, index) => {
Object.assign(obj, rawData);
if (index === fields.length - 1) {
obj[key] = itemData;
return;
}
if (Array.isArray(rawData[key])) {
rawData[key] = {};
obj[key] = {};
}
rawData = rawData[key];
obj = obj[key];
});
return data;
};
const compliedDataSourceField = (value, data) => {
const [prefixId, ...fields] = value;
const prefixIndex = prefixId.indexOf(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
if (prefixIndex > -1) {
const dsId = prefixId.substring(prefixIndex + DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX.length);
const dsData = data[dsId];
if (!dsData) return value;
try {
return getValueByKeyPath(fields.join("."), dsData);
} catch (e) {
return value;
}
}
return value;
};
const template = (value, data) => value.replace(dataSourceTemplateRegExp, (match, $1) => {
try {
return getValueByKeyPath($1, data);
} catch (e) {
return match;
}
});
const compiledNodeField = (value, data) => {
if (typeof value === "string") {
return template(value, data);
}
if (value?.isBindDataSource && value.dataSourceId) {
return data[value.dataSourceId];
}
if (value?.isBindDataSourceField && value.dataSourceId && typeof value.template === "string") {
return template(value.template, data[value.dataSourceId]);
}
if (Array.isArray(value) && typeof value[0] === "string") {
return compliedDataSourceField(value, data);
}
return value;
};
const compliedIteratorItem = ({
compile,
dsId,
item,
deps,
condDeps,
inEditor,
ctxData
}) => {
const { items, ...node } = item;
const newNode = cloneDeep(node);
if (condDeps[node.id]?.keys.length && !inEditor) {
newNode.condResult = compliedConditions(node, ctxData);
}
if (Array.isArray(items) && items.length) {
newNode.items = items.map(
(item2) => compliedIteratorItem({ compile, dsId, item: item2, deps, condDeps, inEditor, ctxData })
);
} else if (items) {
newNode.items = items;
}
if (!deps[newNode.id]?.keys.length) {
return newNode;
}
return compiledNode(
compile,
newNode,
{
[dsId]: deps
},
dsId
);
};
const registerDataSourceOnDemand = async (dsl, dataSourceModules) => {
const { dataSourceMethodsDeps = {}, dataSourceCondDeps = {}, dataSourceDeps = {}, dataSources = [] } = dsl;
const dsModuleMap = {};
dataSources.forEach((ds) => {
let dep = dataSourceCondDeps[ds.id] || {};
if (!Object.keys(dep).length) {
dep = dataSourceDeps[ds.id] || {};
}
if (!Object.keys(dep).length) {
dep = dataSourceMethodsDeps[ds.id] || {};
}
if (Object.keys(dep).length && dataSourceModules[ds.type]) {
dsModuleMap[ds.type] = dataSourceModules[ds.type];
}
});
const modules = await Promise.all(Object.values(dsModuleMap).map((asyncModule) => asyncModule()));
const moduleMap = {};
modules.forEach((module, index) => {
const type = Object.keys(dsModuleMap)[index];
moduleMap[type] = module.default;
});
return moduleMap;
};
class DataSourceManager extends EventEmitter$1 {
static dataSourceClassMap = /* @__PURE__ */ new Map([
["base", DataSource],
["http", HttpDataSource]
]);
static ObservedDataClass = SimpleObservedData;
static waitInitSchemaList = /* @__PURE__ */ new Map();
static register(type, dataSource) {
DataSourceManager.dataSourceClassMap.set(type, dataSource);
DataSourceManager.waitInitSchemaList?.forEach((listMap, app) => {
const list = listMap[type] || [];
for (let config = list.shift(); config; config = list.shift()) {
const ds = app.addDataSource(config);
if (ds) {
app.init(ds);
}
}
});
}
static getDataSourceClass(type) {
return DataSourceManager.dataSourceClassMap.get(type);
}
static clearDataSourceClass() {
DataSourceManager.dataSourceClassMap.clear();
DataSourceManager.dataSourceClassMap.set("base", DataSource);
DataSourceManager.dataSourceClassMap.set("http", HttpDataSource);
}
static registerObservedData(ObservedDataClass) {
DataSourceManager.ObservedDataClass = ObservedDataClass;
}
app;
dataSourceMap = /* @__PURE__ */ new Map();
data = {};
initialData = {};
useMock = false;
constructor({ app, useMock, initialData }) {
super();
DataSourceManager.waitInitSchemaList.set(this, {});
this.app = app;
this.useMock = useMock;
if (initialData) {
this.initialData = initialData;
this.data = { ...initialData };
}
app.dsl?.dataSources?.forEach((config) => {
this.addDataSource(config);
});
if (this.isAllDataSourceRegistered()) {
this.callDsInit();
} else {
this.on("registered-all", () => {
this.callDsInit();
});
}
}
async init(ds) {
if (ds.isInit) {
return;
}
if (this.app.jsEngine && ds.schema.disabledInitInJsEngine?.includes(this.app.jsEngine)) {
return;
}
for (const method of ds.methods) {
if (typeof method.content !== "function") return;
if (method.timing === "beforeInit") {
await method.content({ params: {}, dataSource: ds, app: this.app });
}
}
await ds.init();
for (const method of ds.methods) {
if (typeof method.content !== "function") return;
if (method.timing === "afterInit") {
await method.content({ params: {}, dataSource: ds, app: this.app });
}
}
}
get(id) {
return this.dataSourceMap.get(id);
}
addDataSource(config) {
if (!config) return;
const DataSourceClass = DataSourceManager.dataSourceClassMap.get(config.type);
if (!DataSourceClass) {
let listMap = DataSourceManager.waitInitSchemaList.get(this);
if (!listMap) {
listMap = {};
DataSourceManager.waitInitSchemaList.set(this, listMap);
}
if (listMap[config.type]) {
listMap[config.type].push(config);
} else {
listMap[config.type] = [config];
}
this.data[config.id] = this.initialData[config.id] ?? getDefaultValueFromFields(config.fields);
return;
}
const ds = new DataSourceClass({
app: this.app,
schema: config,
request: this.app.request,
useMock: this.useMock,
initialData: this.initialData[config.id],
ObservedDataClass: DataSourceManager.ObservedDataClass
});
this.dataSourceMap.set(config.id, ds);
this.data[ds.id] = ds.data;
ds.on("change", (changeEvent) => {
this.setData(ds, changeEvent);
});
if (this.isAllDataSourceRegistered()) {
this.emit("registered-all");
}
return ds;
}
setData(ds, changeEvent) {
this.data[ds.id] = ds.data;
this.emit("change", ds.id, changeEvent);
}
removeDataSource(id) {
this.get(id)?.destroy();
delete this.data[id];
this.dataSourceMap.delete(id);
}
/**
* 更新数据源dsl,在编辑器中修改配置后需要更新,一般在其他环境下不需要更新dsl
* @param {DataSourceSchema[]} schemas 所有数据源配置
*/
updateSchema(schemas) {
for (const schema of schemas) {
const ds = this.get(schema.id);
if (!ds) {
return;
}
this.removeDataSource(schema.id);
}
for (const schema of schemas) {
this.addDataSource(cloneDeep(schema));
const newDs = this.get(schema.id);
if (newDs) {
this.init(newDs);
}
}
}
/**
* 将组件dsl中所有key中数据源相关的配置编译成对应的值
* @param {MNode} node 组件dsl
* @param {string | number} sourceId 数据源ID
* @param {boolean} deep 是否编译子项(items),默认为false
* @returns {MNode} 编译后的组件dsl
*/
compiledNode(n, sourceId, deep = false) {
if (n[NODE_DISABLE_DATA_SOURCE_KEY]) {
return n;
}
const { items, ...node } = n;
const newNode = cloneDeep(node);
if (items) {
newNode.items = Array.isArray(items) && deep ? items.map((item) => this.compiledNode(item, sourceId, deep)) : items;
}
if (node.condResult === false) return newNode;
if (node.visible === false) return newNode;
return compiledNode(
(value) => compiledNodeField(value, this.data),
newNode,
this.app.dsl?.dataSourceDeps || {},
sourceId
);
}
/**
* 编译组件条件组配置(用于配置组件显示时机)
* @param {{ [NODE_CONDS_KEY]?: DisplayCond[] }} node 显示条件组配置
* @returns {boolean} 是否显示
*/
compliedConds(node) {
if (node[NODE_DISABLE_DATA_SOURCE_KEY]) {
return true;
}
return compliedConditions(node, this.data);
}
/**
* 编译迭代器容器的迭代项的显示条件
* @param {any[]} itemData 迭代数据
* @param {{ [NODE_CONDS_KEY]?: DisplayCond[] }} node 显示条件组配置
* @param {string[]} dataSourceField 迭代数据在数据源中的字段,格式如['dsId', 'key1', 'key2']
* @returns {boolean}是否显示
*/
compliedIteratorItemConds(itemData, node, dataSourceField = []) {
const [dsId, ...keys] = dataSourceField;
const ds = this.get(dsId);
if (!ds) return true;
const ctxData = createIteratorContentData(itemData, ds.id, keys, this.data);
return compliedConditions(node, ctxData);
}
compliedIteratorItems(itemData, nodes, dataSourceField = []) {
const [dsId, ...keys] = dataSourceField;
const ds = this.get(dsId);
if (!ds) return nodes;
const inEditor = this.app.platform === "editor";
const ctxData = createIteratorContentData(itemData, ds.id, keys, this.data);
const { deps = {}, condDeps = {} } = getDeps(ds.schema, nodes, inEditor);
if (!Object.keys(deps).length && !Object.keys(condDeps).length) {
return nodes;
}
return nodes.map(
(item) => compliedIteratorItem({
compile: (value) => compiledNodeField(value, ctxData),
dsId: ds.id,
item,
deps,
condDeps,
inEditor,
ctxData
})
);
}
isAllDataSourceRegistered() {
return !this.app.dsl?.dataSources?.length || this.dataSourceMap.size === this.app.dsl.dataSources.length;
}
destroy() {
this.removeAllListeners();
this.data = {};
this.initialData = {};
this.dataSourceMap.forEach((ds) => {
ds.destroy();
});
this.dataSourceMap.clear();
DataSourceManager.waitInitSchemaList.delete(this);
}
onDataChange(id, path, callback) {
return this.get(id)?.onDataChange(path, callback);
}
offDataChange(id, path, callback) {
return this.get(id)?.offDataChange(path, callback);
}
callDsInit() {
const dataSourceList = Array.from(this.dataSourceMap);
if (typeof Promise.allSettled === "function") {
Promise.allSettled(dataSourceList.map(([, ds]) => this.init(ds))).then((values) => {
const data = {};
const errors = {};
values.forEach((value, index) => {
const dsId = dataSourceList[index][0];
if (value.status === "fulfilled") {
if (this.data[dsId]) {
data[dsId] = this.data[dsId];
} else {
delete data[dsId];
}
} else if (value.status === "rejected") {
delete data[dsId];
errors[dsId] = value.reason;
}
});
this.emit("init", data, errors);
});
} else {
Promise.all(dataSourceList.map(([, ds]) => this.init(ds))).then(() => {
this.emit("init", this.data);
}).catch(() => {
this.emit("init", this.data);
});
}
}
}
const createDataSourceManager = (app, useMock, initialData) => {
const { dsl, platform } = app;
if (!dsl?.dataSources) return;
const dataSourceManager = new DataSourceManager({ app, useMock, initialData });
if (dsl.dataSources && dsl.dataSourceCondDeps && platform !== "editor") {
getNodes(getDepNodeIds(dsl.dataSourceCondDeps), dsl.items).forEach((node) => {
node.condResult = dataSourceManager.compliedConds(node);
updateNode(node, dsl);
});
}
if (dsl.dataSources && dsl.dataSourceDeps) {
getNodes(getDepNodeIds(dsl.dataSourceDeps), dsl.items).forEach((node) => {
updateNode(dataSourceManager.compiledNode(node), dsl);
});
}
if (app.jsEngine !== "nodejs") {
dataSourceManager.on("change", (sourceId, changeEvent) => {
const dep = dsl.dataSourceDeps?.[sourceId] || {};
const condDep = dsl.dataSourceCondDeps?.[sourceId] || {};
const nodeIds = union([...Object.keys(condDep), ...Object.keys(dep)]);
const pages = app.page?.data && app.platform !== "editor" ? [app.page.data] : dsl.items;
dataSourceManager.emit(
"update-data",
getNodes(nodeIds, pages).map((node) => {
if (app.platform !== "editor") {
node.condResult = dataSourceManager.compliedConds(node);
}
const newNode = dataSourceManager.compiledNode(node);
if (typeof app.page?.setData === "function") {
if (isPage(newNode)) {
app.page.setData(newNode);
} else {
const n = app.page.getNode(node.id);
n?.setData(newNode);
}
}
return newNode;
}),
sourceId,
changeEvent
);
});
}
return dataSourceManager;
};
const ignoreFirstCall = (fn) => {
let calledTimes = 0;
return (...args) => {
if (calledTimes === 0) {
calledTimes += 1;
return;
}
return fn(...args);
};
};
class DeepObservedData extends ObservedData {
state;
subscribers = /* @__PURE__ */ new Map();
constructor(initialData) {
super();
this.state = new State(initialData);
}
update = (data, path) => {
this.state?.update(path ?? "", data);
};
on = (path, callback) => {
const unsubscribe = this.state.subscribe(path, ignoreFirstCall(callback));
const pathSubscribers = this.subscribers.get(path) ?? /* @__PURE__ */ new Map();
pathSubscribers.set(callback, unsubscribe);
this.subscribers.set(path, pathSubscribers);
};
off = (path, callback) => {
const pathSubscribers = this.subscribers.get(path);
if (!pathSubscribers) return;
pathSubscribers.get(callback)?.();
pathSubscribers.delete(callback);
};
getData = (path) => !this.state ? {} : this.state?.get(path);
destroy = () => {
this.subscribers.forEach((pathSubscribers) => {
pathSubscribers.forEach((unsubscribe) => unsubscribe());
});
};
}
export { DataSource, DataSourceManager, DeepObservedData, HttpDataSource, ObservedData, SimpleObservedData, compiledCondition, compiledNodeField, compliedConditions, compliedDataSourceField, compliedIteratorItem, createDataSourceManager, createIteratorContentData, registerDataSourceOnDemand, template, updateNode };