add 支持查看调用树,并且支持搜索节点

update 修改 ESLINT 语法为 Vue3
update 优化解析器逻辑,便于理解
pull/3/head
yupi 2 years ago
parent bcf65b204d
commit 7f9d13ccc6

@ -4,7 +4,7 @@ module.exports = {
es2021: true, es2021: true,
node: true, node: true,
}, },
extends: ["plugin:vue/essential", "plugin:prettier/recommended"], extends: ["plugin:vue/vue3-recommended", "plugin:prettier/recommended"],
parserOptions: { parserOptions: {
ecmaVersion: "latest", ecmaVersion: "latest",
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { doGenerateSQL } from "./generator"; import { doGenerateSQL } from "./generator";
import { importExample } from "./examples";
import { onMounted, ref, toRaw } from "vue"; import { onMounted, ref, toRaw } from "vue";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import { format } from "sql-formatter"; import { format } from "sql-formatter";
@ -12,6 +13,7 @@ import HtmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import TsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; import TsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
import IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor; import IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
import SearchableTree from "./components/SearchableTree.vue";
(self as any).MonacoEnvironment = { (self as any).MonacoEnvironment = {
getWorker(_: any, label: any) { getWorker(_: any, label: any) {
@ -31,33 +33,37 @@ import IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
}, },
}; };
const initTreeNode = {
label: "main",
sql: "",
children: [],
};
const inputEditor = ref<IStandaloneCodeEditor>(); const inputEditor = ref<IStandaloneCodeEditor>();
const outputEditor = ref<IStandaloneCodeEditor>(); const outputEditor = ref<IStandaloneCodeEditor>();
const inputContainer = ref<HTMLElement>(); const inputContainer = ref<HTMLElement>();
const outputContainer = ref<HTMLElement>(); const outputContainer = ref<HTMLElement>();
const treeNode = ref<InvokeTreeNode>({ ...initTreeNode }); const invokeTree = ref<InvokeTree>();
const drawerVisible = ref(false);
const getSQL = () => { const getSQL = () => {
if (inputEditor.value && outputEditor.value) { if (!inputEditor.value || !outputEditor.value) {
const inputJSON = JSON.parse(toRaw(inputEditor.value).getValue()); return;
treeNode.value = { ...initTreeNode };
const sqlResult = doGenerateSQL(inputJSON, treeNode.value);
let result = format(sqlResult);
//
result = result.replaceAll("{ {", "{{");
result = result.replaceAll("} }", "}}");
toRaw(outputEditor.value).setValue(result);
console.log(treeNode.value);
} }
const inputJSON = JSON.parse(toRaw(inputEditor.value).getValue());
const generateResult = doGenerateSQL(inputJSON);
if (!generateResult) {
return;
}
let result = format(generateResult.resultSQL);
//
result = result.replaceAll("{ {", "{{");
result = result.replaceAll("} }", "}}");
toRaw(outputEditor.value).setValue(result);
//
invokeTree.value = [generateResult.invokeTree];
}; };
const getInvokeTree = () => {}; const showInvokeTree = () => {
if (!invokeTree.value) {
getSQL();
}
drawerVisible.value = true;
};
const initJSONValue = const initJSONValue =
"{\n" + "{\n" +
@ -109,14 +115,19 @@ onMounted(() => {
<div> <div>
<a-row justify="space-between" align="middle" :gutter="[0, 16]"> <a-row justify="space-between" align="middle" :gutter="[0, 16]">
<h1 style="margin-bottom: 0">🔨 结构化 SQL 生成器</h1> <h1 style="margin-bottom: 0">🔨 结构化 SQL 生成器</h1>
<div>使用 JSON 来编写 SQL告别重复代码点击查看文档</div> <a href="https://github.com/liyupi/sql-generator" target="_blank">
使用 JSON 来编写 SQL告别重复代码点击查看文档
</a>
<a-space size="large"> <a-space size="large">
<a-button size="large" type="primary" @click="getSQL"> <a-button size="large" type="primary" @click="getSQL">
生成 SQL 生成 SQL
</a-button> </a-button>
<a-button size="large" type="default" @click="getInvokeTree"> <a-button size="large" type="primary" ghost @click="showInvokeTree">
查看调用树 查看调用树
</a-button> </a-button>
<a-button size="large" type="default" @click="importExample">
导入例子
</a-button>
</a-space> </a-space>
</a-row> </a-row>
<div style="margin-top: 16px" /> <div style="margin-top: 16px" />
@ -137,7 +148,9 @@ onMounted(() => {
</a-col> </a-col>
</a-row> </a-row>
<br /> <br />
<div>yupi你能体会手写一句 3000 行的 SQL牵一发而动全身的恐惧么</div> <div style="margin-bottom: 16px">
yupi你能体会手写一句 3000 行的 SQL牵一发而动全身的恐惧么
</div>
<a-row justify="center"> <a-row justify="center">
<a-space> <a-space>
作者<a href="https://github.com/liyupi" target="_blank">鱼皮</a> 作者<a href="https://github.com/liyupi" target="_blank">鱼皮</a>
@ -148,6 +161,14 @@ onMounted(() => {
</a> </a>
</a-space> </a-space>
</a-row> </a-row>
<a-drawer
v-model:visible="drawerVisible"
title="调用树"
placement="right"
body-style="width: 50vw"
>
<SearchableTree :tree="invokeTree" />
</a-drawer>
</div> </div>
</template> </template>
@ -155,4 +176,8 @@ onMounted(() => {
#app { #app {
padding: 20px; padding: 20px;
} }
.ant-drawer-content-wrapper {
width: auto !important;
}
</style> </style>

@ -0,0 +1,91 @@
<template>
<div>
<a-input-search
v-model:value="searchValue"
size="large"
style="margin-bottom: 16px"
placeholder="输入规则名搜索"
enter-button
/>
<a-tree
:expanded-keys="expandedKeys"
:auto-expand-parent="autoExpandParent"
:tree-data="tree"
@expand="onExpand"
>
<template #title="{ title }">
<span v-if="title.indexOf(searchValue) > -1">
{{ title.substr(0, title.indexOf(searchValue)) }}
<span style="color: #f50">{{ searchValue }}</span>
{{ title.substr(title.indexOf(searchValue) + searchValue.length) }}
</span>
<span v-else>{{ title }}</span>
</template>
</a-tree>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
const props = defineProps<{ tree: InvokeTree }>();
const tree = ref(props.tree);
if (!tree.value) {
tree.value = [];
}
const expandedKeys = ref<string[]>([]);
const searchValue = ref<string>("");
const autoExpandParent = ref<boolean>(true);
const getParentKey = (
key: string | undefined,
tree: InvokeTree
): string | number | undefined => {
let parentKey;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey;
};
const onExpand = (keys: string[]) => {
expandedKeys.value = keys;
autoExpandParent.value = false;
};
const dataList: InvokeTreeNode[] = [];
const generateList = (data: InvokeTreeNode[], preKey: string) => {
for (let i = 0; i < data.length; i++) {
const node = data[i];
// tree key
const key = preKey + "-" + i;
node.key = key;
dataList.push(node);
if (node.children) {
generateList(node.children, key);
}
}
};
generateList(tree.value, "");
watch(searchValue, (value) => {
expandedKeys.value = dataList
.map((item: InvokeTreeNode) => {
if (item.title.indexOf(value) > -1) {
return getParentKey(item.key, tree.value);
}
return null;
})
.filter((item, i, self) => item && self.indexOf(item) === i) as any;
searchValue.value = value;
autoExpandParent.value = true;
});
</script>

@ -0,0 +1,3 @@
export const importExample = () => {
alert(1);
};

@ -1,14 +1,53 @@
/** /**
* SQL * SQL
* @param json * @param json
*/ */
export function doGenerateSQL(json: InputJSON): string { export function doGenerateSQL(json: InputJSON) {
if (!json?.main) { // 缺失入口
return ""; if (!json?.main) {
} return null;
const context = json; }
const result = replaceParams(context.main, context); const sql = json.main.sql ?? json.main;
return replaceSubSql(result, context); if (!sql) {
return null;
}
const initTreeNode = {
title: "main",
sql,
children: [],
};
const rootInvokeTreeNode = { ...initTreeNode };
const context = json;
const resultSQL = generateSQL(
context.main,
context,
context.main?.params,
rootInvokeTreeNode
);
return {
resultSQL,
invokeTree: rootInvokeTreeNode,
};
}
/**
* SQL
* @param currentNode
* @param context
* @param params
* @param invokeTreeNode
*/
function generateSQL(
currentNode: InputJSONValue,
context: InputJSON,
params?: Record<string, string>,
invokeTreeNode?: InvokeTreeNode
): string {
if (!currentNode) {
return "";
}
const result = replaceParams(currentNode, context, params, invokeTreeNode);
return replaceSubSql(result, context, invokeTreeNode);
} }
/** /**
@ -16,78 +55,112 @@ export function doGenerateSQL(json: InputJSON): string {
* @param currentNode * @param currentNode
* @param context * @param context
* @param params * @param params
* @param invokeTreeNode
*/ */
function replaceParams(currentNode: InputJSONValue, context: InputJSON, params?: Record<string, string>): string { function replaceParams(
if (currentNode == null) { currentNode: InputJSONValue,
return ""; context: InputJSON,
} params?: Record<string, string>,
const sql = currentNode.sql ?? currentNode; invokeTreeNode?: InvokeTreeNode
if (!sql) { ): string {
return ""; if (currentNode == null) {
} return "";
// 动态、静态参数结合,且优先用静态参数 }
params = {...(params ?? {}), ...currentNode.params}; const sql = currentNode.sql ?? currentNode;
// 无需替换 if (!sql) {
if (!params || Object.keys(params).length < 1) { return "";
return sql; }
} // 动态、静态参数结合,且优先用静态参数
let result = sql; params = { ...(params ?? {}), ...currentNode.params };
for (const paramsKey in params) { // 无需替换
const replacedKey = `#{${paramsKey}}`; if (!params || Object.keys(params).length < 1) {
// 递归解析 return sql;
const replacement = replaceSubSql(params[paramsKey], context); }
// find and replace let result = sql;
result = result.replaceAll(replacedKey, replacement); for (const paramsKey in params) {
} const replacedKey = `#{${paramsKey}}`;
return result; // 递归解析
// const replacement = replaceSubSql(
// params[paramsKey],
// context,
// invokeTreeNode
// );
const replacement = params[paramsKey];
// find and replace
result = result.replaceAll(replacedKey, replacement);
}
return result;
} }
/** /**
* SQL@xxx * SQL@xxx
* @param sql * @param sql
* @param context * @param context
* @param invokeTreeNode
*/ */
function replaceSubSql(sql: string, context: InputJSON): string { function replaceSubSql(
if (!sql) { sql: string,
return ""; context: InputJSON,
invokeTreeNode?: InvokeTreeNode
): string {
if (!sql) {
return "";
}
let result = sql;
result = String(result);
let regExpMatchArray = matchSubQuery(result);
// 依次替换
while (regExpMatchArray && regExpMatchArray.length > 2) {
// 找到结果
const subKey = regExpMatchArray[1];
// 可用来替换的节点
const replacementNode = context[subKey];
// 没有可替换的节点
if (!replacementNode) {
const errorMsg = `${subKey} 不存在`;
alert(errorMsg);
throw new Error(errorMsg);
}
// 获取要传递的动态参数
// e.g. "a = b, c = d"
let paramsStr = regExpMatchArray[2];
if (paramsStr) {
paramsStr = paramsStr.trim();
} }
let result = sql; // e.g. ["a = b", "c = d"]
result = String(result); const singleParamsStrArray = paramsStr.split("|||");
let regExpMatchArray = matchSubQuery(result); // string => object
// 依次替换 const params: Record<string, string> = {};
while (regExpMatchArray && regExpMatchArray.length > 2) { for (const singleParamsStr of singleParamsStrArray) {
// 找到结果 // 必须分成 2 段
const subKey = regExpMatchArray[1]; const keyValueArray = singleParamsStr.split("=", 2);
// 可用来替换的节点 if (keyValueArray.length < 2) {
const replacementNode = context[subKey]; continue;
// 没有可替换的节点 }
if (!replacementNode) { const key = keyValueArray[0].trim();
throw new Error(`${subKey} 不存在`); params[key] = keyValueArray[1].trim();
}
// 获取要传递的动态参数
// e.g. "a = b, c = d"
let paramsStr = regExpMatchArray[2];
if (paramsStr) {
paramsStr = paramsStr.trim();
}
// e.g. ["a = b", "c = d"]
const singleParamsStrArray = paramsStr.split('|||');
// string => object
const params: Record<string, string> = {};
for (const singleParamsStr of singleParamsStrArray) {
// 必须分成 2 段
const keyValueArray = singleParamsStr.split('=', 2);
if (keyValueArray.length < 2) {
continue;
}
const key = keyValueArray[0].trim();
params[key] = keyValueArray[1].trim();
}
const replacement = replaceParams(replacementNode, context, params);
result = result.replaceAll(regExpMatchArray[0], replacement);
regExpMatchArray = matchSubQuery(result);
} }
return result; let childInvokeTreeNode;
if (invokeTreeNode) {
childInvokeTreeNode = {
title: subKey,
sql,
params,
children: [],
};
invokeTreeNode.children?.push(childInvokeTreeNode);
}
// 递归解析被替换节点
const replacement = generateSQL(
replacementNode,
context,
params,
childInvokeTreeNode
);
result = result.replace(regExpMatchArray[0], replacement);
regExpMatchArray = matchSubQuery(result);
}
return result;
} }
/** /**
@ -95,41 +168,41 @@ function replaceSubSql(sql: string, context: InputJSON): string {
* @param str * @param str
*/ */
function matchSubQuery(str: string) { function matchSubQuery(str: string) {
if (!str) { if (!str) {
return null; return null;
} }
const regExp = /@([\u4e00-\u9fa5_a-zA-Z0-9]+)\((.*?)\)/; const regExp = /@([\u4e00-\u9fa5_a-zA-Z0-9]+)\((.*?)\)/;
let regExpMatchArray = str.match(regExp); let regExpMatchArray = str.match(regExp);
if (!regExpMatchArray || regExpMatchArray.index === undefined) { if (!regExpMatchArray || regExpMatchArray.index === undefined) {
return null; return null;
}
// @ 开始位置
let startPos = regExpMatchArray.index;
// 左括号右侧
let leftParenthesisPos = startPos + regExpMatchArray[1].length + 2;
// 遍历游标
let currPos = leftParenthesisPos;
// 默认匹配结束位置,需要对此结果进行修正
let endPos = startPos + regExpMatchArray[0].length;
// 剩余待匹配左括号数量
let leftCount = 1;
while (currPos < str.length) {
const currentChar = str.charAt(currPos);
if (currentChar === "(") {
leftCount++;
} else if (currentChar === ")") {
leftCount--;
} }
// @ 开始位置 // 匹配结束
let startPos = regExpMatchArray.index; if (leftCount == 0) {
// 左括号右侧 endPos = currPos + 1;
let leftParenthesisPos = startPos + regExpMatchArray[1].length + 2; break;
// 遍历游标
let currPos = leftParenthesisPos;
// 默认匹配结束位置,需要对此结果进行修正
let endPos = startPos + regExpMatchArray[0].length;
// 剩余待匹配左括号数量
let leftCount = 1;
while (currPos < str.length) {
const currentChar = str.charAt(currPos);
if (currentChar === '(') {
leftCount++;
} else if (currentChar === ')') {
leftCount--;
}
// 匹配结束
if (leftCount == 0) {
endPos = currPos + 1;
break;
}
currPos++;
} }
return [ currPos++;
str.slice(startPos, endPos), }
regExpMatchArray[1], return [
str.slice(leftParenthesisPos, endPos - 1) str.slice(startPos, endPos),
] regExpMatchArray[1],
} str.slice(leftParenthesisPos, endPos - 1),
];
}

@ -12,8 +12,9 @@ interface InputJSONValue {
* *
*/ */
interface InvokeTreeNode { interface InvokeTreeNode {
label: string; title: string;
sql: string; sql: string;
key?: string;
params?: Record<string, string>; params?: Record<string, string>;
children?: InvokeTreeNode[]; children?: InvokeTreeNode[];
} }

Loading…
Cancel
Save