/** * 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'; import { sortCollectionsByInherits } from './sortCollectionsByInherits'; export interface DataSourceOptions extends Record { key: string; displayName?: string; description?: string; [key: string]: any; } export type DataSourceRequester = (options: Record) => Promise; export interface DataSourceLoadResult { collections?: CollectionOptions[]; dataSources?: Array; } export type DataSourceLoader = (context: { key: string; manager: DataSourceManager }) => Promise; export class DataSourceManager { dataSources: Map; flowEngine: FlowEngine; requester?: DataSourceRequester; collectionFieldInterfaceManager?: { addFieldInterfaces?: (fieldInterfaceClasses?: any[]) => void; addFieldInterfaceGroups?: (groups: Record) => void; addFieldInterfaceComponentOption?: (name: string, option: any) => void; addFieldInterfaceOperator?: (name: string, operator: any) => void; registerFieldFilterOperator?: (operator: any) => void; registerFieldFilterOperatorGroup?: (name: string, operators?: any[]) => void; addFieldFilterOperatorsToGroup?: (name: string, operators?: any[]) => void; getFieldInterface?: (name: string) => any; registerFieldInterfaceConfigure?: (options: unknown) => void; getFieldInterfaceConfigure?: (name: string, collectionInfo?: unknown) => unknown; getFieldInterfaceConfigureProperties?: (name: string, collectionInfo?: any) => Record; }; loaders = new Map(); loadedKeys = new Set(); loadingKeys = new Set(); loadErrors = new Map(); loadingPromise: Promise | null = null; constructor() { this.dataSources = observable.shallow>(new Map()); } setFlowEngine(flowEngine: FlowEngine) { this.flowEngine = flowEngine; } setRequester(requester?: DataSourceRequester) { this.requester = requester; } setCollectionFieldInterfaceManager(manager: DataSourceManager['collectionFieldInterfaceManager']) { this.collectionFieldInterfaceManager = manager; } addFieldInterfaces(fieldInterfaceClasses: any[] = []) { this.collectionFieldInterfaceManager?.addFieldInterfaces?.(fieldInterfaceClasses); } addFieldInterfaceGroups(groups: Record) { this.collectionFieldInterfaceManager?.addFieldInterfaceGroups?.(groups); } addFieldInterfaceComponentOption(name: string, option: any) { this.collectionFieldInterfaceManager?.addFieldInterfaceComponentOption?.(name, option); } addFieldInterfaceOperator(name: string, operator: any) { this.collectionFieldInterfaceManager?.addFieldInterfaceOperator?.(name, operator); } registerFieldFilterOperator(operator: any) { this.collectionFieldInterfaceManager?.registerFieldFilterOperator?.(operator); } registerFieldFilterOperatorGroup(name: string, operators: any[] = []) { this.collectionFieldInterfaceManager?.registerFieldFilterOperatorGroup?.(name, operators); } addFieldFilterOperatorsToGroup(name: string, operators: any[] = []) { this.collectionFieldInterfaceManager?.addFieldFilterOperatorsToGroup?.(name, operators); } registerLoader(key: string, loader: DataSourceLoader) { this.loaders.set(key, loader); } removeLoader(key: string) { this.loaders.delete(key); } 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)?.patchOptions(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('.')); } async ensureLoaded(options: { force?: boolean; keys?: string[] } = {}) { const { force = false } = options; const keys = this.resolveLoadKeys(options.keys); const pendingKeys = force ? keys : keys.filter((key) => !this.loadedKeys.has(key)); if (!pendingKeys.length) { return; } if (this.loadingPromise) { return this.loadingPromise; } this.loadingPromise = (async () => { try { for (const key of pendingKeys) { await this.loadKey(key, { initial: !this.loadedKeys.has(key), force }); } } finally { this.loadingPromise = null; } })(); return this.loadingPromise; } async reload(options: { keys?: string[] } = {}) { const keys = this.resolveLoadKeys(options.keys); if (!keys.length) { return; } if (this.loadingPromise) { return this.loadingPromise; } this.loadingPromise = (async () => { try { for (const key of keys) { await this.loadKey(key, { initial: false, force: true }); } } finally { this.loadingPromise = null; } })(); return this.loadingPromise; } async reloadDataSource(key: string) { if (this.loadingKeys.has(key) && this.loadingPromise) { return this.loadingPromise; } if (!this.loaders.has(key) && this.loaders.has('*')) { return this.reload({ keys: ['*'] }); } return this.reload({ keys: [key] }); } protected resolveLoadKeys(requestedKeys?: string[]) { const normalizedKeys = requestedKeys?.length ? requestedKeys : ['main']; const explicitKeys = normalizedKeys.filter((key) => this.loaders.has(key)); if (this.loaders.has('*')) { return _.uniq(['*', ...explicitKeys]); } return explicitKeys.length ? explicitKeys : normalizedKeys; } protected getApp() { return this.flowEngine?.context?.app as { eventBus?: EventTarget } | undefined; } protected dispatchDataSourceEvent( type: 'dataSource:loaded' | 'dataSource:loadFailed', detail: { dataSourceKey: string; initial: boolean; error?: Error }, ) { this.getApp()?.eventBus?.dispatchEvent(new CustomEvent(type, { detail })); } protected setDataSourceState( key: string, options: Partial> & Record, ) { const dataSource = this.getDataSource(key); if (!dataSource) { return; } dataSource.patchOptions(options); } protected applyDataSourceLoadResult(key: string, result: DataSourceLoadResult) { if (key === '*') { const dataSources = result?.dataSources || []; dataSources.forEach((dataSourceOptions) => { const { collections, ...dataSource } = dataSourceOptions; this.upsertDataSource(dataSource); if (collections) { this.getDataSource(dataSource.key)?.setCollections(collections, { clearFields: true }); } }); return; } const dataSource = this.getDataSource(key); if (!dataSource) { return; } dataSource.setCollections(result?.collections || [], { clearFields: true }); } protected async loadKey(key: string, options: { initial: boolean; force: boolean }) { const loader = this.loaders.get(key); if (!loader) { return; } if (!this.getDataSource(key) && key !== '*') { this.addDataSource({ key }); } const { initial } = options; this.loadingKeys.add(key); if (key !== '*') { this.setDataSourceState(key, { status: initial ? 'loading' : 'reloading', errorMessage: undefined, }); } this.loadErrors.set(key, null); try { const result = (await loader({ key, manager: this })) || {}; this.applyDataSourceLoadResult(key, result); this.loadedKeys.add(key); if (key !== '*') { this.setDataSourceState(key, { status: 'loaded', errorMessage: undefined, }); } this.dispatchDataSourceEvent('dataSource:loaded', { dataSourceKey: key, initial }); } catch (error) { const normalizedError = error instanceof Error ? error : new Error(String(error)); this.loadErrors.set(key, normalizedError); if (key !== '*') { this.setDataSourceState(key, { status: initial ? 'loading-failed' : 'reloading-failed', errorMessage: normalizedError.message, }); } this.dispatchDataSourceEvent('dataSource:loadFailed', { dataSourceKey: key, initial, error: normalizedError, }); throw normalizedError; } finally { this.loadingKeys.delete(key); } } } export type CollectionFieldInterfaceDataSourceManager = Pick; export function getCollectionFieldInterface( interfaceName: string | undefined, ...dataSourceManagers: Array ) { if (!interfaceName) { return undefined; } // TODO: Once legacy client is removed and all runtimes share the client-v2 flow-engine // DataSourceManager, callers should only pass the flow-engine context DataSourceManager. for (const dataSourceManager of dataSourceManagers) { const collectionFieldInterfaceManager = dataSourceManager?.collectionFieldInterfaceManager; const getFieldInterface = collectionFieldInterfaceManager?.getFieldInterface; if (typeof getFieldInterface === 'function') { return getFieldInterface.call(collectionFieldInterfaceManager, interfaceName); } } return undefined; } function shouldTranslateOptionLabel(label: unknown): label is string { return typeof label === 'string' && /\{\{\s*t\s*\(/.test(label); } function translateOptionLabel(flowEngine: FlowEngine, label: unknown, options?: Record) { return shouldTranslateOptionLabel(label) ? flowEngine.translate(label, options) : label; } export class DataSource { dataSourceManager: DataSourceManager; collectionManager: CollectionManager; options: Record; constructor(options: Record = {}) { this.options = observable({ ...options }); this.collectionManager = new CollectionManager(this); } get flowEngine() { return this.dataSourceManager.flowEngine; } get displayName() { return this.flowEngine.translate(this.options.displayName, { ns: 'lm-collections' }) || this.key; } get key() { return this.options.key; } get name() { return this.options.key; } get status() { return this.options.status; } get errorMessage() { return this.options.errorMessage; } 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[], options: { clearFields?: boolean } = {}) { return this.collectionManager.upsertCollections(collections, options); } setCollections(collections: CollectionOptions[], options: { clearFields?: boolean } = {}) { return this.collectionManager.setCollections(collections, options); } 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); } patchOptions(newOptions: any = {}) { Object.assign(this.options, newOptions); } reload() { return this.dataSourceManager.reloadDataSource(this.key); } getCollectionField(fieldPath: string) { const [collectionName, ...otherKeys] = fieldPath.split('.'); const fieldName = otherKeys.join('.'); const collection = this.getCollection(collectionName); if (!collection) { return; } 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; allCollectionsInheritChain: string[]; protected childrenCollectionsName: { supportView?: string[]; notSupportView?: string[] } = {}; constructor(public dataSource: DataSource) { this.collections = observable.shallow>(new Map()); } protected resetCaches() { this.childrenCollectionsName = {}; this.allCollectionsInheritChain = undefined; } 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); this.resetCaches(); } removeCollection(name: string) { this.collections.delete(name); this.resetCaches(); } updateCollection(newOptions: CollectionOptions, options: { clearFields?: boolean } = {}) { const collection = this.getCollection(newOptions.name); if (!collection) { throw new Error(`Collection ${newOptions.name} not found`); } collection.setOptions(newOptions, options); this.resetCaches(); } upsertCollection(options: CollectionOptions) { if (this.collections.has(options.name)) { this.updateCollection(options); } else { this.addCollection(options); } return this.getCollection(options.name); } upsertCollections(collections: CollectionOptions[], options: { clearFields?: boolean } = {}) { for (const collection of sortCollectionsByInherits(collections)) { if (this.collections.has(collection.name)) { this.updateCollection(collection, options); } else { this.addCollection(collection); } } this.resetCaches(); } setCollections(collections: CollectionOptions[], options: { clearFields?: boolean } = {}) { this.clearCollections(); this.upsertCollections(collections, options); } sortCollectionsByInherits(collections: CollectionOptions[]): CollectionOptions[] { // 1. 构建 name -> CollectionOptions 映射 const map = new Map(); for (const col of collections) { map.set(col.name, col); } // 2. 构建依赖图和入度表 const graph: Record> = {}; const inDegree: Record = {}; 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(); this.resetCaches(); } 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); } getChildrenCollections(name) { const childrens = []; const collections = Array.from(this.collections.values()); const getChildrens = (name) => { const inheritCollections = collections.filter((v: any) => { return v.options.inherits?.includes(name); }); inheritCollections.forEach((v) => { const collectionKey = v.name; childrens.push(v); return getChildrens(collectionKey); }); return childrens; }; return getChildrens(name); } getChildrenCollectionsName(name, isSupportView = false) { const cacheKey = isSupportView ? 'supportView' : 'notSupportView'; if (this.childrenCollectionsName[cacheKey]) { return this.childrenCollectionsName[cacheKey].slice(); } const children: string[] = []; const collections = [...this.getCollections()]; const getChildrenCollectionsInner = (collectionName: string) => { const inheritCollections = collections.filter((v: any) => { return [...v.inherits]?.includes(collectionName); }); inheritCollections.forEach((v) => { const collectionKey = v.name; children.push(collectionKey); return getChildrenCollectionsInner(collectionKey); }); if (isSupportView) { const sourceCollections = collections.filter((v: any) => { return [...v.sources]?.length === 1 && v?.sources[0] === collectionName; }); sourceCollections.forEach((v) => { const collectionKey = v.name; children.push(v.name); return getChildrenCollectionsInner(collectionKey); }); } return _.uniq(children); }; this.childrenCollectionsName[cacheKey] = getChildrenCollectionsInner(name); return this.childrenCollectionsName[cacheKey]; } getAllCollectionsInheritChain(name) { if (this.allCollectionsInheritChain) { return this.allCollectionsInheritChain.slice(); } const collectionsInheritChain = [name]; const getInheritChain = (name: string) => { const collection = this.getCollection(name); if (collection) { const { inherits } = collection as any; const children = this.getChildrenCollectionsName(name); // 搜寻祖先表 if (inherits) { for (let index = 0; index < inherits.length; index++) { const collectionKey = inherits[index]; if (collectionsInheritChain.includes(collectionKey)) { continue; } collectionsInheritChain.push(collectionKey); getInheritChain(collectionKey); } } // 搜寻后代表 if (children) { for (let index = 0; index < children.length; index++) { const collection = this.getCollection(children[index]); const collectionKey = collection.name; if (collectionsInheritChain.includes(collectionKey)) { continue; } collectionsInheritChain.push(collectionKey); getInheritChain(collectionKey); } } } return collectionsInheritChain; }; this.allCollectionsInheritChain = getInheritChain(name); return this.allCollectionsInheritChain || []; } } // Collection 负责管理自己的 Field export class Collection { fields: Map; options: Record; inherits: Map; dataSource: DataSource; constructor(options: Record = {}) { this.options = observable({ ...options }); this.fields = observable.shallow>(new Map()); this.inherits = observable.shallow>(new Map()); this.setFields(options.fields || []); } clone() { const newCollection = new Collection(_.cloneDeep(this.options)); newCollection.setDataSource(this.dataSource); return newCollection; } 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]; } if (Array.isArray(this.filterTargetKey) && this.filterTargetKey.length === 1) { return record[this.filterTargetKey[0]]; } return _.pick(record, this.filterTargetKey); } get titleableFields() { return this.getFields().filter((field) => field.titleable); } 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.flowEngine.translate(this.options.title, { ns: 'lm-collections' }) || this.name; } get titleCollectionField() { const titleFieldName = this.options.titleField || this.filterTargetKey; if (Array.isArray(titleFieldName)) { if (titleFieldName.length !== 1) { return undefined; } return this.getField(titleFieldName[0]); } 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) { console.warn(`Warning: Collection ${inherit} not found for collection ${this.name}`); continue; } this.inherits.set(inherit, collection); } } setDataSource(dataSource: DataSource) { this.dataSource = dataSource; } setOptions(newOptions: any = {}, options: { clearFields?: boolean } = {}) { Object.keys(this.options).forEach((key) => delete this.options[key]); Object.assign(this.options, newOptions); this.initInherits(); if (options.clearFields) { this.clearFields(); } this.upsertFields(this.options.fields || []); } setOption(key: string, value: any) { this.options[key] = value; } getFields(): CollectionField[] { // 合并自身 fields 和所有 inherits 的 fields,后者优先被覆盖 const fieldMap = new Map(); 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[]) { this.fields.clear(); for (const field of fields) { this.addField(field); } } upsertFields(fields: Record[] = []) { 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) { 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(); 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; collection: Collection; constructor(options: Record) { 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 titleable() { return !!(this.options.titleable ?? this.options.titleUsable); } 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?.uiSchema?.title || this.options?.title; return this.flowEngine.translate(titleValue, { ns: 'lm-collections' }) || this.options.name; } set title(value: string) { this.options.title = value; } get enum(): any[] { const options = this.options.uiSchema?.enum || []; if (this.type === 'integer') { return options.map((v) => { if (typeof v !== 'object') { return v; } if (v.value === null || v.value === undefined) { return v; } return { ...v, label: translateOptionLabel(this.flowEngine, v.label, { ns: 'lm-collections' }), value: Number(v.value), }; }); } return options.map((v) => { return { ...v, label: translateOptionLabel(this.flowEngine, v.label, { ns: 'lm-collections' }), }; }); } 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.interface === 'multipleSelect' ? 'multiple' : undefined, multiple: target ? ['belongsToMany', 'hasMany', 'belongsToArray'].includes(type) : undefined, maxCount: target && !['belongsToMany', 'hasMany', 'belongsToArray'].includes(type) ? 1 : undefined, target: target, template: this.targetCollection?.template, }, _.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) => { const translated = this.flowEngine.translate(d.type, { ...d.context, ns: 'data-source-main', label, }); if (translated && translated !== d.type) { return translated; } return 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 ctx = this.flowEngine.context; return getCollectionFieldInterface( this.interface, this.collection?.dataSource?.dataSourceManager, ctx.dataSourceManager, ctx.app?.dataSourceManager, ); } 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 };