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

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

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

@ -1,5 +1,6 @@
<script setup lang="ts">
import { doGenerateSQL } from "./generator";
import { importExample } from "./examples";
import { onMounted, ref, toRaw } from "vue";
import * as monaco from "monaco-editor";
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";
// eslint-disable-next-line no-undef
import IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
import SearchableTree from "./components/SearchableTree.vue";
(self as any).MonacoEnvironment = {
getWorker(_: any, label: any) {
@ -31,33 +33,37 @@ import IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
},
};
const initTreeNode = {
label: "main",
sql: "",
children: [],
};
const inputEditor = ref<IStandaloneCodeEditor>();
const outputEditor = ref<IStandaloneCodeEditor>();
const inputContainer = ref<HTMLElement>();
const outputContainer = ref<HTMLElement>();
const treeNode = ref<InvokeTreeNode>({ ...initTreeNode });
const invokeTree = ref<InvokeTree>();
const drawerVisible = ref(false);
const getSQL = () => {
if (inputEditor.value && outputEditor.value) {
const inputJSON = JSON.parse(toRaw(inputEditor.value).getValue());
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);
if (!inputEditor.value || !outputEditor.value) {
return;
}
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 =
"{\n" +
@ -109,14 +115,19 @@ onMounted(() => {
<div>
<a-row justify="space-between" align="middle" :gutter="[0, 16]">
<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-button size="large" type="primary" @click="getSQL">
生成 SQL
</a-button>
<a-button size="large" type="default" @click="getInvokeTree">
<a-button size="large" type="primary" ghost @click="showInvokeTree">
查看调用树
</a-button>
<a-button size="large" type="default" @click="importExample">
导入例子
</a-button>
</a-space>
</a-row>
<div style="margin-top: 16px" />
@ -137,7 +148,9 @@ onMounted(() => {
</a-col>
</a-row>
<br />
<div>yupi你能体会手写一句 3000 行的 SQL牵一发而动全身的恐惧么</div>
<div style="margin-bottom: 16px">
yupi你能体会手写一句 3000 行的 SQL牵一发而动全身的恐惧么
</div>
<a-row justify="center">
<a-space>
作者<a href="https://github.com/liyupi" target="_blank">鱼皮</a>
@ -148,6 +161,14 @@ onMounted(() => {
</a>
</a-space>
</a-row>
<a-drawer
v-model:visible="drawerVisible"
title="调用树"
placement="right"
body-style="width: 50vw"
>
<SearchableTree :tree="invokeTree" />
</a-drawer>
</div>
</template>
@ -155,4 +176,8 @@ onMounted(() => {
#app {
padding: 20px;
}
.ant-drawer-content-wrapper {
width: auto !important;
}
</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
*/
export function doGenerateSQL(json: InputJSON): string {
if (!json?.main) {
return "";
}
const context = json;
const result = replaceParams(context.main, context);
return replaceSubSql(result, context);
export function doGenerateSQL(json: InputJSON) {
// 缺失入口
if (!json?.main) {
return null;
}
const sql = json.main.sql ?? json.main;
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 context
* @param params
* @param invokeTreeNode
*/
function replaceParams(currentNode: InputJSONValue, context: InputJSON, params?: Record<string, string>): string {
if (currentNode == null) {
return "";
}
const sql = currentNode.sql ?? currentNode;
if (!sql) {
return "";
}
// 动态、静态参数结合,且优先用静态参数
params = {...(params ?? {}), ...currentNode.params};
// 无需替换
if (!params || Object.keys(params).length < 1) {
return sql;
}
let result = sql;
for (const paramsKey in params) {
const replacedKey = `#{${paramsKey}}`;
// 递归解析
const replacement = replaceSubSql(params[paramsKey], context);
// find and replace
result = result.replaceAll(replacedKey, replacement);
}
return result;
function replaceParams(
currentNode: InputJSONValue,
context: InputJSON,
params?: Record<string, string>,
invokeTreeNode?: InvokeTreeNode
): string {
if (currentNode == null) {
return "";
}
const sql = currentNode.sql ?? currentNode;
if (!sql) {
return "";
}
// 动态、静态参数结合,且优先用静态参数
params = { ...(params ?? {}), ...currentNode.params };
// 无需替换
if (!params || Object.keys(params).length < 1) {
return sql;
}
let result = sql;
for (const paramsKey in params) {
const replacedKey = `#{${paramsKey}}`;
// 递归解析
// const replacement = replaceSubSql(
// params[paramsKey],
// context,
// invokeTreeNode
// );
const replacement = params[paramsKey];
// find and replace
result = result.replaceAll(replacedKey, replacement);
}
return result;
}
/**
* SQL@xxx
* @param sql
* @param context
* @param invokeTreeNode
*/
function replaceSubSql(sql: string, context: InputJSON): string {
if (!sql) {
return "";
function replaceSubSql(
sql: string,
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;
result = String(result);
let regExpMatchArray = matchSubQuery(result);
// 依次替换
while (regExpMatchArray && regExpMatchArray.length > 2) {
// 找到结果
const subKey = regExpMatchArray[1];
// 可用来替换的节点
const replacementNode = context[subKey];
// 没有可替换的节点
if (!replacementNode) {
throw new Error(`${subKey} 不存在`);
}
// 获取要传递的动态参数
// 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);
// 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();
}
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
*/
function matchSubQuery(str: string) {
if (!str) {
return null;
}
const regExp = /@([\u4e00-\u9fa5_a-zA-Z0-9]+)\((.*?)\)/;
let regExpMatchArray = str.match(regExp);
if (!regExpMatchArray || regExpMatchArray.index === undefined) {
return null;
if (!str) {
return null;
}
const regExp = /@([\u4e00-\u9fa5_a-zA-Z0-9]+)\((.*?)\)/;
let regExpMatchArray = str.match(regExp);
if (!regExpMatchArray || regExpMatchArray.index === undefined) {
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;
// 左括号右侧
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--;
}
// 匹配结束
if (leftCount == 0) {
endPos = currPos + 1;
break;
}
currPos++;
// 匹配结束
if (leftCount == 0) {
endPos = currPos + 1;
break;
}
return [
str.slice(startPos, endPos),
regExpMatchArray[1],
str.slice(leftParenthesisPos, endPos - 1)
]
}
currPos++;
}
return [
str.slice(startPos, endPos),
regExpMatchArray[1],
str.slice(leftParenthesisPos, endPos - 1),
];
}

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

Loading…
Cancel
Save