UNPKG

@tmagic/data-source

Version:
832 lines (821 loc) 26 kB
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, NODE_CONDS_RESULT_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, options) { if (options?.immediate) { callback(this.getData(path)); } 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 || typeof node.condResult === "undefined" && node[NODE_CONDS_RESULT_KEY]) { 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, data = this.data) { if (node[NODE_DISABLE_DATA_SOURCE_KEY]) { return true; } const result = compliedConditions(node, data); if (!node[NODE_CONDS_RESULT_KEY]) { return result; } return !result; } /** * 编译迭代器容器的迭代项的显示条件 * @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 this.compliedConds(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") { return dataSourceManager; } dataSourceManager.on("change", (sourceId, changeEvent) => { const dep = dsl.dataSourceDeps?.[sourceId] || {}; const condDep = dsl.dataSourceCondDeps?.[sourceId] || {}; const nodeIds = union([...Object.keys(condDep), ...Object.keys(dep)]); for (const page of dsl.items) { if (app.platform === "editor" || isPage(page) && page.id === app.page?.data.id || isPageFragment(page)) { const newNodes = getNodes(nodeIds, [page]).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 if (page.id === app.page.data.id && !app.page.instance) { replaceChildNode(newNode, [app.page.data]); } app.getNode(node.id, { strict: true })?.setData(newNode); for (const [, pageFragment] of app.pageFragments) { if (pageFragment.data.id === newNode.id) { pageFragment.setData(newNode); } else if (pageFragment.data.id === page.id) { pageFragment.getNode(newNode.id, { strict: true })?.setData(newNode); if (!pageFragment.instance) { replaceChildNode(newNode, [pageFragment.data]); } } } } return newNode; }); if (newNodes.length) { dataSourceManager.emit("update-data", newNodes, sourceId, changeEvent, page.id); } } } }); 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, options) => { const unsubscribe = this.state.subscribe(path, options?.immediate ? callback : 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 };