From cd9c2e1e8c2ea84ed32c2733b20b05f35e4e514d Mon Sep 17 00:00:00 2001 From: sealday Date: Sat, 20 Apr 2024 17:41:23 +0800 Subject: [PATCH] feat: init external data source support (#785) Co-authored-by: sealday Reviewed-on: https://git.daoyoucloud.com/daoyoucloud/tachybase/pulls/785 --- .../plugin-external-data-source/.npmignore | 2 + .../plugin-external-data-source/README.md | 1 + .../plugin-external-data-source/client.d.ts | 2 + .../plugin-external-data-source/client.js | 1 + .../plugin-external-data-source/package.json | 20 ++ .../plugin-external-data-source/server.d.ts | 2 + .../plugin-external-data-source/server.js | 1 + .../src/client/index.tsx | 110 ++++++++ .../src/client/locale.ts | 14 + .../plugin-external-data-source/src/index.ts | 2 + .../src/locale/en-US.json | 26 ++ .../src/locale/zh-CN.json | 26 ++ .../src/server/collections/.gitkeep | 0 .../src/server/index.ts | 1 + .../src/server/plugin.ts | 28 ++ .../src/server/postgres-collection-manager.ts | 2 + .../src/server/postgres-data-source.ts | 94 +++++++ .../server/services/database-introspector.ts | 252 ++++++++++++++++++ .../src/server/services/type-interface-map.ts | 221 +++++++++++++++ .../src/server/services/utils.ts | 22 ++ pnpm-lock.yaml | 65 +++-- 21 files changed, 875 insertions(+), 17 deletions(-) create mode 100644 packages/plugins/@hera/plugin-external-data-source/.npmignore create mode 100644 packages/plugins/@hera/plugin-external-data-source/README.md create mode 100644 packages/plugins/@hera/plugin-external-data-source/client.d.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/client.js create mode 100644 packages/plugins/@hera/plugin-external-data-source/package.json create mode 100644 packages/plugins/@hera/plugin-external-data-source/server.d.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/server.js create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/client/index.tsx create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/client/locale.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/index.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/locale/en-US.json create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/locale/zh-CN.json create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/server/collections/.gitkeep create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/server/index.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/server/plugin.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/server/postgres-collection-manager.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/server/postgres-data-source.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/server/services/database-introspector.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/server/services/type-interface-map.ts create mode 100644 packages/plugins/@hera/plugin-external-data-source/src/server/services/utils.ts diff --git a/packages/plugins/@hera/plugin-external-data-source/.npmignore b/packages/plugins/@hera/plugin-external-data-source/.npmignore new file mode 100644 index 000000000..65f5e8779 --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/.npmignore @@ -0,0 +1,2 @@ +/node_modules +/src diff --git a/packages/plugins/@hera/plugin-external-data-source/README.md b/packages/plugins/@hera/plugin-external-data-source/README.md new file mode 100644 index 000000000..488f8e9b1 --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/README.md @@ -0,0 +1 @@ +# @hera/plugin-external-data-source diff --git a/packages/plugins/@hera/plugin-external-data-source/client.d.ts b/packages/plugins/@hera/plugin-external-data-source/client.d.ts new file mode 100644 index 000000000..6c459cbac --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/client.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client'; +export { default } from './dist/client'; diff --git a/packages/plugins/@hera/plugin-external-data-source/client.js b/packages/plugins/@hera/plugin-external-data-source/client.js new file mode 100644 index 000000000..b6e3be70e --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/client.js @@ -0,0 +1 @@ +module.exports = require('./dist/client/index.js'); diff --git a/packages/plugins/@hera/plugin-external-data-source/package.json b/packages/plugins/@hera/plugin-external-data-source/package.json new file mode 100644 index 000000000..5967c2690 --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/package.json @@ -0,0 +1,20 @@ +{ + "name": "@hera/plugin-external-data-source", + "version": "0.0.1", + "main": "dist/server/index.js", + "dependencies": { + "antd": "5.16.1", + "lodash": "^4.17.21", + "pg": "^8.11.3", + "react-i18next": "^11.15.1" + }, + "devDependencies": { + "@types/lodash": "^4.17.0" + }, + "peerDependencies": { + "@nocobase/client": "workspace:*", + "@nocobase/plugin-data-source-manager": "workspace:*", + "@nocobase/server": "workspace:*", + "@nocobase/test": "workspace:*" + } +} diff --git a/packages/plugins/@hera/plugin-external-data-source/server.d.ts b/packages/plugins/@hera/plugin-external-data-source/server.d.ts new file mode 100644 index 000000000..c41081ddc --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/server.d.ts @@ -0,0 +1,2 @@ +export * from './dist/server'; +export { default } from './dist/server'; diff --git a/packages/plugins/@hera/plugin-external-data-source/server.js b/packages/plugins/@hera/plugin-external-data-source/server.js new file mode 100644 index 000000000..972842039 --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/server.js @@ -0,0 +1 @@ +module.exports = require('./dist/server/index.js'); diff --git a/packages/plugins/@hera/plugin-external-data-source/src/client/index.tsx b/packages/plugins/@hera/plugin-external-data-source/src/client/index.tsx new file mode 100644 index 000000000..fee0eb60a --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/client/index.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Plugin, SchemaComponent } from '@nocobase/client'; +import {} from '@nocobase/client'; +import { PluginDataSourceManagerClient } from '@nocobase/plugin-data-source-manager/client'; +import { Space } from 'antd'; +import { generateNTemplate as tval, usePluginTranslation } from './locale'; + +const DataSourceSettingsForm = () => { + const { t } = usePluginTranslation(); + return ( + + ); +}; + +export class PluginExternalDataSourceClient extends Plugin { + async load() { + this.app.pm + .get(PluginDataSourceManagerClient) + .registerType('postgres', { DataSourceSettingsForm, label: tval('PostgreSQL') }); + } +} + +export default PluginExternalDataSourceClient; diff --git a/packages/plugins/@hera/plugin-external-data-source/src/client/locale.ts b/packages/plugins/@hera/plugin-external-data-source/src/client/locale.ts new file mode 100644 index 000000000..9244f8cf3 --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/client/locale.ts @@ -0,0 +1,14 @@ +import { i18n } from '@nocobase/client'; +import { useTranslation } from 'react-i18next'; + +export const NAMESPACE = '@hera/plugin-external-data-source'; + +export function lang(key: string, options = {}) { + return i18n.t(key, { ...options, ns: NAMESPACE }); +} +export function generateNTemplate(key: string) { + return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`; +} +export function usePluginTranslation() { + return useTranslation(NAMESPACE); +} diff --git a/packages/plugins/@hera/plugin-external-data-source/src/index.ts b/packages/plugins/@hera/plugin-external-data-source/src/index.ts new file mode 100644 index 000000000..7e74612df --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/index.ts @@ -0,0 +1,2 @@ +export * from './server'; +export { default } from './server'; diff --git a/packages/plugins/@hera/plugin-external-data-source/src/locale/en-US.json b/packages/plugins/@hera/plugin-external-data-source/src/locale/en-US.json new file mode 100644 index 000000000..fd64840be --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/locale/en-US.json @@ -0,0 +1,26 @@ +{ + "Data source name": "Data source name", + "Data source display name": "Data source display name", + "Host": "Host", + "Port": "Port", + "Database": "Database", + "Database connections": "Database connections", + "Test Connection": "Test Connection", + "Connection successful'": "Connection successful'", + "Display name": "Display name", + "Username": "Username", + "Password": "Password", + "Database dialaect": "Database dialaect", + "Description": "Description", + "Storage": "Storage", + "Collections": "Collections", + "Permissions": "Permissions", + "Allow adding and modifying collection": "Allow adding and modifying collection", + "Unknown field type": "Unknown field type", + "The following field types are not compatible and do not support output and display": "The following field types are not compatible and do not support output and display", + "Field database type": "Field database type", + "Postgres": "Postgres", + "Enabled the data source": "Enabled the data source", + "Table prefix": "Table prefix", + "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter." +} \ No newline at end of file diff --git a/packages/plugins/@hera/plugin-external-data-source/src/locale/zh-CN.json b/packages/plugins/@hera/plugin-external-data-source/src/locale/zh-CN.json new file mode 100644 index 000000000..0e531da37 --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/locale/zh-CN.json @@ -0,0 +1,26 @@ +{ + "Data source name": "数据源标识", + "Data source display name": "数据源名称", + "Host": "服务器地址", + "Port": "端口", + "Database": "数据库", + "Database connections": "连接第三方数据库", + "Test Connection": "测试连接", + "Connection successful'": "连接成功", + "Display name": "名称", + "Username": "用户名", + "Password": "密码", + "Database dialaect": "数据库", + "Description": "描述", + "Storage": "文件地址", + "Collections": "数据表", + "Permissions": "权限", + "Allow adding and modifying collection": "允许添加和修改数据表结构", + "Unknown field type": "未知字段类型", + "The following field types are not compatible and do not support output and display": "以下字段类型未适配,不支持输出和显示", + "Field database type": "字段数据库类型", + "Postgres":"PostgreSQL", + "Enabled the data source":"启用数据源", + "Table prefix":"表前缀", + "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。" +} diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/collections/.gitkeep b/packages/plugins/@hera/plugin-external-data-source/src/server/collections/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/index.ts b/packages/plugins/@hera/plugin-external-data-source/src/server/index.ts new file mode 100644 index 000000000..b68aea57f --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/server/index.ts @@ -0,0 +1 @@ +export { default } from './plugin'; diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/plugin.ts b/packages/plugins/@hera/plugin-external-data-source/src/server/plugin.ts new file mode 100644 index 000000000..415bafe47 --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/server/plugin.ts @@ -0,0 +1,28 @@ +import { Plugin } from '@nocobase/server'; +import { PostgresDataSource } from './postgres-data-source'; + +export class PluginExternalDataSourceServer extends Plugin { + async afterAdd() {} + + async beforeLoad() { + this.app.dataSourceManager.factory.register('postgres', PostgresDataSource); + } + async beforeEnable() { + const plugin = this.pm.get('data-source-manager'); + if (!plugin.enabled) { + throw new Error(`${this.name} plugin need data-source-manager plugin enabled`); + } + } + + async load() {} + + async install() {} + + async afterEnable() {} + + async afterDisable() {} + + async remove() {} +} + +export default PluginExternalDataSourceServer; diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/postgres-collection-manager.ts b/packages/plugins/@hera/plugin-external-data-source/src/server/postgres-collection-manager.ts new file mode 100644 index 000000000..9b9dd600b --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/server/postgres-collection-manager.ts @@ -0,0 +1,2 @@ +import { SequelizeCollectionManager } from '@nocobase/data-source-manager'; +export class PostgresCollectionManager extends SequelizeCollectionManager {} diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/postgres-data-source.ts b/packages/plugins/@hera/plugin-external-data-source/src/server/postgres-data-source.ts new file mode 100644 index 000000000..167be8d30 --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/server/postgres-data-source.ts @@ -0,0 +1,94 @@ +import { DataSource } from '@nocobase/data-source-manager'; +import { Database } from '@nocobase/database'; +import { PostgresCollectionManager } from './postgres-collection-manager'; +import { DatabaseIntrospector } from './services/database-introspector'; +import typeInterfaceMap from './services/type-interface-map'; + +export class PostgresDataSource extends DataSource { + static async testConnection(options) { + const database = new Database({ + dialect: 'postgres', + ...options, + }); + try { + await database.sequelize.authenticate(); + if (options.schema) { + const queryRes = await database.sequelize.query( + ` +SELECT schema_name +FROM information_schema.schemata +WHERE schema_name = $1; + `, + { + bind: [options.schema], + type: 'SELECT', + }, + ); + // @ts-ignore + if (queryRes.length === 0) { + throw new Error(`schema ${options.schema} not exists`); + } + } + } catch (e) { + throw e; + } finally { + await database.close(); + } + return true; + } + async load(options) { + // @ts-ignore + const db = this.collectionManager.db; + const introspector = new DatabaseIntrospector({ + db, + typeInterfaceMap, + }); + introspector.on('loadMessage', ({ message }) => { + this.emit('loadMessage', { message }); + }); + const { localData } = options; + const collections = await introspector.getCollections({ + localData, + }); + const delayFields = /* @__PURE__ */ new Map(); + for (const collection of collections) { + try { + const fields = collection.fields; + for (const field of fields) { + if (field.type === 'belongsToMany') { + if (!delayFields.has(collection.name)) { + delayFields.set(collection.name, []); + } + delayFields.get(collection.name).push(field); + } + } + this.collectionManager.defineCollection({ + ...collection, + fields: fields.filter((field) => field.type !== 'belongsToMany'), + introspected: true, + }); + } catch (e) { + db.logger.error(`load collection failed, ${e}`); + delayFields.delete(collection.name); + } + } + for (const [name, fields] of delayFields.entries()) { + try { + for (const field of fields) { + this.collectionManager.getCollection(name).setField(field.name, field); + } + } catch (e) { + db.logger.error(`load belongs to many field failed, ${e}`); + } + } + } + createCollectionManager(options) { + const database = new Database({ + dialect: 'postgres', + ...options, + }); + return new PostgresCollectionManager({ + database, + }); + } +} diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/services/database-introspector.ts b/packages/plugins/@hera/plugin-external-data-source/src/server/services/database-introspector.ts new file mode 100644 index 000000000..0fc6587e4 --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/server/services/database-introspector.ts @@ -0,0 +1,252 @@ +import { ViewFieldInference } from '@nocobase/database'; +import { mergeOptions } from '@nocobase/plugin-data-source-manager'; +import { EventEmitter } from 'events'; +import _ from 'lodash'; +import { showUniqueIndexes } from './utils'; +export class DatabaseIntrospector extends EventEmitter { + db; + typeInterfaceMap; + constructor(options) { + super(); + this.db = options.db; + this.typeInterfaceMap = options.typeInterfaceMap; + } + async getCollections(options = {}) { + let tableList = await this.db.sequelize.getQueryInterface().showAllTables(); + const views = (await this.db.queryInterface.listViews()).map((view) => view.name); + tableList = tableList.concat(views); + if (this.db.options.tablePrefix) { + tableList = tableList.filter((tableName) => { + return tableName.startsWith(this.db.options.tablePrefix); + }); + } + const batchSize = 5; + const results = []; + for (let i = 0; i < tableList.length; i += batchSize) { + const batch = tableList.slice(i, i + batchSize); + const batchResults = await Promise.all( + batch.map(async (tableName) => { + const tableInfo = { + tableName, + }; + this.emit('loadMessage', { + message: `load table ${tableName}`, + }); + const collectionOptions = this.tableInfoToCollectionOptions(tableInfo); + // @ts-ignore + const localOptions = options.localData?.[collectionOptions.name]; + try { + return await this.getCollection({ + tableInfo, + localOptions, + mergedOptions: views.includes(tableName) ? { view: true, template: 'view' } : {}, + }); + } catch (e) { + if (e.message.includes('No description found for')) { + return false; + } + this.db.logger.error(`load table ${tableName} failed, ${e.message}`); + return false; + } + }), + ); + results.push(...batchResults); + } + return results.filter(Boolean); + } + async getPrimaryKeysOfTable(tableInfo) { + const sql = ` + SELECT + pg_attribute.attname, + format_type(pg_attribute.atttypid, pg_attribute.atttypmod) + FROM pg_index, pg_class, pg_attribute, pg_namespace + WHERE + pg_class.oid = '${tableInfo.tableName}'::regclass AND + indrelid = pg_class.oid AND + nspname = '${tableInfo.schema || 'public'}' AND + pg_class.relnamespace = pg_namespace.oid AND + pg_attribute.attrelid = pg_class.oid AND + pg_attribute.attnum = any(pg_index.indkey) + AND indisprimary + `; + return await this.db.sequelize.query(sql, { + type: 'SELECT', + }); + } + async getCollection(options) { + const { tableInfo } = options; + const columnsInfo = await this.db.sequelize.getQueryInterface().describeTable(tableInfo); + const primaryKeys = await this.getPrimaryKeysOfTable(tableInfo); + Object.keys(columnsInfo).forEach((columnName) => { + const columnInfo = columnsInfo[columnName]; + const primaryKey = primaryKeys.find((key) => key.attname === columnName); + if (primaryKey) { + columnInfo.primaryKey = true; + } + }); + const collectionOptions = this.tableInfoToCollectionOptions(tableInfo); + const constraints = await this.db.sequelize.query(showUniqueIndexes(tableInfo), { + type: 'SELECT', + }); + try { + const fields = Object.keys(columnsInfo).map((columnName) => { + return this.columnInfoToFieldOptions(columnsInfo, columnName, constraints); + }); + const unsupportedFields = fields.filter((field) => { + // @ts-ignore + return field.supported === false; + }); + const supportFields = fields.filter((field) => { + // @ts-ignore + return field.supported !== false; + }); + const remoteCollectionInfo = { + ...collectionOptions, + ...((options == null ? void 0 : options.mergedOptions) || {}), + ...this.collectionOptionsByFields(supportFields), + fields: [...supportFields], + }; + if (unsupportedFields.length) { + remoteCollectionInfo.unsupportedFields = unsupportedFields; + } + const finalOptions = this.mergeLocalDataIntoCollectionOptions(remoteCollectionInfo, options.localOptions); + if (finalOptions.view && !finalOptions.filterTargetKey && supportFields.find((field) => field.name === 'id')) { + finalOptions.filterTargetKey = 'id'; + } + return finalOptions; + } catch (e) { + throw new Error(`table ${tableInfo.tableName} introspection error: ${e.message}`); + } + } + loadCollection(options) { + this.db.collection({ + ...options, + introspected: true, + }); + } + loadCollections(options) { + options.collections.forEach((collection) => { + this.loadCollection(collection); + }); + } + tableInfoToCollectionOptions(tableInfo) { + const tableName = tableInfo.tableName; + let name = tableName; + if (this.db.options.tablePrefix) { + name = tableName.replace(this.db.options.tablePrefix, ''); + } + return { + name, + title: name, + schema: tableInfo.schema, + tableName, + }; + } + collectionOptionsByFields(fields) { + const options = { + timestamps: false, + autoGenId: false, + filterTargetKey: undefined, + }; + const autoIncrementField = fields.find((field) => field.autoIncrement); + if (autoIncrementField) { + options.filterTargetKey = autoIncrementField.name; + } + const primaryKeys = fields.filter((field) => field.primaryKey); + if (!options.filterTargetKey && primaryKeys.length === 1) { + options.filterTargetKey = primaryKeys[0].name; + } + const uniques = fields.filter((field) => field.unique); + if (!options.filterTargetKey && uniques.length === 1) { + options.filterTargetKey = uniques[0].name; + } + return options; + } + mergeLocalDataIntoCollectionOptions(collectionOptions, localData) { + if (!localData) { + return collectionOptions; + } + const collectionFields = collectionOptions.fields || []; + const localFieldsAsObject = _.keyBy(localData.fields, 'name'); + const newFields = collectionFields.map((field) => { + const localField = localFieldsAsObject[field.name]; + if (!localField) { + return field; + } + return mergeOptions(field, localField); + }); + const localAssociationFields = localData.fields?.filter((field) => { + return ['belongsTo', 'belongsToMany', 'hasMany', 'hasOne'].includes(field.type); + }); + if (localAssociationFields) { + newFields.push(...localAssociationFields); + } + return { + ...collectionOptions, + ..._.omit(localData, ['fields']), + fields: newFields, + }; + } + columnInfoToFieldOptions(columnsInfo, columnName, indexes) { + const columnInfo = columnsInfo[columnName]; + let fieldOptions = { + ...this.columnAttribute(columnsInfo, columnName, indexes), + ...ViewFieldInference.inferToFieldType({ + dialect: this.db.options.dialect, + type: columnInfo.type, + name: columnName, + }), + rawType: columnInfo.type, + name: columnName, + }; + const interfaceConfig = this.getDefaultInterfaceByType(columnsInfo, columnName, fieldOptions.type); + if (typeof interfaceConfig === 'string') { + // @ts-ignore + fieldOptions.interface = interfaceConfig; + } else { + fieldOptions = { + ...fieldOptions, + ...interfaceConfig, + }; + } + if (!fieldOptions.type) { + return { + rawType: columnInfo.type, + name: columnName, + supported: false, + }; + } + _.set(fieldOptions, 'uiSchema.title', columnName); + return fieldOptions; + } + getDefaultInterfaceByType(columnsInfo, columnName, type) { + const interfaceConfig = this.typeInterfaceMap[type]; + if (typeof interfaceConfig === 'function') { + return interfaceConfig(columnsInfo[columnName]); + } + return interfaceConfig; + } + columnAttribute(columnsInfo, columnName, indexes) { + const columnInfo = columnsInfo[columnName]; + const attr = { + type: columnInfo.type, + allowNull: columnInfo.allowNull, + primaryKey: columnInfo.primaryKey, + unique: false, + autoIncrement: false, + }; + if (columnInfo.defaultValue && typeof columnInfo.defaultValue === 'string') { + const isSerial = columnInfo.defaultValue.match(/^nextval\(/); + const isUUID = columnInfo.defaultValue.match(/^uuid_generate_v4\(/); + if (isSerial || isUUID) { + attr.autoIncrement = true; + } + } + for (const index of indexes) { + if (index['is_single_column'] && index['column_name'] === columnName) { + attr.unique = true; + } + } + return attr; + } +} diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/services/type-interface-map.ts b/packages/plugins/@hera/plugin-external-data-source/src/server/services/type-interface-map.ts new file mode 100644 index 000000000..bdbbc033e --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/server/services/type-interface-map.ts @@ -0,0 +1,221 @@ +const typeInterfaceMap = { + array: '', + belongsTo: '', + belongsToMany: '', + boolean: () => { + return { + interface: 'checkbox', + uiSchema: { + type: 'boolean', + 'x-component': 'Checkbox', + }, + }; + }, + context: '', + date: () => { + return { + interface: 'datetime', + uiSchema: { + 'x-component': 'DatePicker', + 'x-component-props': { + dateFormat: 'YYYY-MM-DD', + showTime: false, + }, + }, + }; + }, + hasMany: '', + hasOne: '', + json: () => { + return { + interface: 'json', + uiSchema: { + 'x-component': 'Input.JSON', + 'x-component-props': { + autoSize: { + minRows: 5, + // maxRows: 20, + }, + }, + default: null, + }, + }; + }, + jsonb: () => { + return { + interface: 'json', + uiSchema: { + 'x-component': 'Input.JSON', + 'x-component-props': { + autoSize: { + minRows: 5, + // maxRows: 20, + }, + }, + default: null, + }, + }; + }, + integer: () => ({ + interface: 'integer', + // name, + uiSchema: { + type: 'number', + // title, + 'x-component': 'InputNumber', + 'x-component-props': { + stringMode: true, + step: '1', + }, + 'x-validator': 'integer', + }, + }), + bigInt: (columnInfo) => { + return { + interface: 'integer', + uiSchema: { + 'x-component': 'InputNumber', + 'x-component-props': { + style: { + width: '100%', + }, + }, + }, + }; + }, + float: () => { + return { + interface: 'number', + uiSchema: { + type: 'number', + // title, + 'x-component': 'InputNumber', + 'x-component-props': { + stringMode: true, + step: '1', + }, + }, + }; + }, + double: () => { + return { + interface: 'number', + uiSchema: { + type: 'number', + // title, + 'x-component': 'InputNumber', + 'x-component-props': { + stringMode: true, + step: '1', + }, + }, + }; + }, + real: () => { + return { + interface: 'number', + uiSchema: { + type: 'number', + // title, + 'x-component': 'InputNumber', + 'x-component-props': { + stringMode: true, + step: '1', + }, + }, + }; + }, + decimal: () => { + return { + interface: 'number', + uiSchema: { + type: 'number', + // title, + 'x-component': 'InputNumber', + 'x-component-props': { + stringMode: true, + step: '1', + }, + }, + }; + }, + password: () => ({ + interface: 'password', + hidden: true, + // name, + uiSchema: { + type: 'string', + // title, + 'x-component': 'Password', + }, + }), + radio: '', + set: '', + sort: '', + string: () => { + return { + interface: 'input', + uiSchema: { + 'x-component': 'Input', + 'x-component-props': { + style: { + width: '100%', + }, + }, + }, + }; + }, + text: () => { + return { + interface: 'textarea', + // name, + uiSchema: { + type: 'string', + 'x-component': 'Input.TextArea', + }, + }; + }, + time: () => ({ + interface: 'time', + // name, + uiSchema: { + type: 'string', + 'x-component': 'TimePicker', + 'x-component-props': { + format: 'HH:mm:ss', + }, + }, + }), + uid: () => { + return { + interface: 'input', + uiSchema: { + 'x-component': 'Input', + 'x-component-props': { + style: { + width: '100%', + }, + }, + }, + }; + }, + uuid: () => { + return { + interface: 'uuid', + uiSchema: { + 'x-component': 'Input', + 'x-component-props': { + style: { + width: '100%', + }, + }, + }, + }; + }, + virtual: '', + point: '', + polygon: '', + lineString: '', + circle: '', +}; +export default typeInterfaceMap; diff --git a/packages/plugins/@hera/plugin-external-data-source/src/server/services/utils.ts b/packages/plugins/@hera/plugin-external-data-source/src/server/services/utils.ts new file mode 100644 index 000000000..81a9eb9eb --- /dev/null +++ b/packages/plugins/@hera/plugin-external-data-source/src/server/services/utils.ts @@ -0,0 +1,22 @@ +export function showUniqueIndexes(tableInfo2) { + return ` + SELECT + tc.constraint_schema, + tc.constraint_name, + tc.table_name, + kcu.column_name, + CASE WHEN (SELECT COUNT(*) + FROM information_schema.key_column_usage kcu2 + WHERE kcu2.constraint_name = tc.constraint_name + AND kcu2.table_schema = tc.constraint_schema) = 1 THEN TRUE + ELSE FALSE END AS is_single_column +FROM information_schema.table_constraints AS tc +JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema +WHERE tc.constraint_type = 'UNIQUE' + AND tc.table_name = '${tableInfo2.tableName}' + AND tc.table_schema = '${tableInfo2.schema || 'public'}' +ORDER BY tc.constraint_name, kcu.column_name; + `; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8df07b269..cb91eaaea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1541,6 +1541,37 @@ importers: specifier: ^8.13.0 version: 8.15.1 + packages/plugins/@hera/plugin-external-data-source: + dependencies: + '@nocobase/client': + specifier: workspace:* + version: link:../../../core/client + '@nocobase/plugin-data-source-manager': + specifier: workspace:* + version: link:../../@nocobase/plugin-data-source-manager + '@nocobase/server': + specifier: workspace:* + version: link:../../../core/server + '@nocobase/test': + specifier: workspace:* + version: link:../../../core/test + antd: + specifier: 5.16.1 + version: 5.16.1(react-dom@18.2.0)(react@18.2.0) + lodash: + specifier: ^4.17.21 + version: 4.17.21 + pg: + specifier: ^8.11.3 + version: 8.11.3 + react-i18next: + specifier: ^11.15.1 + version: 11.18.6(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0) + devDependencies: + '@types/lodash': + specifier: ^4.17.0 + version: 4.17.0 + packages/plugins/@hera/plugin-mobile: dependencies: '@nocobase/client': @@ -4695,7 +4726,7 @@ packages: '@emotion/unitless': 0.7.5 classnames: 2.5.1 csstype: 3.1.3 - rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) + rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) stylis: 4.3.1 @@ -4768,7 +4799,7 @@ packages: react-dom: '>=17.0.0' dependencies: '@ant-design/cssinjs': 1.19.0(react-dom@18.2.0)(react@18.2.0) - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.4 '@ctrl/tinycolor': 3.6.1 antd: 5.16.1(react-dom@18.2.0)(react@18.2.0) rc-util: 5.39.1(react-dom@18.2.0)(react@18.2.0) @@ -4785,7 +4816,7 @@ packages: dependencies: '@ant-design/icons': 5.3.6(react-dom@18.2.0)(react@18.2.0) '@ant-design/pro-provider': 2.13.5(antd@5.16.1)(react-dom@18.2.0)(react@18.2.0) - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.4 antd: 5.16.1(react-dom@18.2.0)(react@18.2.0) classnames: 2.5.1 dayjs: 1.11.10 @@ -7275,7 +7306,7 @@ packages: /@changesets/apply-release-plan@7.0.0: resolution: {integrity: sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@changesets/config': 3.0.0 '@changesets/get-version-range-type': 0.4.0 '@changesets/git': 3.0.0 @@ -7293,7 +7324,7 @@ packages: /@changesets/assemble-release-plan@6.0.0: resolution: {integrity: sha512-4QG7NuisAjisbW4hkLCmGW2lRYdPrKzro+fCtZaILX+3zdUELSvYjpL4GTv0E4aM9Mef3PuIQp89VmHJ4y2bfw==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.0.0 '@changesets/types': 6.0.0 @@ -7376,7 +7407,7 @@ packages: /@changesets/get-release-plan@4.0.0: resolution: {integrity: sha512-9L9xCUeD/Tb6L/oKmpm8nyzsOzhdNBBbt/ZNcjynbHC07WW4E1eX8NMGC5g5SbM5z/V+MOrYsJ4lRW41GCbg3w==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@changesets/assemble-release-plan': 6.0.0 '@changesets/config': 3.0.0 '@changesets/pre': 2.0.0 @@ -7392,7 +7423,7 @@ packages: /@changesets/git@3.0.0: resolution: {integrity: sha512-vvhnZDHe2eiBNRFHEgMiGd2CT+164dfYyrJDhwwxTVD/OW0FUD6G7+4DIx1dNwkwjHyzisxGAU96q0sVNBns0w==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@changesets/errors': 0.2.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 @@ -7417,7 +7448,7 @@ packages: /@changesets/pre@2.0.0: resolution: {integrity: sha512-HLTNYX/A4jZxc+Sq8D1AMBsv+1qD6rmmJtjsCJa/9MSRybdxh0mjbTvE6JYZQ/ZiQ0mMlDOlGPXTm9KLTU3jyw==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@changesets/errors': 0.2.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 @@ -7427,7 +7458,7 @@ packages: /@changesets/read@0.6.0: resolution: {integrity: sha512-ZypqX8+/im1Fm98K4YcZtmLKgjs1kDQ5zHpc2U1qdtNBmZZfo/IBiG162RoP0CUF05tvp2y4IspH11PLnPxuuw==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@changesets/git': 3.0.0 '@changesets/logger': 0.1.0 '@changesets/parse': 0.4.0 @@ -7448,7 +7479,7 @@ packages: /@changesets/write@0.3.0: resolution: {integrity: sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@changesets/types': 6.0.0 fs-extra: 7.0.1 human-id: 1.0.2 @@ -7899,7 +7930,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.4 '@emotion/babel-plugin': 11.11.0 '@emotion/cache': 11.11.0 '@emotion/serialize': 1.1.2 @@ -9448,7 +9479,7 @@ packages: /@manypkg/get-packages@1.1.3: resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -13164,7 +13195,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.24.1(@babel/core@7.24.4) '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.4) react-refresh: 0.14.0 - vite: 4.5.2(@types/node@20.12.7)(less@4.1.3) + vite: 4.5.2(@types/node@20.12.2)(less@4.1.3) transitivePeerDependencies: - supports-color @@ -25270,7 +25301,7 @@ packages: react: '*' react-dom: '*' dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@rc-component/trigger': 1.18.3(react-dom@18.2.0)(react@18.2.0) classnames: 2.5.1 rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0) @@ -25880,7 +25911,7 @@ packages: react-native: optional: true dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 '@types/react-redux': 7.1.33 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -26190,7 +26221,7 @@ packages: /redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.4 /reflect-metadata@0.1.14: resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} @@ -29754,7 +29785,6 @@ packages: rollup: 3.29.4 optionalDependencies: fsevents: 2.3.3 - dev: false /vite@4.5.2(@types/node@20.12.7)(less@4.1.3): resolution: {integrity: sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==} @@ -29791,6 +29821,7 @@ packages: rollup: 3.29.4 optionalDependencies: fsevents: 2.3.3 + dev: true /vite@5.1.5(@types/node@20.12.7): resolution: {integrity: sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==}