Skip to content

Commit 4b4b84a

Browse files
committed
feat: Add functionality to create and manage internal functions with MySQL and PostgreSQL queries
1 parent 5ec9486 commit 4b4b84a

File tree

6 files changed

+154
-63
lines changed

6 files changed

+154
-63
lines changed

apps/function_lib/migrations/0003_functionlib_function_type_functionlib_icon_and_more.py

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,48 @@ def langsearch(query, apikey):
6666
else:
6767
raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}")
6868
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);
69+
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'
70+
def query_mysql(host,port, user, password, database, sql):
71+
import pymysql
72+
import json
73+
from pymysql.cursors import DictCursor
74+
75+
try:
76+
# 创建连接
77+
db = pymysql.connect(
78+
host=host,
79+
port=int(port),
80+
user=user,
81+
password=password,
82+
database=database,
83+
cursorclass=DictCursor # 使用字典游标
84+
)
85+
86+
# 使用 cursor() 方法创建一个游标对象 cursor
87+
cursor = db.cursor()
88+
89+
# 使用 execute() 方法执行 SQL 查询
90+
cursor.execute(sql)
91+
92+
# 使用 fetchall() 方法获取所有数据
93+
data = cursor.fetchall()
94+
95+
# 处理 bytes 类型的数据
96+
for row in data:
97+
for key, value in row.items():
98+
if isinstance(value, bytes):
99+
row[key] = value.decode("utf-8") # 转换为字符串
100+
101+
# 将数据序列化为 JSON
102+
json_data = json.dumps(data, ensure_ascii=False)
103+
print(json_data)
104+
return json_data
105+
106+
# 关闭数据库连接
107+
db.close()
108+
109+
except Exception as e:
110+
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);
69111
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):
70112
import psycopg2
71113
import json
@@ -114,49 +156,7 @@ def default_serializer(obj):
114156
cursor.close()
115157
if conn:
116158
conn.close()
117-
', '{"{\"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);
118-
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'
119-
def query_mysql(host,port, user, password, database, sql):
120-
import pymysql
121-
import json
122-
from pymysql.cursors import DictCursor
123-
124-
try:
125-
# 创建连接
126-
db = pymysql.connect(
127-
host=host,
128-
port=int(port),
129-
user=user,
130-
password=password,
131-
database=database,
132-
cursorclass=DictCursor # 使用字典游标
133-
)
134-
135-
# 使用 cursor() 方法创建一个游标对象 cursor
136-
cursor = db.cursor()
137-
138-
# 使用 execute() 方法执行 SQL 查询
139-
cursor.execute(sql)
140-
141-
# 使用 fetchall() 方法获取所有数据
142-
data = cursor.fetchall()
143-
144-
# 处理 bytes 类型的数据
145-
for row in data:
146-
for key, value in row.items():
147-
if isinstance(value, bytes):
148-
row[key] = value.decode(\"utf-8\") # 转换为字符串
149-
150-
# 将数据序列化为 JSON
151-
json_data = json.dumps(data, ensure_ascii=False)
152-
print(json_data)
153-
return json_data
154-
155-
# 关闭数据库连接
156-
db.close()
157-
158-
except Exception as e:
159-
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);
159+
', '{"{\"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);
160160
161161
'''
162162

apps/function_lib/serializers/function_lib_serializer.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,6 @@ def get_query_set(self):
158158
query_set = query_set.filter(function_type=self.data.get('function_type'))
159159
query_set = query_set.order_by("-create_time")
160160

161-
subquery = FunctionLib.objects.filter(template_id=OuterRef('id'))
162-
subquery = subquery.filter(user_id=self.data.get('user_id'))
163-
query_set = query_set.annotate(added=Exists(subquery))
164-
165161
return query_set
166162

167163
def list(self, with_valid=True):
@@ -180,7 +176,6 @@ def page(self, current_page: int, page_size: int, with_valid=True):
180176
def post_records_handler(row):
181177
return {
182178
**FunctionLibModelSerializer(row).data,
183-
'added': row.added,
184179
'init_params': None
185180
}
186181

@@ -390,22 +385,19 @@ def edit(self, with_valid=True):
390385
class InternalFunction(serializers.Serializer):
391386
id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("function ID")))
392387
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
388+
name = serializers.CharField(required=True, error_messages=ErrMessage.char(_("function name")))
393389

394390
def add(self, with_valid=True):
395391
if with_valid:
396392
self.is_valid(raise_exception=True)
397393

398-
if QuerySet(FunctionLib).filter(template_id=self.data.get('id')).filter(
399-
user_id=self.data.get('user_id')).exists():
400-
raise AppApiException(500, _('Function already exists'))
401-
402394
internal_function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
403395
if internal_function_lib is None:
404396
raise AppApiException(500, _('Function does not exist'))
405397

406398
function_lib = FunctionLib(
407399
id=uuid.uuid1(),
408-
name=internal_function_lib.name,
400+
name=self.data.get('name'),
409401
desc=internal_function_lib.desc,
410402
code=internal_function_lib.code,
411403
user_id=self.data.get('user_id'),

apps/function_lib/views/function_lib_views.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,11 @@ def put(self, request: Request, id: str):
166166
class AddInternalFun(APIView):
167167
authentication_classes = [TokenAuth]
168168

169-
@action(methods=['GET'], detail=False)
169+
@action(methods=['POST'], detail=False)
170170
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
171171
@log(menu=_('Function'), operate=_("Add internal function"))
172-
def get(self, request: Request, id: str):
172+
def post(self, request: Request, id: str):
173173
return result.success(
174174
FunctionLibSerializer.InternalFunction(
175-
data={'id': id, 'user_id': request.user.id}).add())
175+
data={'id': id, 'user_id': request.user.id, 'name': request.data.get('name')})
176+
.add())

ui/src/api/function-lib.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,10 @@ const putFunctionLibIcon: (
122122

123123
const addInternalFunction: (
124124
id: string,
125+
data: any,
125126
loading?: Ref<boolean>
126-
) => Promise<Result<any>> = (id, loading) => {
127-
return get(`${prefix}/${id}/add_internal_fun`, undefined, loading)
127+
) => Promise<Result<any>> = (id, data, loading) => {
128+
return post(`${prefix}/${id}/add_internal_fun`, data, undefined, loading)
128129
}
129130

130131
const importFunctionLib: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<template>
2+
<el-dialog
3+
:title="$t('views.functionLib.functionForm.form.functionName.placeholder')"
4+
v-model="dialogVisible"
5+
:close-on-click-modal="false"
6+
:close-on-press-escape="false"
7+
:destroy-on-close="true"
8+
append-to-body
9+
width="450"
10+
>
11+
<el-form
12+
label-position="top"
13+
ref="fieldFormRef"
14+
:rules="rules"
15+
:model="form"
16+
require-asterisk-position="right"
17+
>
18+
<el-form-item prop="name">
19+
<el-input v-model="form.name"></el-input>
20+
</el-form-item>
21+
</el-form>
22+
<template #footer>
23+
<span class="dialog-footer">
24+
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
25+
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
26+
{{ isEdit ? $t('common.save') : $t('common.add') }}
27+
</el-button>
28+
</span>
29+
</template>
30+
</el-dialog>
31+
</template>
32+
<script setup lang="ts">
33+
import { reactive, ref, watch } from 'vue'
34+
import type { FormInstance } from 'element-plus'
35+
import { cloneDeep } from 'lodash'
36+
import { t } from '@/locales'
37+
import functionLibApi from '@/api/function-lib'
38+
import { MsgSuccess } from '@/utils/message'
39+
40+
const emit = defineEmits(['refresh'])
41+
42+
const fieldFormRef = ref()
43+
const loading = ref<boolean>(false)
44+
const isEdit = ref(false)
45+
46+
const form = ref<any>({
47+
name: ''
48+
})
49+
50+
const rules = reactive({
51+
name: [
52+
{
53+
required: true,
54+
message: t('views.functionLib.functionForm.form.functionName.placeholder'),
55+
trigger: 'blur'
56+
}
57+
]
58+
})
59+
60+
const dialogVisible = ref<boolean>(false)
61+
62+
watch(dialogVisible, (bool) => {
63+
if (!bool) {
64+
form.value = {
65+
name: ''
66+
}
67+
isEdit.value = false
68+
}
69+
})
70+
71+
const open = (row: any) => {
72+
if (row) {
73+
form.value = cloneDeep(row)
74+
isEdit.value = true
75+
}
76+
77+
dialogVisible.value = true
78+
}
79+
80+
const submit = async (formEl: FormInstance | undefined) => {
81+
if (!formEl) return
82+
await formEl.validate((valid) => {
83+
if (valid) {
84+
emit('refresh', form.value)
85+
dialogVisible.value = false
86+
}
87+
})
88+
}
89+
90+
defineExpose({ open })
91+
</script>
92+
<style lang="scss" scoped></style>

ui/src/views/function-lib/index.vue

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,12 @@
229229
/>
230230
</template>
231231
<div class="status-button">
232-
<el-tag class="info-tag" v-if="item.added" style="height: 22px">
233-
{{ $t('views.functionLib.added') }}</el-tag
234-
>
232+
235233
</div>
236234
<template #footer>
237235
<div class="footer-content flex-between">
238236
<div>{{ $t('common.author') }}: MaxKB</div>
239-
<div @click.stop v-if="!item.added">
237+
<div @click.stop>
240238
<el-button type="primary" link @click="addInternalFunction(item)">
241239
{{ $t('common.add') }}
242240
</el-button>
@@ -250,6 +248,7 @@
250248
</div>
251249
<FunctionFormDrawer ref="FunctionFormDrawerRef" @refresh="refresh" :title="title" />
252250
<PermissionDialog ref="PermissionDialogRef" @refresh="refresh" />
251+
<AddInternalFunctionDialog ref="AddInternalFunctionDialogRef" @refresh="confirmAddInternalFunction" />
253252
<InitParamDrawer ref="InitParamDrawerRef" @refresh="refresh" />
254253
<component :is="internalDescComponent" ref="internalDescRef" />
255254
</div>
@@ -269,6 +268,7 @@ import { isAppIcon } from '@/utils/application'
269268
import InfiniteScroll from '@/components/infinite-scroll/index.vue'
270269
import CardBox from '@/components/card-box/index.vue'
271270
import type { Dict } from '@/api/type/common'
271+
import AddInternalFunctionDialog from '@/views/function-lib/component/AddInternalFunctionDialog.vue'
272272
273273
const internalIcons: Dict<any> = import.meta.glob('@/assets/fx/*/*.png', { eager: true })
274274
let internalDesc: Dict<any> = import.meta.glob('@/assets/fx/*/index.vue', { eager: true })
@@ -281,6 +281,7 @@ const loading = ref(false)
281281
282282
const FunctionFormDrawerRef = ref()
283283
const PermissionDialogRef = ref()
284+
const AddInternalFunctionDialogRef = ref()
284285
const InitParamDrawerRef = ref()
285286
286287
const functionLibList = ref<any[]>([])
@@ -356,7 +357,11 @@ function openDescDrawer(row: any) {
356357
}
357358
358359
function addInternalFunction(data?: any) {
359-
functionLibApi.addInternalFunction(data.id, changeStateloading).then((res) => {
360+
AddInternalFunctionDialogRef.value.open(data)
361+
}
362+
363+
function confirmAddInternalFunction(data?: any) {
364+
functionLibApi.addInternalFunction(data.id, {name: data.name}, changeStateloading).then((res) => {
360365
MsgSuccess(t('common.submitSuccess'))
361366
searchHandle()
362367
})

0 commit comments

Comments
 (0)