@@ -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) { | |||||
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 = | 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 { | |||||
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 currentNode | ||||
* @param context | * @param context | ||||
* @param params 动态参数 | * @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) | * 替换子 SQL(@xxx) | ||||
* @param sql | * @param sql | ||||
* @param context | * @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 | * @param str | ||||
*/ | */ | ||||
function matchSubQuery(str: string) { | 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 { | interface InvokeTreeNode { | ||||
label: string; | |||||
title: string; | |||||
sql: string; | sql: string; | ||||
key?: string; | |||||
params?: Record<string, string>; | params?: Record<string, string>; | ||||
children?: InvokeTreeNode[]; | children?: InvokeTreeNode[]; | ||||
} | } | ||||