Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -114,49 +156,7 @@ def default_serializer(obj):
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);

'''

Expand Down
12 changes: 2 additions & 10 deletions apps/function_lib/serializers/function_lib_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,6 @@ def get_query_set(self):
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):
Expand All @@ -180,7 +176,6 @@ def page(self, current_page: int, page_size: int, with_valid=True):
def post_records_handler(row):
return {
**FunctionLibModelSerializer(row).data,
'added': row.added,
'init_params': None
}

Expand Down Expand Up @@ -390,22 +385,19 @@ def edit(self, with_valid=True):
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'),
Expand Down
7 changes: 4 additions & 3 deletions apps/function_lib/views/function_lib_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,11 @@ def put(self, request: Request, id: str):
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())
5 changes: 3 additions & 2 deletions ui/src/api/function-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>> = (
Expand Down
92 changes: 92 additions & 0 deletions ui/src/views/function-lib/component/AddInternalFunctionDialog.vue
Original file line number Diff line number Diff line change
@@ -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>
15 changes: 10 additions & 5 deletions ui/src/views/function-lib/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand All @@ -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 })
Expand All @@ -281,6 +281,7 @@ const loading = ref(false)

const FunctionFormDrawerRef = ref()
const PermissionDialogRef = ref()
const AddInternalFunctionDialogRef = ref()
const InitParamDrawerRef = ref()

const functionLibList = ref<any[]>([])
Expand Down Expand Up @@ -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()
})
Expand Down