feat: Add functionality to create and manage internal functions with MySQL and PostgreSQL queries

This commit is contained in:
CaptainB 2025-03-19 12:00:57 +08:00 committed by 刘瑞斌
parent 5ec94860b2
commit 4b4b84a220
6 changed files with 154 additions and 63 deletions

View File

@ -66,6 +66,48 @@ def langsearch(query, apikey):
else:
raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}")
return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/src/assets/fx/langsearch/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "apikey", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "apikey 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "apikey长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL);
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-17 08:16:32.626245 +00:00', '2025-03-17 08:16:32.626308 +00:00', '22c21b76-0308-11f0-9694-5618c4394482', 'MySQL 查询', '', e'
def query_mysql(host,port, user, password, database, sql):
import pymysql
import json
from pymysql.cursors import DictCursor
try:
# 创建连接
db = pymysql.connect(
host=host,
port=int(port),
user=user,
password=password,
database=database,
cursorclass=DictCursor # 使用字典游标
)
# 使用 cursor() 方法创建一个游标对象 cursor
cursor = db.cursor()
# 使用 execute() 方法执行 SQL 查询
cursor.execute(sql)
# 使用 fetchall() 方法获取所有数据
data = cursor.fetchall()
# 处理 bytes 类型的数据
for row in data:
for key, value in row.items():
if isinstance(value, bytes):
row[key] = value.decode("utf-8") # 转换为字符串
# 将数据序列化为 JSON
json_data = json.dumps(data, ensure_ascii=False)
print(json_data)
return json_data
# 关闭数据库连接
db.close()
except Exception as e:
print(f"Error while connecting to MySQL: {e}")', '{"{\"name\": \"sql\", \"type\": \"string\", \"source\": \"reference\", \"is_required\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', true, 'PUBLIC', 'INTERNAL', '/src/assets/fx/mysql/icon.png', '[{"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "host", "label": "host", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "host 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "host长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 20, "minlength": 1, "show-word-limit": true}, "field": "port", "label": "port", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "port 为必填属性", "required": true}, {"max": 20, "min": 1, "message": "port长度在 1 到 20 个字符", "trigger": "blur"}]}, "default_value": "3306", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "user", "label": "user", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "user 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "user长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "root", "show_default_value": false}, {"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "password", "label": "password", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "password 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "password长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "database", "label": "database", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "database 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "database长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', null, null);
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-17 07:37:54.620836 +00:00', '2025-03-17 07:37:54.620887 +00:00', 'bd1e8b88-0302-11f0-87bb-5618c4394482', 'PostgreSQL 查询', '', e'def queryPgSQL(dbname, user, password, host, port, query):
import psycopg2
import json
@ -114,49 +156,7 @@ INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, inpu
cursor.close()
if conn:
conn.close()
', '{"{\"name\": \"dbname\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"user\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"password\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"host\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"port\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"query\", \"type\": \"string\", \"source\": \"reference\", \"is_required\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', true, 'PUBLIC', 'INTERNAL', '/src/assets/fx/postgresql/icon.png', '[]', null, null);
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-17 08:16:32.626245 +00:00', '2025-03-17 08:16:32.626308 +00:00', '22c21b76-0308-11f0-9694-5618c4394482', 'MySQL 查询', '', e'
def query_mysql(host,port, user, password, database, sql):
import pymysql
import json
from pymysql.cursors import DictCursor
try:
# 创建连接
db = pymysql.connect(
host=host,
port=int(port),
user=user,
password=password,
database=database,
cursorclass=DictCursor # 使用字典游标
)
# 使用 cursor() 方法创建一个游标对象 cursor
cursor = db.cursor()
# 使用 execute() 方法执行 SQL 查询
cursor.execute(sql)
# 使用 fetchall() 方法获取所有数据
data = cursor.fetchall()
# 处理 bytes 类型的数据
for row in data:
for key, value in row.items():
if isinstance(value, bytes):
row[key] = value.decode(\"utf-8\") # 转换为字符串
# 将数据序列化为 JSON
json_data = json.dumps(data, ensure_ascii=False)
print(json_data)
return json_data
# 关闭数据库连接
db.close()
except Exception as e:
print(f"Error while connecting to MySQL: {e}")', '{"{\"name\": \"host\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"port\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"user\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"password\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"database\", \"type\": \"string\", \"source\": \"custom\", \"is_required\": true}","{\"name\": \"sql\", \"type\": \"string\", \"source\": \"reference\", \"is_required\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', true, 'PUBLIC', 'INTERNAL', '/src/assets/fx/mysql/icon.png', '[]', null, null);
', '{"{\"name\": \"query\", \"type\": \"string\", \"source\": \"reference\", \"is_required\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', true, 'PUBLIC', 'INTERNAL', '/src/assets/fx/postgresql/icon.png', '[{"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "dbname", "label": "dbname", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "dbname 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "dbname长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "user", "label": "user", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "user 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "user长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "root", "show_default_value": false}, {"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "password", "label": "password", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "password 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "password长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "host", "label": "host", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "host 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "host长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 20, "minlength": 1, "show-word-limit": true}, "field": "port", "label": "port", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "port 为必填属性", "required": true}, {"max": 20, "min": 1, "message": "port长度在 1 到 20 个字符", "trigger": "blur"}]}, "default_value": "5432", "show_default_value": false}]', null, null);
'''

View File

@ -158,10 +158,6 @@ class FunctionLibSerializer(serializers.Serializer):
query_set = query_set.filter(function_type=self.data.get('function_type'))
query_set = query_set.order_by("-create_time")
subquery = FunctionLib.objects.filter(template_id=OuterRef('id'))
subquery = subquery.filter(user_id=self.data.get('user_id'))
query_set = query_set.annotate(added=Exists(subquery))
return query_set
def list(self, with_valid=True):
@ -180,7 +176,6 @@ class FunctionLibSerializer(serializers.Serializer):
def post_records_handler(row):
return {
**FunctionLibModelSerializer(row).data,
'added': row.added,
'init_params': None
}
@ -390,22 +385,19 @@ class FunctionLibSerializer(serializers.Serializer):
class InternalFunction(serializers.Serializer):
id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("function ID")))
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
name = serializers.CharField(required=True, error_messages=ErrMessage.char(_("function name")))
def add(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
if QuerySet(FunctionLib).filter(template_id=self.data.get('id')).filter(
user_id=self.data.get('user_id')).exists():
raise AppApiException(500, _('Function already exists'))
internal_function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
if internal_function_lib is None:
raise AppApiException(500, _('Function does not exist'))
function_lib = FunctionLib(
id=uuid.uuid1(),
name=internal_function_lib.name,
name=self.data.get('name'),
desc=internal_function_lib.desc,
code=internal_function_lib.code,
user_id=self.data.get('user_id'),

View File

@ -166,10 +166,11 @@ class FunctionLibView(APIView):
class AddInternalFun(APIView):
authentication_classes = [TokenAuth]
@action(methods=['GET'], detail=False)
@action(methods=['POST'], detail=False)
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
@log(menu=_('Function'), operate=_("Add internal function"))
def get(self, request: Request, id: str):
def post(self, request: Request, id: str):
return result.success(
FunctionLibSerializer.InternalFunction(
data={'id': id, 'user_id': request.user.id}).add())
data={'id': id, 'user_id': request.user.id, 'name': request.data.get('name')})
.add())

View File

@ -122,9 +122,10 @@ const putFunctionLibIcon: (
const addInternalFunction: (
id: string,
data: any,
loading?: Ref<boolean>
) => Promise<Result<any>> = (id, loading) => {
return get(`${prefix}/${id}/add_internal_fun`, undefined, loading)
) => Promise<Result<any>> = (id, data, loading) => {
return post(`${prefix}/${id}/add_internal_fun`, data, undefined, loading)
}
const importFunctionLib: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (

View File

@ -0,0 +1,92 @@
<template>
<el-dialog
:title="$t('views.functionLib.functionForm.form.functionName.placeholder')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
append-to-body
width="450"
>
<el-form
label-position="top"
ref="fieldFormRef"
:rules="rules"
:model="form"
require-asterisk-position="right"
>
<el-form-item prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
{{ isEdit ? $t('common.save') : $t('common.add') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import type { FormInstance } from 'element-plus'
import { cloneDeep } from 'lodash'
import { t } from '@/locales'
import functionLibApi from '@/api/function-lib'
import { MsgSuccess } from '@/utils/message'
const emit = defineEmits(['refresh'])
const fieldFormRef = ref()
const loading = ref<boolean>(false)
const isEdit = ref(false)
const form = ref<any>({
name: ''
})
const rules = reactive({
name: [
{
required: true,
message: t('views.functionLib.functionForm.form.functionName.placeholder'),
trigger: 'blur'
}
]
})
const dialogVisible = ref<boolean>(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
name: ''
}
isEdit.value = false
}
})
const open = (row: any) => {
if (row) {
form.value = cloneDeep(row)
isEdit.value = true
}
dialogVisible.value = true
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
emit('refresh', form.value)
dialogVisible.value = false
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -229,14 +229,12 @@
/>
</template>
<div class="status-button">
<el-tag class="info-tag" v-if="item.added" style="height: 22px">
{{ $t('views.functionLib.added') }}</el-tag
>
</div>
<template #footer>
<div class="footer-content flex-between">
<div>{{ $t('common.author') }}: MaxKB</div>
<div @click.stop v-if="!item.added">
<div @click.stop>
<el-button type="primary" link @click="addInternalFunction(item)">
{{ $t('common.add') }}
</el-button>
@ -250,6 +248,7 @@
</div>
<FunctionFormDrawer ref="FunctionFormDrawerRef" @refresh="refresh" :title="title" />
<PermissionDialog ref="PermissionDialogRef" @refresh="refresh" />
<AddInternalFunctionDialog ref="AddInternalFunctionDialogRef" @refresh="confirmAddInternalFunction" />
<InitParamDrawer ref="InitParamDrawerRef" @refresh="refresh" />
<component :is="internalDescComponent" ref="internalDescRef" />
</div>
@ -269,6 +268,7 @@ import { isAppIcon } from '@/utils/application'
import InfiniteScroll from '@/components/infinite-scroll/index.vue'
import CardBox from '@/components/card-box/index.vue'
import type { Dict } from '@/api/type/common'
import AddInternalFunctionDialog from '@/views/function-lib/component/AddInternalFunctionDialog.vue'
const internalIcons: Dict<any> = import.meta.glob('@/assets/fx/*/*.png', { eager: true })
let internalDesc: Dict<any> = import.meta.glob('@/assets/fx/*/index.vue', { eager: true })
@ -281,6 +281,7 @@ const loading = ref(false)
const FunctionFormDrawerRef = ref()
const PermissionDialogRef = ref()
const AddInternalFunctionDialogRef = ref()
const InitParamDrawerRef = ref()
const functionLibList = ref<any[]>([])
@ -356,7 +357,11 @@ function openDescDrawer(row: any) {
}
function addInternalFunction(data?: any) {
functionLibApi.addInternalFunction(data.id, changeStateloading).then((res) => {
AddInternalFunctionDialogRef.value.open(data)
}
function confirmAddInternalFunction(data?: any) {
functionLibApi.addInternalFunction(data.id, {name: data.name}, changeStateloading).then((res) => {
MsgSuccess(t('common.submitSuccess'))
searchHandle()
})