@tmagic/data-source
Version:
396 lines (329 loc) • 11.5 kB
text/typescript
/* eslint-disable @typescript-eslint/naming-convention */
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import EventEmitter from 'events';
import { cloneDeep } from 'lodash-es';
import type { DataSourceSchema, default as TMagicApp, DisplayCond, Id, MNode } from '@tmagic/core';
import {
compiledNode,
getDefaultValueFromFields,
NODE_CONDS_KEY,
NODE_CONDS_RESULT_KEY,
NODE_DISABLE_DATA_SOURCE_KEY,
} from '@tmagic/core';
import { SimpleObservedData } from './observed-data/SimpleObservedData';
import { DataSource, HttpDataSource } from './data-sources';
import { getDeps } from './depsCache';
import type {
ChangeEvent,
DataSourceManagerData,
DataSourceManagerOptions,
ObservedDataClass,
SchemaListMap,
} from './types';
import { compiledNodeField, compliedConditions, compliedIteratorItem, createIteratorContentData } from './utils';
class DataSourceManager extends EventEmitter {
private static dataSourceClassMap = new Map<string, any>([
['base', DataSource],
['http', HttpDataSource],
]);
private static ObservedDataClass: ObservedDataClass = SimpleObservedData;
private static waitInitSchemaList = new Map<DataSourceManager, SchemaListMap>();
public static register<T extends typeof DataSource = typeof DataSource>(type: string, dataSource: T) {
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);
}
}
});
}
public static getDataSourceClass(type: string) {
return DataSourceManager.dataSourceClassMap.get(type);
}
public static clearDataSourceClass() {
DataSourceManager.dataSourceClassMap.clear();
DataSourceManager.dataSourceClassMap.set('base', DataSource);
DataSourceManager.dataSourceClassMap.set('http', HttpDataSource);
}
public static registerObservedData(ObservedDataClass: ObservedDataClass) {
DataSourceManager.ObservedDataClass = ObservedDataClass;
}
public app: TMagicApp;
public dataSourceMap = new Map<string, DataSource & Record<string, any>>();
public data: DataSourceManagerData = {};
public initialData: DataSourceManagerData = {};
public useMock?: boolean = false;
constructor({ app, useMock, initialData }: DataSourceManagerOptions) {
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();
});
}
}
public async init(ds: DataSource) {
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 });
}
}
}
public get(id: string) {
return this.dataSourceMap.get(id);
}
public addDataSource(config?: DataSourceSchema) {
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: ChangeEvent) => {
this.setData(ds, changeEvent);
});
if (this.isAllDataSourceRegistered()) {
this.emit('registered-all');
}
return ds;
}
public setData(ds: DataSource, changeEvent: ChangeEvent) {
this.data[ds.id] = ds.data;
this.emit('change', ds.id, changeEvent);
}
public removeDataSource(id: string) {
this.get(id)?.destroy();
delete this.data[id];
this.dataSourceMap.delete(id);
}
/**
* 更新数据源dsl,在编辑器中修改配置后需要更新,一般在其他环境下不需要更新dsl
* @param {DataSourceSchema[]} schemas 所有数据源配置
*/
public updateSchema(schemas: DataSourceSchema[]) {
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
*/
public compiledNode(n: MNode, sourceId?: Id, 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: any) => compiledNodeField(value, this.data),
newNode,
this.app.dsl?.dataSourceDeps || {},
sourceId,
);
}
/**
* 编译组件条件组配置(用于配置组件显示时机)
* @param {{ [NODE_CONDS_KEY]?: DisplayCond[] }} node 显示条件组配置
* @returns {boolean} 是否显示
*/
public compliedConds(
node: {
[NODE_CONDS_KEY]?: DisplayCond[];
[NODE_CONDS_RESULT_KEY]?: boolean;
[NODE_DISABLE_DATA_SOURCE_KEY]?: boolean;
},
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}是否显示
*/
public compliedIteratorItemConds(
itemData: any,
node: {
[NODE_CONDS_KEY]?: DisplayCond[];
[NODE_CONDS_RESULT_KEY]?: boolean;
[NODE_DISABLE_DATA_SOURCE_KEY]?: boolean;
},
dataSourceField: string[] = [],
) {
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);
}
public compliedIteratorItems(itemData: any, nodes: MNode[], dataSourceField: string[] = []): MNode[] {
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: any) => compiledNodeField(value, ctxData),
dsId: ds.id,
item,
deps,
condDeps,
inEditor,
ctxData,
}),
);
}
public isAllDataSourceRegistered() {
return !this.app.dsl?.dataSources?.length || this.dataSourceMap.size === this.app.dsl.dataSources.length;
}
public destroy() {
this.removeAllListeners();
this.data = {};
this.initialData = {};
this.dataSourceMap.forEach((ds) => {
ds.destroy();
});
this.dataSourceMap.clear();
DataSourceManager.waitInitSchemaList.delete(this);
}
public onDataChange(id: string, path: string, callback: (newVal: any) => void) {
return this.get(id)?.onDataChange(path, callback);
}
public offDataChange(id: string, path: string, callback: (newVal: any) => void) {
return this.get(id)?.offDataChange(path, callback);
}
private callDsInit() {
const dataSourceList = Array.from(this.dataSourceMap);
if (typeof Promise.allSettled === 'function') {
Promise.allSettled<Record<string, any>>(dataSourceList.map(([, ds]) => this.init(ds))).then((values) => {
const data: DataSourceManagerData = {};
const errors: Record<string, Error> = {};
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<Record<string, any>>(dataSourceList.map(([, ds]) => this.init(ds)))
.then(() => {
this.emit('init', this.data);
})
.catch(() => {
this.emit('init', this.data);
});
}
}
}
export default DataSourceManager;