mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: Add functionality to create and manage internal functions with MySQL and PostgreSQL queries
This commit is contained in:
parent
5ec94860b2
commit
4b4b84a220
|
|
@ -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);
|
||||
|
||||
'''
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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>> = (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue