fix(data-center): populate SQL data source options and normalize dataset source id mapping

This commit is contained in:
plum 2026-04-15 23:34:52 +08:00
parent 27b4b720ed
commit fbfaa59b14
4 changed files with 318 additions and 168 deletions

View File

@ -17,7 +17,7 @@ export interface DataSetPayload {
type: "API" | "SQL" | "JSON" | string; type: "API" | "SQL" | "JSON" | string;
method?: IDataSet.Item["method"]; method?: IDataSet.Item["method"];
api?: string; api?: string;
dataSourceId?: string; dataSourceId?: IDataSet.Item["id"];
sql?: string; sql?: string;
json?: string; json?: string;
} }

View File

@ -65,7 +65,7 @@ const defaultDataSet: IDataSet.Item = {
type: 'API', type: 'API',
method: 'GET', method: 'GET',
api: '', api: '',
dataSource: '', dataSourceId: '',
sql: '', sql: '',
json: '' json: ''
}; };
@ -179,7 +179,7 @@ async function editDataSet(item) {
const detail = (res.data || item) as any; const detail = (res.data || item) as any;
resetCurrentDataSet({ resetCurrentDataSet({
...detail, ...detail,
dataSource: detail.dataSourceId || detail.dataSource || "" dataSourceId: detail.dataSourceId || detail.dataSource ? String(detail.dataSourceId || detail.dataSource) : ""
}); });
showDataSetModal.value = true showDataSetModal.value = true
} }

View File

@ -1,50 +1,62 @@
<template> <template>
<n-modal :show="show" @mask-click="handleClose"> <n-modal :show="show" @mask-click="handleClose">
<n-card class="w-200 max-w-1200px" :title="t('home.Data set config')"> <n-card class="w-200 max-w-1200px" :title="t('home.dataCenter.Data set config')">
<n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto" :disabled="submitLoading"> <n-form :model="model" :rules="rules" ref="formRef" label-placement="left" label-width="auto" :disabled="submitLoading">
<n-grid :cols="24" :x-gap="24"> <n-grid :cols="24" :x-gap="24">
<n-form-item-gi :span="12" :label="t('home.Data set name')" path="name"> <n-form-item-gi :span="12" :label="t('home.dataCenter.Data set name')" path="name">
<n-input v-model:value="model.name" /> <n-input v-model:value="model.name" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="12" :label="t('home.Data set group')"> <n-form-item-gi :span="12" :label="t('home.dataCenter.Data set group')" path="groupId">
<n-cascader v-model:value="model.groupId" expand-trigger="hover" :options="groupOptions" <n-cascader
check-strategy="all" show-path filterable clearable label-field="name" value-field="id" /> v-model:value="model.groupId"
expand-trigger="hover"
:options="groupOptions"
check-strategy="all"
show-path
filterable
clearable
label-field="name"
value-field="id"
/>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="12" :label="t('home.Data set type')"> <n-form-item-gi :span="12" :label="t('home.dataCenter.Data set type')" path="type">
<n-select v-model:value="model.type" :options="setTypes" /> <n-select v-model:value="model.type" :options="setTypes" />
</n-form-item-gi> </n-form-item-gi>
<template v-if="model.type === 'API'"> <template v-if="model.type === 'API'">
<n-form-item-gi :span="12" :label="t('home.Method')"> <n-form-item-gi :span="12" :label="t('home.dataCenter.Method')" path="method">
<n-select v-model:value="model.method" :options="[ <n-select
v-model:value="model.method"
:options="[
{ label: 'GET', value: 'GET' }, { label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' }, { label: 'POST', value: 'POST' },
]" /> ]"
/>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="24" :label="t('home.API interface')"> <n-form-item-gi :span="24" :label="t('home.dataCenter.API interface')" path="api">
<n-input v-model:value="model.api" type="textarea" /> <n-input v-model:value="model.api" type="textarea" />
</n-form-item-gi> </n-form-item-gi>
</template> </template>
<template v-else-if="model.type === 'SQL'"> <template v-else-if="model.type === 'SQL'">
<n-form-item-gi :span="12" :label="t('home.Data sources')"> <n-form-item-gi :span="12" :label="t('home.dataCenter.Data sources')" path="dataSourceId">
<n-select v-model:value="model.dataSource" :options="dataSourceOptions" /> <n-select v-model:value="model.dataSourceId" :options="dataSourceOptions" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="24" :label="`SQL(${t('home.Query only')})`"> <n-form-item-gi :span="24" :label="`SQL(${t('home.dataCenter.Query only')})`" path="sql">
<SQLEditor v-model:value="model.sql as string" /> <SQLEditor v-model:value="model.sql as string" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="24" label=" "> <n-form-item-gi :span="24" label=" ">
<n-blockquote> <n-blockquote>
{{ t('home.Parameter is passed as ') }} {{ t('home.dataCenter["Parameter is passed as:"]') }}
'${id}' , '${id}' ,
{{ t('home.For example:') }} {{ t('home.dataCenter["For example:"]') }}
SELECT * FROM table WHERE id='${id}' SELECT * FROM table WHERE id='${id}'
</n-blockquote> </n-blockquote>
</n-form-item-gi> </n-form-item-gi>
</template> </template>
<template v-else-if="model.type === 'JSON'"> <template v-else-if="model.type === 'JSON'">
<n-form-item-gi :span="24" label="JSON"> <n-form-item-gi :span="24" label="JSON" path="json">
<JSONEditor v-model:value="model.json as string" /> <JSONEditor v-model:value="model.json as string" />
</n-form-item-gi> </n-form-item-gi>
</template> </template>
@ -62,119 +74,253 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch, useTemplateRef } from "vue"; import { ref, watch, computed, useTemplateRef } from "vue";
import type { FormInst } from 'naive-ui' import type { FormInst } from "naive-ui";
import { t } from "@/language"; import { t } from "@/language";
import { DataSetPayload, fetchCreateDataSet, fetchUpdateDataSet } from "@/http/api/dataSet"; import { DataSetPayload, fetchCreateDataSet, fetchUpdateDataSet } from "@/http/api/dataSet";
import { fetchDataSetGroupTree } from "@/http/api/dataSetGroup"; import { fetchDataSetGroupTree } from "@/http/api/dataSetGroup";
import { fetchDataSourceList } from "@/http/api/dataSource"; import { fetchDataSourceList } from "@/http/api/dataSource";
import SQLEditor from "@/components/code/SQLEditor.vue"; import SQLEditor from "@/components/code/SQLEditor.vue";
import JSONEditor from "@/components/code/JSONEditor.vue"; import JSONEditor from "@/components/code/JSONEditor.vue";
const props = withDefaults(
const props = withDefaults(defineProps<{ defineProps<{
show: boolean, show: boolean;
model: IDataSet.Item model?: IDataSet.Item;
}>(), { }>(),
{
show: false, show: false,
model: () => ({ model: () => ({
id: '', id: "",
groupId: "", groupId: "",
name: '', name: "",
type: 'SQL', type: "API",
}) method: "GET",
}) api: "",
const emits = defineEmits(["update:show", "refresh"]); dataSourceId: "",
sql: "",
json: "",
createTime: "",
}),
}
);
const emits = defineEmits(["update:show", "refresh"]);
const formRef = useTemplateRef<FormInst>("formRef"); const formRef = useTemplateRef<FormInst>("formRef");
const rules = { function createRequiredRule(message: string, trigger: Array<"input" | "blur" | "change">) {
name: { required: true, message: t("prompt.Please enter a name for the dataset"), trigger: 'blur' } return {
}; trigger,
const groupOptions = ref<IDataSet.IGroup[]>([]); validator(_: unknown, value: unknown) {
const setTypes = [ if (value === undefined || value === null || value === "") {
{ label: 'API', value: 'API' }, return new Error(message);
{ label: 'SQL', value: 'SQL' }, }
{ label: 'JSON', value: 'JSON' }, if (Array.isArray(value) && value.length === 0) {
]; return new Error(message);
const dataSourceOptions = ref<{ label: string; value: IDataSource.Item["id"] }[]>([]); }
const submitLoading = ref(false); return true;
},
};
}
watch(() => props.show, (show) => { const rules = computed(() => {
const baseRules: Record<string, any> = {
name: { required: true, message: t("home.dataCenter.Please enter a name for the dataset"), trigger: ["input", "blur"] },
groupId: createRequiredRule(t("home.dataCenter.Please select a grouping for the dataset"), ["change", "blur"]),
type: { required: true, message: t("home.dataCenter.Please select a dataset type"), trigger: ["change", "blur"] },
};
const type = props.model?.type;
if (type === "API") {
baseRules.method = {
required: true,
message: t("home.dataCenter.Please select a method"),
trigger: ["change", "blur"],
};
baseRules.api = {
required: true,
message: t("home.dataCenter.Please enter the interface url"),
trigger: ["input", "blur"],
};
} else if (type === "SQL") {
baseRules.dataSourceId = createRequiredRule(t("home.dataCenter.Please select a data source"), ["change", "blur"]);
baseRules.sql = { required: true, message: t("home.dataCenter.Please enter a SQL string"), trigger: ["blur"] };
} else if (type === "JSON") {
baseRules.json = { required: true, message: t("home.dataCenter.Please enter the JSON data"), trigger: ["blur"] };
}
return baseRules;
});
const groupOptions = ref<IDataSet.IGroup[]>([]);
const setTypes = [
{ label: "API", value: "API" },
{ label: "SQL", value: "SQL" },
{ label: "JSON", value: "JSON" },
];
const dataSourceOptions = ref<{ label: string; value: IDataSource.Item["id"] }[]>([]);
const submitLoading = ref(false);
const isEdit = computed(() => Boolean(props.model?.id));
function normalizeDataSourceId(value: unknown): IDataSource.Item["id"] | undefined {
if (value === undefined || value === null || value === "") {
return undefined;
}
return String(value);
}
function extractList<T>(value: unknown): T[] {
if (Array.isArray(value)) {
return value as T[];
}
if (value && typeof value === "object") {
const nested = (value as Record<string, unknown>).result;
if (Array.isArray(nested)) {
return nested as T[];
}
}
return [];
}
async function loadOptions() {
const [groupRes, dataSourceRes] = await Promise.allSettled([fetchDataSetGroupTree(), fetchDataSourceList()]);
if (groupRes.status === "fulfilled") {
groupOptions.value = extractList<IDataSet.IGroup>(groupRes.value.data);
} else {
groupOptions.value = [];
}
if (dataSourceRes.status === "fulfilled") {
const list = extractList<IDataSource.Item>(dataSourceRes.value.data);
dataSourceOptions.value = list.map(item => ({
label: item.name,
value: String(item.id),
}));
} else {
dataSourceOptions.value = [];
}
}
function normalizeJsonValue(value?: IDataSet.Item["json"]) {
if (!value) {
return "";
}
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value, null, 2);
} catch {
return "";
}
}
function resetFieldsForType(type: string) {
if (!props.model) {
return;
}
if (type !== "API") {
props.model.method = undefined;
props.model.api = "";
}
if (type === "API" && !props.model.method) {
props.model.method = "GET";
}
if (type !== "SQL") {
props.model.dataSourceId = undefined;
props.model.sql = "";
}
if (type !== "JSON") {
props.model.json = "";
}
}
function validateSql(sql: string) {
const trimmed = sql.trim();
if (!/^(SELECT|WITH)\b/i.test(trimmed)) {
window.$message?.error(t("home.dataCenter.SQL must start with SELECT or WITH"));
return false;
}
if (/[;]|--|\/\*/.test(trimmed)) {
window.$message?.error(t("home.dataCenter.SQL must not include unsafe tokens"));
return false;
}
return true;
}
function buildPayload(): DataSetPayload {
const payload: DataSetPayload = {
name: props.model?.name?.trim() || "",
groupId: props.model?.groupId ?? "",
type: props.model?.type || "API",
};
if (props.model?.id) {
payload.id = props.model.id;
}
if (payload.type === "API") {
payload.method = props.model?.method;
payload.api = props.model?.api?.trim();
} else if (payload.type === "SQL") {
payload.dataSourceId = normalizeDataSourceId(props.model?.dataSourceId);
payload.sql = props.model?.sql?.trim();
} else if (payload.type === "JSON") {
payload.json = normalizeJsonValue(props.model?.json);
}
return payload;
}
watch(
() => props.show,
show => {
if (!show) { if (!show) {
return; return;
} }
loadOptions(); loadOptions();
}); if (props.model) {
props.model.dataSourceId = normalizeDataSourceId(props.model.dataSourceId);
watch(() => props.model.type, (nextType, oldType) => {
if (!nextType || nextType === oldType) {
return;
} }
if (nextType !== "API") { if (props.model?.type === "API" && !props.model.method) {
props.model.method = undefined;
props.model.api = "";
} else if (!props.model.method) {
props.model.method = "GET"; props.model.method = "GET";
} }
if (nextType !== "SQL") { if (props.model?.type === "JSON") {
props.model.dataSource = ""; props.model.json = normalizeJsonValue(props.model.json);
props.model.sql = "";
} }
if (nextType !== "JSON") {
props.model.json = "";
} }
}); );
async function loadOptions() { watch(
const [groupRes, dataSourceRes] = await Promise.all([ () => props.model?.type,
fetchDataSetGroupTree(), (next, prev) => {
fetchDataSourceList() if (!next || next === prev) {
]); return;
groupOptions.value = groupRes.data || []; }
dataSourceOptions.value = (dataSourceRes.data || []).map(item => ({ resetFieldsForType(next);
label: item.name, formRef.value?.restoreValidation();
value: item.id }
})); );
}
function handleClose() { function handleClose() {
emits("update:show", false); emits("update:show", false);
}
function saveDataSet(e: MouseEvent) {
e.preventDefault()
formRef.value?.validate(async (errors) => {
if (!errors) {
submitLoading.value = true;
const payload: DataSetPayload = {
id: props.model.id || undefined,
name: props.model.name?.trim() || "",
groupId: props.model.groupId,
type: props.model.type
};
if (props.model.type === "API") {
payload.method = props.model.method;
payload.api = props.model.api?.trim();
} else if (props.model.type === "SQL") {
payload.dataSourceId = props.model.dataSource;
payload.sql = props.model.sql?.trim();
} else if (props.model.type === "JSON") {
payload.json = props.model.json;
} }
const res = props.model.id ? await fetchUpdateDataSet(payload) : await fetchCreateDataSet(payload); function saveDataSet(e: MouseEvent) {
e.preventDefault();
formRef.value?.validate(async errors => {
if (errors || !props.model) {
return;
}
if (props.model.type === "SQL" && !validateSql(props.model.sql || "")) {
return;
}
submitLoading.value = true;
const payload = buildPayload();
const res = isEdit.value ? await fetchUpdateDataSet(payload) : await fetchCreateDataSet(payload);
submitLoading.value = false; submitLoading.value = false;
if (res.error) { if (res.error) {
return; return;
} }
window.$message?.success(props.model.id ? t("prompt.Success to update") : t("prompt.Saved successfully!")); window.$message?.success(isEdit.value ? t("prompt.Success to update") : t("prompt.Saved successfully!"));
//
emits("refresh"); emits("refresh");
handleClose(); handleClose();
});
} }
})
}
</script> </script>

View File

@ -1,6 +1,7 @@
declare namespace IDataSource { declare namespace IDataSource {
type Id = string | number;
interface Item { interface Item {
id: string; id: Id;
name: string; name: string;
type: string; type: string;
connectionString: string; connectionString: string;
@ -10,21 +11,24 @@ declare namespace IDataSource {
} }
declare namespace IDataSet { declare namespace IDataSet {
type Id = string | number;
interface Item { interface Item {
id: string; id: Id;
groupId: string; groupId: Id;
name: string; name: string;
type: string; type: string;
method?: "GET" | "POST"; method?: "GET" | "POST";
api?: string; api?: string;
dataSource?: string; dataSourceId?: Id;
dataSource?: Id;
sql?: string; sql?: string;
json?: string; json?: string;
createTime?: string;
} }
interface IGroup { interface IGroup {
id: string; id: Id;
pid?: string; pid?: Id;
name: string; name: string;
children?: IGroup[]; children?: IGroup[];
} }