UNPKG

@nocobase/flow-engine

Version:

A standalone flow engine for NocoBase, managing workflows, models, and actions.

813 lines (690 loc) 22.2 kB
/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import { observable } from '@formily/reactive'; import _ from 'lodash'; import { FlowEngine } from '../flowEngine'; import { jioToJoiSchema } from './jioToJoiSchema'; export interface DataSourceOptions extends Record<string, any> { key: string; displayName?: string; description?: string; [key: string]: any; } export class DataSourceManager { dataSources: Map<string, DataSource>; flowEngine: FlowEngine; constructor() { this.dataSources = observable.shallow<Map<string, DataSource>>(new Map()); } setFlowEngine(flowEngine: FlowEngine) { this.flowEngine = flowEngine; } addDataSource(ds: DataSource | DataSourceOptions) { if (this.dataSources.has(ds.key)) { throw new Error(`DataSource with name ${ds.key} already exists`); } if (ds instanceof DataSource) { this.dataSources.set(ds.key, ds); } else { const clz = ds.use || DataSource; ds = new clz(ds); this.dataSources.set(ds.key, ds as DataSource); } ds.setDataSourceManager(this); } upsertDataSource(ds: DataSource | DataSourceOptions) { if (this.dataSources.has(ds.key)) { this.dataSources.get(ds.key)?.setOptions(ds); } else { this.addDataSource(ds); } } removeDataSource(key: string) { this.dataSources.delete(key); } clearDataSources() { this.dataSources.clear(); } getDataSources(): DataSource[] { return Array.from(this.dataSources.values()); } getDataSource(key: string): DataSource | undefined { return this.dataSources.get(key); } getCollection(dataSourceKey: string, collectionName: string): Collection | undefined { const ds = this.getDataSource(dataSourceKey); if (!ds) return undefined; return ds.collectionManager.getCollection(collectionName); } getCollectionField(fieldPathWithDataSource: string) { const [dataSourceKey, ...otherKeys] = fieldPathWithDataSource.split('.'); const ds = this.getDataSource(dataSourceKey); if (!ds) return undefined; return ds.getCollectionField(otherKeys.join('.')); } } export class DataSource { dataSourceManager: DataSourceManager; collectionManager: CollectionManager; options: Record<string, any>; constructor(options: Record<string, any> = {}) { this.options = observable({ ...options }); this.collectionManager = new CollectionManager(this); } get flowEngine() { return this.dataSourceManager.flowEngine; } get displayName() { return this.options.displayName ? this.flowEngine.translate(this.options.displayName) : this.key; } get key() { return this.options.key; } get name() { return this.options.key; } setDataSourceManager(dataSourceManager: DataSourceManager) { this.dataSourceManager = dataSourceManager; } getCollections(): Collection[] { return this.collectionManager.getCollections(); } getCollection(name: string): Collection | undefined { return this.collectionManager.getCollection(name); } /** * @deprecated use getAssociation instead */ getAssocation(associationName: string): CollectionField | undefined { return this.getAssociation(associationName); } getAssociation(associationName: string): CollectionField | undefined { return this.collectionManager.getAssociation(associationName); } addCollection(collection: Collection | CollectionOptions) { return this.collectionManager.addCollection(collection); } updateCollection(newOptions: CollectionOptions) { return this.collectionManager.updateCollection(newOptions); } upsertCollection(options: CollectionOptions) { return this.collectionManager.upsertCollection(options); } upsertCollections(collections: CollectionOptions[]) { return this.collectionManager.upsertCollections(collections); } removeCollection(name: string) { return this.collectionManager.removeCollection(name); } clearCollections() { this.collectionManager.clearCollections(); } setOptions(newOptions: any = {}) { Object.keys(this.options).forEach((key) => delete this.options[key]); Object.assign(this.options, newOptions); } getCollectionField(fieldPath: string) { const [collectionName, ...otherKeys] = fieldPath.split('.'); const fieldName = otherKeys.join('.'); const collection = this.getCollection(collectionName); if (!collection) { throw new Error(`Collection ${collectionName} not found in data source ${this.key}`); } const field = collection.getFieldByPath(fieldName); if (!field) { return; } return field; } } export interface CollectionOptions { name: string; title?: string; inherits?: string[]; [key: string]: any; } export class CollectionManager { collections: Map<string, Collection>; constructor(public dataSource: DataSource) { this.collections = observable.shallow<Map<string, Collection>>(new Map()); } get flowEngine() { return this.dataSource.flowEngine; } addCollection(collection: Collection | CollectionOptions) { let col: Collection; if (collection instanceof Collection) { col = collection; } else { col = new Collection(collection); } col.setDataSource(this.dataSource); col.initInherits(); this.collections.set(col.name, col); } removeCollection(name: string) { this.collections.delete(name); } updateCollection(newOptions: CollectionOptions) { const collection = this.getCollection(newOptions.name); if (!collection) { throw new Error(`Collection ${newOptions.name} not found`); } collection.setOptions(newOptions); } upsertCollection(options: CollectionOptions) { if (this.collections.has(options.name)) { this.updateCollection(options); } else { this.addCollection(options); } return this.getCollection(options.name); } upsertCollections(collections: CollectionOptions[]) { for (const collection of this.sortCollectionsByInherits(collections)) { if (this.collections.has(collection.name)) { this.updateCollection(collection); } else { this.addCollection(collection); } } } sortCollectionsByInherits(collections: CollectionOptions[]): CollectionOptions[] { // 1. 构建 name -> CollectionOptions 映射 const map = new Map<string, CollectionOptions>(); for (const col of collections) { map.set(col.name, col); } // 2. 构建依赖图和入度表 const graph: Record<string, Set<string>> = {}; const inDegree: Record<string, number> = {}; for (const col of collections) { graph[col.name] = new Set(); inDegree[col.name] = 0; } for (const col of collections) { const inherits = col.inherits || []; for (const parent of inherits) { if (!graph[parent]) graph[parent] = new Set(); graph[parent].add(col.name); inDegree[col.name] = (inDegree[col.name] || 0) + 1; } } // 3. Kahn 算法拓扑排序 const queue: string[] = []; for (const name in inDegree) { if (inDegree[name] === 0) queue.push(name); } const result: CollectionOptions[] = []; while (queue.length) { const curr = queue.shift(); if (map.has(curr)) { result.push(map.get(curr)); } for (const child of graph[curr] || []) { inDegree[child]--; if (inDegree[child] === 0) queue.push(child); } } // 4. 检查是否有环 if (result.length !== collections.length) { throw new Error('Collection inherits has circular dependency!'); } return result; } getCollection(name: string): Collection | undefined { if (name.includes('.')) { const [collectionName, fieldName] = name.split('.'); const collection = this.getCollection(collectionName); if (!collection) { throw new Error(`Collection ${collectionName} not found in data source ${this.dataSource.key}`); } const field = collection.getField(fieldName); if (!field) { throw new Error(`Field ${fieldName} not found in collection ${collectionName}`); } return field.targetCollection; } return this.collections.get(name); } getCollections(): Collection[] { return _.sortBy( Array.from(this.collections.values()).filter((collection) => !collection.hidden), 'sort', ); } clearCollections() { this.collections.clear(); } getAssociation(associationName: string): CollectionField | undefined { const [collectionName, fieldName] = associationName.split('.'); const collection = this.getCollection(collectionName); if (!collection) { throw new Error(`Collection ${collectionName} not found in data source ${this.dataSource.key}`); } return collection.getField(fieldName); } } // Collection 负责管理自己的 Field export class Collection { fields: Map<string, CollectionField>; options: Record<string, any>; inherits: Map<string, Collection>; dataSource: DataSource; constructor(options: Record<string, any> = {}) { this.options = observable({ ...options }); this.fields = observable.shallow<Map<string, CollectionField>>(new Map()); this.inherits = observable.shallow<Map<string, Collection>>(new Map()); this.setFields(options.fields || []); } getFilterByTK(record) { if (!record) { throw new Error('Record is required to get filterByTk'); } if (Array.isArray(record)) { return record.map((r) => this.getFilterByTK(r)); } if (!this.filterTargetKey) { throw new Error(`filterTargetKey is not defined for collection ${this.name}`); } if (typeof this.filterTargetKey === 'string') { return record[this.filterTargetKey]; } return _.pick(record, this.filterTargetKey); } get hidden() { return this.options.hidden || false; } get flowEngine() { return this.dataSource.flowEngine; } get collectionManager() { return this.dataSource.collectionManager; } get sort() { return this.options.sort || 0; } get filterTargetKey() { return this.options.filterTargetKey; } get dataSourceKey() { return this.dataSource.key; } get name() { return this.options.name; } get template() { return this.options.template; } get storage() { return this.options.storage || 'local'; } get title() { return this.options.title ? this.flowEngine.translate(this.options.title) : this.name; } get titleCollectionField() { const titleFieldName = this.options.titleField || this.filterTargetKey; const titleCollectionField = this.getField(titleFieldName); return titleCollectionField; } initInherits() { this.inherits.clear(); for (const inherit of this.options.inherits || []) { const collection = this.collectionManager.getCollection(inherit); if (!collection) { throw new Error(`Collection ${inherit} not found`); } this.inherits.set(inherit, collection); } } setDataSource(dataSource: DataSource) { this.dataSource = dataSource; } setOptions(newOptions: any = {}) { Object.keys(this.options).forEach((key) => delete this.options[key]); Object.assign(this.options, newOptions); this.initInherits(); this.upsertFields(this.options.fields || []); } getFields(): CollectionField[] { // 合并自身 fields 和所有 inherits 的 fields,后者优先被覆盖 const fieldMap = new Map<string, CollectionField>(); for (const inherit of this.inherits.values()) { if (inherit && typeof inherit.getFields === 'function') { for (const field of inherit.getFields()) { fieldMap.set(field.name, field); } } } // 自身 fields 覆盖同名 for (const [name, field] of this.fields.entries()) { fieldMap.set(name, field); } return Array.from(fieldMap.values()); } getToOneAssociationFields(): CollectionField[] { return this.getAssociationFields(['one']); } getAssociationFields(types = []): CollectionField[] { if (types.includes('new')) { return this.getFields().filter((field) => ['hasMany', 'belongsToMany', 'belongsToArray'].includes(field.type)); } if (types.includes('many') && types.includes('one')) { return this.getFields().filter((field) => field.isAssociationField()); } if (types.includes('many')) { return this.getFields().filter((field) => ['hasMany', 'belongsToMany', 'belongsToArray'].includes(field.type)); } if (types.includes('one')) { return this.getFields().filter((field) => ['hasOne', 'belongsTo'].includes(field.type)); } return this.getFields().filter((field) => field.isAssociationField()); } mapFields(callback: (field: CollectionField) => any): any[] { return this.getFields().map(callback); } setFields(fields: CollectionField[] | Record<string, any>[]) { this.fields.clear(); for (const field of fields) { this.addField(field); } } upsertFields(fields: Record<string, any>[] = []) { for (const field of fields) { if (this.fields.has(field.name)) { this.fields.get(field.name).setOptions(field); } else { this.addField(field); } } } getFieldByPath(fieldPath: string): CollectionField | undefined { const [fieldName, ...otherKeys] = fieldPath.split('.'); const field = this.getField(fieldName); if (otherKeys.length === 0) { return field; } if (!field.targetCollection) { return null; } return field.targetCollection.getFieldByPath(otherKeys.join('.')); } getField(fieldName: string): CollectionField | undefined { return this.fields.get(fieldName); } getFullFieldPath(name: string): string { return this.dataSource.key + '.' + this.name + '.' + name; } addField(field: CollectionField | Record<string, any>) { if (field.name && this.fields.has(field.name)) { throw new Error(`Field with name ${field.name} already exists in collection ${this.name}`); } if (field instanceof CollectionField) { field.setCollection(this); this.fields.set(field.name, field); } else { const newField = new CollectionField(field); newField.setCollection(this); this.fields.set(newField.name, newField); } } removeField(fieldName: string) { return this.fields.delete(fieldName); } clearFields() { return this.fields.clear(); } refresh() { // 刷新集合 } /** * 获取所有关联字段 * @returns 关联字段数组 */ getRelationshipFields(): CollectionField[] { const relationshipInterfaces = [ 'o2o', 'oho', 'obo', 'm2o', 'createdBy', 'updatedBy', 'o2m', 'm2m', 'linkTo', 'chinaRegion', 'mbm', ]; return this.getFields().filter((field) => relationshipInterfaces.includes(field.interface)); } /** * 获取所有关联的集合 * @returns 关联集合数组 */ getRelatedCollections(): Collection[] { const relationshipFields = this.getRelationshipFields(); const relatedCollections: Collection[] = []; const addedCollectionNames = new Set<string>(); for (const field of relationshipFields) { if (field.target && !addedCollectionNames.has(field.target)) { const targetCollection = this.collectionManager.getCollection(field.target); if (targetCollection && targetCollection.name !== this.name) { relatedCollections.push(targetCollection); addedCollectionNames.add(field.target); } } } return relatedCollections; } /** * 检查是否有关联字段 * @returns 是否有关联字段 */ hasRelationshipFields(): boolean { return this.getRelationshipFields().length > 0; } } export class CollectionField { options: Record<string, any>; collection: Collection; constructor(options: Record<string, any>) { this.options = observable({ ...options }); } setOptions(newOptions: any = {}) { Object.keys(this.options).forEach((key) => delete this.options[key]); Object.assign(this.options, newOptions); } setCollection(collection: Collection) { this.collection = collection; } get targetCollectionTitleFieldName() { return this.targetCollection?.titleCollectionField?.name; } get targetCollectionTitleField() { return this.targetCollection?.titleCollectionField; } get flowEngine() { return this.collection.flowEngine; } get dataSourceKey() { return this.collection?.dataSourceKey; } get resourceName() { return `${this.collection.name}.${this.name}`; } get collectionName() { return this.collection?.name || this.options.collectionName; } get readonly() { return this.options.readonly || this.options.uiSchema?.['x-read-pretty'] || false; } get fullpath() { return this.collection.dataSource.key + '.' + this.collection.name + '.' + this.name; } get name() { return this.options.name; } get type() { return this.options.type; } get dataType() { return this.options.dataType; } get foreignKey() { return this.options.foreignKey; } get targetKey() { return this.options.targetKey || this.targetCollection.filterTargetKey; } get sourceKey() { return this.options.sourceKey; } get target() { return this.options.target; } get title() { const titleValue = this.options?.title || this.options?.uiSchema?.title || this.options.name; return this.flowEngine.translate(titleValue); } set title(value: string) { this.options.title = value; } get enum(): any[] { return this.options.uiSchema?.enum || []; } get defaultValue() { return this.options.defaultValue == null ? undefined : this.options.defaultValue; } get interface() { return this.options.interface; } get filterable() { return this.options.filterable || this.getInterfaceOptions()?.filterable; } get inputable() { return this.options.inputable; } get uiSchema() { return this.options.uiSchema || {}; } get targetCollection() { return this.options.target && this.collection?.collectionManager.getCollection(this.options.target); } get validation() { return this.options.validation; } getComponentProps() { const { type, target } = this.options; const componentProps = _.omitBy( { ..._.omit(this.options.uiSchema?.['x-component-props'] || {}, 'fieldNames'), options: this.enum.length ? this.enum : undefined, mode: this.type === 'array' ? 'multiple' : undefined, multiple: target ? ['belongsToMany', 'hasMany', 'belongsToArray'].includes(type) : undefined, maxCount: target && !['belongsToMany', 'hasMany', 'belongsToArray'].includes(type) ? 1 : undefined, target: target, }, _.isUndefined, ); if (this.validation) { // 初始化数据表字段jio验证规则 const rules = []; const schema = jioToJoiSchema(this.validation); const label = this.title; rules.push({ validator: (_, value) => { const { error } = schema.validate(value, { context: { label }, abortEarly: false, }); if (error) { const message = error.details.map((d: any) => d.message.replace(/"value"/g, `"${label}"`)).join(', '); return Promise.reject(message); } return Promise.resolve(); }, }); componentProps.rules = rules; } return componentProps; } getFields(): CollectionField[] { if (!this.options.target) { return []; } if (!this.targetCollection) { throw new Error(`Target collection ${this.options.target} not found for field ${this.name}`); } return this.targetCollection.getFields(); } getInterfaceOptions() { const app = this.flowEngine.context.app; return app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface(this.interface); } getFilterOperators() { const opts = this.getInterfaceOptions(); return opts?.filterable?.operators || []; } getSubclassesOf(baseClass: string) { return this.flowEngine.getSubclassesOf(baseClass, (M, name) => { const interfaceMatch = isFieldInterfaceMatch(M['supportedFieldInterfaces'], this.interface); return interfaceMatch; }); } getFirstSubclassNameOf(baseClass: string) { const subclasses = this.getSubclassesOf(baseClass); for (const [name, M] of subclasses) { if (M['supportedFieldInterfaces'] !== '*') { return name; } } return undefined; } isAssociationField() { return ['belongsToMany', 'belongsTo', 'hasMany', 'hasOne', 'belongsToArray'].includes(this.type); } /** * 检查字段是否为关联字段 * @returns 是否为关联字段 */ isRelationshipField(): boolean { const relationshipInterfaces = [ 'o2o', 'oho', 'obo', 'm2o', 'createdBy', 'updatedBy', 'o2m', 'm2m', 'linkTo', 'chinaRegion', 'mbm', ]; return relationshipInterfaces.includes(this.interface); } } /** * 判断 fieldInterfaces 是否匹配 targetInterface * @param fieldInterfaces string | string[] | null * @param targetInterface string */ export function isFieldInterfaceMatch( fieldInterfaces: string | string[] | null | undefined, targetInterface: string, ): boolean { if (!fieldInterfaces) return false; if (fieldInterfaces === '*') return true; if (typeof fieldInterfaces === 'string') return fieldInterfaces === targetInterface; if (Array.isArray(fieldInterfaces)) { return fieldInterfaces.includes('*') || fieldInterfaces.includes(targetInterface); } return false; } export { jioToJoiSchema };