Grok 5 updates
This commit is contained in:
parent
3b5fba4063
commit
79c5b896fe
52
bun.lock
52
bun.lock
@ -12,6 +12,7 @@
|
|||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"mariadb": "^3.5.3",
|
"mariadb": "^3.5.3",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
|
"sanitize-html": "^2.17.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
@ -19,6 +20,7 @@
|
|||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/mysql": "^2.15.27",
|
"@types/mysql": "^2.15.27",
|
||||||
"@types/node": "^25.3.3",
|
"@types/node": "^25.3.3",
|
||||||
|
"@types/sanitize-html": "^2.16.1",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
@ -70,6 +72,8 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@25.9.4", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g=="],
|
"@types/node": ["@types/node@25.9.4", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g=="],
|
||||||
|
|
||||||
|
"@types/sanitize-html": ["@types/sanitize-html@2.16.1", "", { "dependencies": { "htmlparser2": "^10.1" } }, "sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA=="],
|
||||||
|
|
||||||
"@types/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="],
|
"@types/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="],
|
||||||
|
|
||||||
"bignumber.js": ["bignumber.js@9.0.0", "", {}, "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="],
|
"bignumber.js": ["bignumber.js@9.0.0", "", {}, "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="],
|
||||||
@ -86,22 +90,44 @@
|
|||||||
|
|
||||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||||
|
|
||||||
|
"dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="],
|
||||||
|
|
||||||
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||||
|
|
||||||
|
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||||
|
|
||||||
|
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||||
|
|
||||||
|
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||||
|
|
||||||
|
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||||
|
|
||||||
|
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
||||||
|
|
||||||
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
||||||
|
|
||||||
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
|
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
|
||||||
|
|
||||||
|
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
"inquirer": ["inquirer@13.4.3", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/core": "^11.1.10", "@inquirer/prompts": "^8.4.3", "@inquirer/type": "^4.0.5", "mute-stream": "^3.0.0", "run-async": "^4.0.6", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EPd3IqieHSavSOXh+LZhrIkdQcOELWeRblLT6kslQr+cF9XTh/HxZdSt1YkHH1iq4dvqBnV42uwg2YlorgOy6g=="],
|
"inquirer": ["inquirer@13.4.3", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/core": "^11.1.10", "@inquirer/prompts": "^8.4.3", "@inquirer/type": "^4.0.5", "mute-stream": "^3.0.0", "run-async": "^4.0.6", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EPd3IqieHSavSOXh+LZhrIkdQcOELWeRblLT6kslQr+cF9XTh/HxZdSt1YkHH1iq4dvqBnV42uwg2YlorgOy6g=="],
|
||||||
|
|
||||||
|
"is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
|
||||||
|
|
||||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||||
|
|
||||||
|
"launder": ["launder@1.7.1", "", { "dependencies": { "dayjs": "^1.11.7" } }, "sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw=="],
|
||||||
|
|
||||||
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
|
"lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
|
||||||
@ -112,6 +138,14 @@
|
|||||||
|
|
||||||
"mysql": ["mysql@2.18.1", "", { "dependencies": { "bignumber.js": "9.0.0", "readable-stream": "2.3.7", "safe-buffer": "5.1.2", "sqlstring": "2.3.1" } }, "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig=="],
|
"mysql": ["mysql@2.18.1", "", { "dependencies": { "bignumber.js": "9.0.0", "readable-stream": "2.3.7", "safe-buffer": "5.1.2", "sqlstring": "2.3.1" } }, "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
||||||
|
|
||||||
|
"parse-srcset": ["parse-srcset@1.0.2", "", {}, "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="],
|
||||||
|
|
||||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||||
|
|
||||||
"readable-stream": ["readable-stream@2.3.7", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw=="],
|
"readable-stream": ["readable-stream@2.3.7", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw=="],
|
||||||
@ -124,8 +158,12 @@
|
|||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"sanitize-html": ["sanitize-html@2.17.6", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^12.0.0", "is-plain-object": "^5.0.0", "launder": "^1.7.1", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, "sha512-M4bo9tfv1yfhQZZKkc6dL07ALrGJtfvNOuhX3hU9AVPR/uPQ+nKOJBqTYc7LfMQblTW04mtSWDJWEyLvygJsLA=="],
|
||||||
|
|
||||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"sqlstring": ["sqlstring@2.3.1", "", {}, "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ=="],
|
"sqlstring": ["sqlstring@2.3.1", "", {}, "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ=="],
|
||||||
|
|
||||||
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
@ -137,5 +175,19 @@
|
|||||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
|
|
||||||
|
"sanitize-html/htmlparser2": ["htmlparser2@12.0.0", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "domutils": "^4.0.2", "entities": "^8.0.0" } }, "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw=="],
|
||||||
|
|
||||||
|
"sanitize-html/htmlparser2/domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="],
|
||||||
|
|
||||||
|
"sanitize-html/htmlparser2/domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="],
|
||||||
|
|
||||||
|
"sanitize-html/htmlparser2/domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="],
|
||||||
|
|
||||||
|
"sanitize-html/htmlparser2/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
|
||||||
|
|
||||||
|
"sanitize-html/htmlparser2/domutils/dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,8 @@
|
|||||||
"@types/inquirer": "^9.0.9",
|
"@types/inquirer": "^9.0.9",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/mysql": "^2.15.27",
|
"@types/mysql": "^2.15.27",
|
||||||
"@types/node": "^25.3.3"
|
"@types/node": "^25.3.3",
|
||||||
|
"@types/sanitize-html": "^2.16.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
@ -37,6 +38,7 @@
|
|||||||
"inquirer": "^13.3.2",
|
"inquirer": "^13.3.2",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"mariadb": "^3.5.3",
|
"mariadb": "^3.5.3",
|
||||||
"mysql": "^2.18.1"
|
"mysql": "^2.18.1",
|
||||||
|
"sanitize-html": "^2.17.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type {
|
|||||||
SQLInsertGenReturn,
|
SQLInsertGenReturn,
|
||||||
} from "../../types";
|
} from "../../types";
|
||||||
import sqlInsertGenerator from "../../utils/sql-insert-generator";
|
import sqlInsertGenerator from "../../utils/sql-insert-generator";
|
||||||
|
import { sanitizeHtmlFieldsBatch } from "../../utils/sanitize-html-fields";
|
||||||
import grabDuplicateSafeInsertSql from "../grab-duplicate-safe-insert-sql";
|
import grabDuplicateSafeInsertSql from "../grab-duplicate-safe-insert-sql";
|
||||||
|
|
||||||
type Params<
|
type Params<
|
||||||
@ -29,7 +30,13 @@ export default async function DbInsert<
|
|||||||
let sqlObj: SQLInsertGenReturn | null = null;
|
let sqlObj: SQLInsertGenReturn | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const finalData: { [k: string]: any }[] = data.map((d) => ({
|
const sanitizedData = sanitizeHtmlFieldsBatch({
|
||||||
|
table,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalData: { [k: string]: any }[] = sanitizedData.map((d) => ({
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
updated_at: Date.now(),
|
updated_at: Date.now(),
|
||||||
...d,
|
...d,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
ServerQueryParam,
|
ServerQueryParam,
|
||||||
} from "../../types";
|
} from "../../types";
|
||||||
import sqlGenerator from "../../utils/sql-generator";
|
import sqlGenerator from "../../utils/sql-generator";
|
||||||
|
import sanitizeHtmlFields from "../../utils/sanitize-html-fields";
|
||||||
|
|
||||||
type Params<
|
type Params<
|
||||||
Schema extends { [k: string]: any } = { [k: string]: any },
|
Schema extends { [k: string]: any } = { [k: string]: any },
|
||||||
@ -69,9 +70,15 @@ export default async function DbUpdate<
|
|||||||
let sql = ``;
|
let sql = ``;
|
||||||
sql += `UPDATE ${quoteIdentifier(table)} SET`;
|
sql += `UPDATE ${quoteIdentifier(table)} SET`;
|
||||||
|
|
||||||
|
const sanitizedData = sanitizeHtmlFields({
|
||||||
|
table,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
const finalData: { [k: string]: SQLInsertGenValueType } = {
|
const finalData: { [k: string]: SQLInsertGenValueType } = {
|
||||||
updated_at: Date.now(),
|
updated_at: Date.now(),
|
||||||
...data,
|
...sanitizedData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const keys = Object.keys(finalData);
|
const keys = Object.keys(finalData);
|
||||||
|
|||||||
23
src/lib/schema/build-foreign-key-constraint.ts
Normal file
23
src/lib/schema/build-foreign-key-constraint.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
|
||||||
|
export default function buildForeignKeyConstraint(
|
||||||
|
field: BUN_MARIADB_FieldSchemaType,
|
||||||
|
): string {
|
||||||
|
const fk = field.foreignKey!;
|
||||||
|
const constraintName = fk.foreignKeyName
|
||||||
|
? `CONSTRAINT ${MariaDBQuoteGen(fk.foreignKeyName)} `
|
||||||
|
: "";
|
||||||
|
|
||||||
|
let constraint = `${constraintName}FOREIGN KEY (${MariaDBQuoteGen(field.fieldName!)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName!)}(${MariaDBQuoteGen(fk.destinationTableColumnName!)})`;
|
||||||
|
|
||||||
|
if (fk.cascadeDelete) {
|
||||||
|
constraint += " ON DELETE CASCADE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fk.cascadeUpdate) {
|
||||||
|
constraint += " ON UPDATE CASCADE";
|
||||||
|
}
|
||||||
|
|
||||||
|
return constraint;
|
||||||
|
}
|
||||||
13
src/lib/schema/build-table-options.ts
Normal file
13
src/lib/schema/build-table-options.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType } from "../../types";
|
||||||
|
|
||||||
|
export default function buildTableOptions(
|
||||||
|
table: BUN_MARIADB_TableSchemaType,
|
||||||
|
): string {
|
||||||
|
const options = ["ENGINE=InnoDB"];
|
||||||
|
|
||||||
|
if (table.collation) {
|
||||||
|
options.push("DEFAULT CHARSET=utf8mb4", `COLLATE ${table.collation}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ` ${options.join(" ")}`;
|
||||||
|
}
|
||||||
@ -10,14 +10,12 @@ export default async function createDBSchema(params: CreateDBSchemaParams) {
|
|||||||
await createDBManagerTable(params);
|
await createDBManagerTable(params);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorder Tables
|
* Reorder Tables (parents before children with FKs)
|
||||||
*/
|
*/
|
||||||
const ordered_db_schema = await orderDBSchema(params);
|
const ordered_db_schema = await orderDBSchema(params);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Tables
|
* Handle Tables (create / update / drop)
|
||||||
*/
|
*/
|
||||||
await handleDBSchemaTables({ ...params, db_schema: ordered_db_schema });
|
await handleDBSchemaTables({ ...params, db_schema: ordered_db_schema });
|
||||||
|
|
||||||
process.exit();
|
|
||||||
}
|
}
|
||||||
|
|||||||
66
src/lib/schema/create-table.ts
Normal file
66
src/lib/schema/create-table.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import type {
|
||||||
|
BUN_MARIADB_TableSchemaType,
|
||||||
|
BunMariaDBConfig,
|
||||||
|
} from "../../types";
|
||||||
|
import buildColumnDefinition from "./build-column-definition";
|
||||||
|
import buildForeignKeyConstraint from "./build-foreign-key-constraint";
|
||||||
|
import buildTableOptions from "./build-table-options";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery from "./run-schema-query";
|
||||||
|
|
||||||
|
export default async function createTable({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (!table.tableName.match(/_temp_\d+$/)) {
|
||||||
|
console.log(`Creating table: ${table.tableName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnDefinitions: string[] = [];
|
||||||
|
const foreignKeys: string[] = [];
|
||||||
|
const primaryKeys: string[] = [];
|
||||||
|
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
columnDefinitions.push(buildColumnDefinition(field));
|
||||||
|
|
||||||
|
if (field.primaryKey && field.fieldName) {
|
||||||
|
primaryKeys.push(field.fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.foreignKey && !table.isVector) {
|
||||||
|
foreignKeys.push(buildForeignKeyConstraint(field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primaryKeys.length > 0) {
|
||||||
|
const pkCols = primaryKeys.map((k) => MariaDBQuoteGen(k)).join(", ");
|
||||||
|
columnDefinitions.push(`PRIMARY KEY (${pkCols})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table.uniqueConstraints) {
|
||||||
|
for (const constraint of table.uniqueConstraints) {
|
||||||
|
if (
|
||||||
|
constraint.constraintTableFields &&
|
||||||
|
constraint.constraintTableFields.length > 0
|
||||||
|
) {
|
||||||
|
const fields = constraint.constraintTableFields
|
||||||
|
.map((field) => MariaDBQuoteGen(field.value))
|
||||||
|
.join(", ");
|
||||||
|
const constraintName =
|
||||||
|
constraint.constraintName ||
|
||||||
|
`unique_${fields.replace(/`/g, "")}`;
|
||||||
|
|
||||||
|
columnDefinitions.push(
|
||||||
|
`CONSTRAINT ${MariaDBQuoteGen(constraintName)} UNIQUE (${fields})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(table.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${buildTableOptions(table)}`;
|
||||||
|
|
||||||
|
await runSchemaQuery({ query: sql, config });
|
||||||
|
}
|
||||||
34
src/lib/schema/get-table-columns.ts
Normal file
34
src/lib/schema/get-table-columns.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
import { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
|
||||||
|
export type ColumnInfoRow = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function getTableColumns({
|
||||||
|
tableName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<ColumnInfoRow[]> {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows<{
|
||||||
|
COLUMN_NAME: string;
|
||||||
|
COLUMN_TYPE: string;
|
||||||
|
COLUMN_COMMENT: string;
|
||||||
|
}>({
|
||||||
|
query: `SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE ${schemaCond.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
name: row.COLUMN_NAME,
|
||||||
|
type: row.COLUMN_TYPE,
|
||||||
|
comment: row.COLUMN_COMMENT,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@ -1,13 +1,14 @@
|
|||||||
import { AppData } from "../../data/app-data";
|
import type { CreateDBSchemaTableHandlerParams } from "../../types";
|
||||||
import type {
|
import createTable from "./create-table";
|
||||||
BUN_MARIADB_DB_TABLE_MANAGER_TABLE,
|
|
||||||
BUN_MARIADB_INFORMATION_SCHEMA_TABLES,
|
|
||||||
CreateDBSchemaTableHandlerParams,
|
|
||||||
} from "../../types";
|
|
||||||
import dbHandler from "../db-handler";
|
|
||||||
import DbInsert from "../mariadb/db-insert";
|
|
||||||
import buildColumnDefinition from "./build-column-definition";
|
|
||||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import resolveTable from "./resolve-table";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
import syncIndexes from "./sync-indexes";
|
||||||
|
import updateTable from "./update-table";
|
||||||
|
import upsertDbManagerTable, {
|
||||||
|
removeDbManagerTable,
|
||||||
|
} from "./upsert-db-manager-table";
|
||||||
|
|
||||||
export default async function handleDBSchemaTable({
|
export default async function handleDBSchemaTable({
|
||||||
db_schema,
|
db_schema,
|
||||||
@ -16,31 +17,64 @@ export default async function handleDBSchemaTable({
|
|||||||
db_manager_table_name,
|
db_manager_table_name,
|
||||||
existing_live_table,
|
existing_live_table,
|
||||||
}: CreateDBSchemaTableHandlerParams) {
|
}: CreateDBSchemaTableHandlerParams) {
|
||||||
if (!db_manager_table_name || !existing_live_table?.TABLE_NAME) {
|
const resolvedTable = resolveTable(table, db_schema);
|
||||||
const insert_table = await DbInsert<BUN_MARIADB_DB_TABLE_MANAGER_TABLE>(
|
|
||||||
{
|
const schemaCond = schemaCondition(config);
|
||||||
data: [
|
const liveTables = await querySchemaRows<{ TABLE_NAME: string }>({
|
||||||
{
|
query: `SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_TYPE = 'BASE TABLE'`,
|
||||||
table_name: table.tableName,
|
values: schemaCond.values,
|
||||||
},
|
|
||||||
],
|
|
||||||
table: AppData["DbSchemaManagerTableName"],
|
|
||||||
config,
|
config,
|
||||||
},
|
});
|
||||||
|
const liveTableNames = liveTables.map((t) => t.TABLE_NAME);
|
||||||
|
|
||||||
|
let tableExistsTracked = Boolean(db_manager_table_name);
|
||||||
|
let tableExistsLive = Boolean(
|
||||||
|
existing_live_table?.TABLE_NAME ||
|
||||||
|
liveTableNames.includes(resolvedTable.tableName),
|
||||||
);
|
);
|
||||||
|
let wasRenamed = false;
|
||||||
|
|
||||||
let sql = ``;
|
if (
|
||||||
|
resolvedTable.tableNameOld &&
|
||||||
sql += `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(table.tableName)}`;
|
resolvedTable.tableNameOld !== resolvedTable.tableName
|
||||||
sql += ` (`;
|
) {
|
||||||
|
if (liveTableNames.includes(resolvedTable.tableNameOld)) {
|
||||||
for (let i = 0; i < table.fields.length; i++) {
|
console.log(
|
||||||
const field = table.fields[i];
|
`Renaming table: ${resolvedTable.tableNameOld} -> ${resolvedTable.tableName}`,
|
||||||
if (!field) continue;
|
);
|
||||||
const col_def = buildColumnDefinition(field);
|
await runSchemaQuery({
|
||||||
sql += ` ${col_def}`;
|
query: `RENAME TABLE ${MariaDBQuoteGen(resolvedTable.tableNameOld)} TO ${MariaDBQuoteGen(resolvedTable.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await upsertDbManagerTable({
|
||||||
|
tableName: resolvedTable.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await removeDbManagerTable({
|
||||||
|
tableName: resolvedTable.tableNameOld,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
tableExistsTracked = true;
|
||||||
|
tableExistsLive = true;
|
||||||
|
wasRenamed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += ` )`;
|
if (!tableExistsTracked && !tableExistsLive) {
|
||||||
|
await createTable({ table: resolvedTable, config });
|
||||||
|
await upsertDbManagerTable({
|
||||||
|
tableName: resolvedTable.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!wasRenamed) {
|
||||||
|
await updateTable({ table: resolvedTable, config });
|
||||||
}
|
}
|
||||||
|
await upsertDbManagerTable({
|
||||||
|
tableName: resolvedTable.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncIndexes({ table: resolvedTable, config });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
|
import _ from "lodash";
|
||||||
import { AppData } from "../../data/app-data";
|
import { AppData } from "../../data/app-data";
|
||||||
import type {
|
import type {
|
||||||
BUN_MARIADB_INFORMATION_SCHEMA_TABLES,
|
BUN_MARIADB_INFORMATION_SCHEMA_TABLES,
|
||||||
CreateDBSchemaParams,
|
CreateDBSchemaParams,
|
||||||
} from "../../types";
|
} from "../../types";
|
||||||
import dbHandler from "../db-handler";
|
|
||||||
import getExistingTablesFromTablesManagerTable from "./get-existing-tables-from-tables-manager-table";
|
|
||||||
import handleDBSchemaTable from "./handle-db-schema-table";
|
import handleDBSchemaTable from "./handle-db-schema-table";
|
||||||
|
import getExistingTablesFromTablesManagerTable from "./get-existing-tables-from-tables-manager-table";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
import { removeDbManagerTable } from "./upsert-db-manager-table";
|
||||||
|
|
||||||
export default async function handleDBSchemaTables(
|
export default async function handleDBSchemaTables(
|
||||||
params: CreateDBSchemaParams,
|
params: CreateDBSchemaParams,
|
||||||
@ -19,34 +23,26 @@ export default async function handleDBSchemaTables(
|
|||||||
const existing_schema_tables =
|
const existing_schema_tables =
|
||||||
await getExistingTablesFromTablesManagerTable(params);
|
await getExistingTablesFromTablesManagerTable(params);
|
||||||
|
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
const existing_live_tables =
|
const existing_live_tables =
|
||||||
await dbHandler<BUN_MARIADB_INFORMATION_SCHEMA_TABLES>({
|
await querySchemaRows<BUN_MARIADB_INFORMATION_SCHEMA_TABLES>({
|
||||||
query: `SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME != ?`,
|
query: `SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME != ?`,
|
||||||
config: params.config,
|
values: [...schemaCond.values, AppData["DbSchemaManagerTableName"]],
|
||||||
values: [AppData["DbSchemaManagerTableName"]],
|
config,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < db_schema.tables.length; i++) {
|
for (let i = 0; i < db_schema.tables.length; i++) {
|
||||||
const table = db_schema.tables[i];
|
const table = db_schema.tables[i];
|
||||||
|
if (!table) continue;
|
||||||
|
|
||||||
const existing_table = existing_schema_tables.find(
|
const existing_table = existing_schema_tables.find(
|
||||||
(t) => t == table?.tableName,
|
(t) => t == table.tableName,
|
||||||
);
|
);
|
||||||
|
|
||||||
const existing_live_table = existing_live_tables.payload?.find(
|
const existing_live_table = existing_live_tables.find(
|
||||||
(t) => t.TABLE_NAME == table?.tableName,
|
(t) => t.TABLE_NAME == table.tableName,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!table) {
|
|
||||||
if (existing_live_table?.TABLE_NAME) {
|
|
||||||
await dbHandler<BUN_MARIADB_INFORMATION_SCHEMA_TABLES>({
|
|
||||||
query: `DROP TABLE ${existing_live_table.TABLE_NAME}`,
|
|
||||||
config: params.config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleDBSchemaTable({
|
await handleDBSchemaTable({
|
||||||
...params,
|
...params,
|
||||||
table,
|
table,
|
||||||
@ -54,4 +50,84 @@ export default async function handleDBSchemaTables(
|
|||||||
existing_live_table,
|
existing_live_table,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop tables tracked by the manager but no longer in the schema.
|
||||||
|
* Skip drops when remaining schema tables still reference the table via FK
|
||||||
|
* (e.g. external tables like `users` that are referenced but not managed).
|
||||||
|
*/
|
||||||
|
const schemaTableNames = db_schema.tables.map((t) => t.tableName);
|
||||||
|
const tablesToDrop = _.uniq(
|
||||||
|
existing_schema_tables.filter(
|
||||||
|
(tableName): tableName is string =>
|
||||||
|
Boolean(tableName) && !schemaTableNames.includes(tableName!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tablesToDrop.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fkRows = await querySchemaRows<{
|
||||||
|
TABLE_NAME: string;
|
||||||
|
REFERENCED_TABLE_NAME: string;
|
||||||
|
}>({
|
||||||
|
query: `SELECT TABLE_NAME, REFERENCED_TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE ${schemaCond.where} AND REFERENCED_TABLE_NAME IS NOT NULL`,
|
||||||
|
values: schemaCond.values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const referencedByRemaining = new Map<string, string[]>();
|
||||||
|
for (const row of fkRows) {
|
||||||
|
if (
|
||||||
|
!row.REFERENCED_TABLE_NAME ||
|
||||||
|
!tablesToDrop.includes(row.REFERENCED_TABLE_NAME)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Referenced by a table we are keeping
|
||||||
|
if (
|
||||||
|
schemaTableNames.includes(row.TABLE_NAME) ||
|
||||||
|
!tablesToDrop.includes(row.TABLE_NAME)
|
||||||
|
) {
|
||||||
|
const list = referencedByRemaining.get(row.REFERENCED_TABLE_NAME) || [];
|
||||||
|
if (!list.includes(row.TABLE_NAME)) {
|
||||||
|
list.push(row.TABLE_NAME);
|
||||||
|
}
|
||||||
|
referencedByRemaining.set(row.REFERENCED_TABLE_NAME, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeToDrop: string[] = [];
|
||||||
|
|
||||||
|
for (const tableName of tablesToDrop) {
|
||||||
|
const dependents = referencedByRemaining.get(tableName);
|
||||||
|
if (dependents && dependents.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
`Skipping drop of table \`${tableName}\`: still referenced by ${dependents.join(", ")}. Removing from schema manager tracking only.`,
|
||||||
|
);
|
||||||
|
await removeDbManagerTable({ tableName, config });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
safeToDrop.push(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (safeToDrop.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
||||||
|
try {
|
||||||
|
for (const tableName of safeToDrop) {
|
||||||
|
console.log(`Dropping table: ${tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP TABLE IF EXISTS ${MariaDBQuoteGen(tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await removeDbManagerTable({ tableName, config });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
92
src/lib/schema/recreate-table.ts
Normal file
92
src/lib/schema/recreate-table.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import type {
|
||||||
|
BUN_MARIADB_TableSchemaType,
|
||||||
|
BunMariaDBConfig,
|
||||||
|
} from "../../types";
|
||||||
|
import createTable from "./create-table";
|
||||||
|
import getTableColumns from "./get-table-columns";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
|
||||||
|
async function checkIfTableExists({
|
||||||
|
tableName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows<{ table_exists: number }>({
|
||||||
|
query: `SELECT 1 AS \`table_exists\` FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_NAME = ? LIMIT 1`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(rows[0]?.table_exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function recreateTable({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const doesTableExist = await checkIfTableExists({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!doesTableExist) {
|
||||||
|
await createTable({ table, config });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
|
||||||
|
const backupOldTableName = `${table.tableName}_old_${Date.now()}`;
|
||||||
|
const existingColumns = await getTableColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columnsToKeep = (table.fields || [])
|
||||||
|
.filter((field) =>
|
||||||
|
existingColumns.some((column) => column.name === field.fieldName),
|
||||||
|
)
|
||||||
|
.map((field) => field.fieldName)
|
||||||
|
.filter((fieldName): fieldName is string => Boolean(fieldName));
|
||||||
|
|
||||||
|
await createTable({
|
||||||
|
table: { ...table, tableName: tempTableName },
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (columnsToKeep.length > 0) {
|
||||||
|
const columnList = columnsToKeep
|
||||||
|
.map((column) => MariaDBQuoteGen(column))
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `INSERT INTO ${MariaDBQuoteGen(tempTableName)} (${columnList}) SELECT ${columnList} FROM ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
||||||
|
try {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `RENAME TABLE ${MariaDBQuoteGen(table.tableName)} TO ${MariaDBQuoteGen(backupOldTableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `RENAME TABLE ${MariaDBQuoteGen(tempTableName)} TO ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP TABLE ${MariaDBQuoteGen(backupOldTableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/lib/schema/resolve-table.ts
Normal file
56
src/lib/schema/resolve-table.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import type {
|
||||||
|
BUN_MARIADB_DatabaseSchemaType,
|
||||||
|
BUN_MARIADB_FieldSchemaType,
|
||||||
|
BUN_MARIADB_TableSchemaType,
|
||||||
|
} from "../../types";
|
||||||
|
|
||||||
|
export default function resolveTable(
|
||||||
|
table: BUN_MARIADB_TableSchemaType,
|
||||||
|
db_schema: BUN_MARIADB_DatabaseSchemaType,
|
||||||
|
): BUN_MARIADB_TableSchemaType {
|
||||||
|
if (!table.parentTableName) {
|
||||||
|
return _.cloneDeep(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentTable = db_schema.tables.find(
|
||||||
|
(schemaTable) => schemaTable.tableName === table.parentTableName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!parentTable) {
|
||||||
|
throw new Error(
|
||||||
|
`Parent table \`${table.parentTableName}\` not found for \`${table.tableName}\``,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedFieldsMap = new Map<string, BUN_MARIADB_FieldSchemaType>();
|
||||||
|
|
||||||
|
(parentTable.fields || []).forEach((f) => {
|
||||||
|
if (f.fieldName) mergedFieldsMap.set(f.fieldName, _.cloneDeep(f));
|
||||||
|
});
|
||||||
|
|
||||||
|
(table.fields || []).forEach((f) => {
|
||||||
|
if (f.fieldName) {
|
||||||
|
const existing = mergedFieldsMap.get(f.fieldName) || {};
|
||||||
|
mergedFieldsMap.set(f.fieldName, _.merge({}, existing, f));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
..._.cloneDeep(parentTable),
|
||||||
|
tableName: table.tableName,
|
||||||
|
tableDescription: table.tableDescription || parentTable.tableDescription,
|
||||||
|
collation: table.collation || parentTable.collation,
|
||||||
|
isVector:
|
||||||
|
table.isVector !== undefined ? table.isVector : parentTable.isVector,
|
||||||
|
fields: Array.from(mergedFieldsMap.values()),
|
||||||
|
indexes: _.uniqBy(
|
||||||
|
[...(parentTable.indexes || []), ...(table.indexes || [])],
|
||||||
|
"indexName",
|
||||||
|
),
|
||||||
|
uniqueConstraints: [
|
||||||
|
...(parentTable.uniqueConstraints || []),
|
||||||
|
...(table.uniqueConstraints || []),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
48
src/lib/schema/run-schema-query.ts
Normal file
48
src/lib/schema/run-schema-query.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
import dbHandler from "../db-handler";
|
||||||
|
|
||||||
|
export default async function runSchemaQuery({
|
||||||
|
query,
|
||||||
|
values,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
values?: any[];
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const res = await dbHandler({
|
||||||
|
query,
|
||||||
|
values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.success) {
|
||||||
|
throw new Error(
|
||||||
|
`Database query failed: ${query} ... ERROR: ${res.error || res.msg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function querySchemaRows<T extends Record<string, any>>({
|
||||||
|
query,
|
||||||
|
values,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
values?: any[];
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<T[]> {
|
||||||
|
const res = await dbHandler<T>({
|
||||||
|
query,
|
||||||
|
values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.success) {
|
||||||
|
throw new Error(
|
||||||
|
`Database query failed: ${query} ... ERROR: ${res.error || res.msg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (res.payload || []) as T[];
|
||||||
|
}
|
||||||
20
src/lib/schema/schema-condition.ts
Normal file
20
src/lib/schema/schema-condition.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
|
||||||
|
export default function schemaCondition(config?: BunMariaDBConfig): {
|
||||||
|
where: string;
|
||||||
|
values: string[];
|
||||||
|
} {
|
||||||
|
const databaseName = config?.db_name || global.CONFIG?.db_name;
|
||||||
|
|
||||||
|
if (databaseName) {
|
||||||
|
return {
|
||||||
|
where: "TABLE_SCHEMA = ?",
|
||||||
|
values: [databaseName],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
where: "TABLE_SCHEMA = DATABASE()",
|
||||||
|
values: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
165
src/lib/schema/sync-indexes.ts
Normal file
165
src/lib/schema/sync-indexes.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import type {
|
||||||
|
BUN_MARIADB_TableSchemaType,
|
||||||
|
BunMariaDBConfig,
|
||||||
|
} from "../../types";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
|
||||||
|
export default async function syncIndexes({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows<{
|
||||||
|
INDEX_NAME: string;
|
||||||
|
COLUMN_NAME: string;
|
||||||
|
INDEX_TYPE: string;
|
||||||
|
}>({
|
||||||
|
query: `SELECT INDEX_NAME, COLUMN_NAME, INDEX_TYPE FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY' ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indexes required by foreign keys / unique constraints cannot be dropped
|
||||||
|
* freely. Skip those when cleaning up schema indexes.
|
||||||
|
*/
|
||||||
|
const protectedConstraintRows = await querySchemaRows<{
|
||||||
|
CONSTRAINT_NAME: string;
|
||||||
|
CONSTRAINT_TYPE: string;
|
||||||
|
}>({
|
||||||
|
query: `SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND CONSTRAINT_TYPE IN ('FOREIGN KEY', 'UNIQUE')`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const protectedIndexNames = new Set(
|
||||||
|
protectedConstraintRows.map((r) => r.CONSTRAINT_NAME),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Column-level UNIQUE creates an index often named after the column
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
if (field.unique && field.fieldName) {
|
||||||
|
protectedIndexNames.add(field.fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const constraint of table.uniqueConstraints || []) {
|
||||||
|
if (constraint.constraintName) {
|
||||||
|
protectedIndexNames.add(constraint.constraintName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingIndexesMap = new Map<
|
||||||
|
string,
|
||||||
|
{ columns: string[]; type: string }
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!existingIndexesMap.has(row.INDEX_NAME)) {
|
||||||
|
existingIndexesMap.set(row.INDEX_NAME, {
|
||||||
|
columns: [],
|
||||||
|
type: row.INDEX_TYPE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
existingIndexesMap.get(row.INDEX_NAME)!.columns.push(row.COLUMN_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [indexName, details] of existingIndexesMap.entries()) {
|
||||||
|
if (protectedIndexNames.has(indexName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaIndex = table.indexes?.find((i) => i.indexName === indexName);
|
||||||
|
|
||||||
|
if (!schemaIndex) {
|
||||||
|
console.log(`Dropping index: ${indexName}`);
|
||||||
|
try {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
if (
|
||||||
|
String(err?.message || "").includes(
|
||||||
|
"needed in a foreign key constraint",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`Skipping drop of index ${indexName}: required by a foreign key constraint`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const schemaColumns = schemaIndex.indexTableFields || [];
|
||||||
|
const columnsMatch =
|
||||||
|
details.columns.length === schemaColumns.length &&
|
||||||
|
details.columns.every((col, idx) => col === schemaColumns[idx]);
|
||||||
|
|
||||||
|
if (!columnsMatch) {
|
||||||
|
console.log(`Recreating changed index: ${indexName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
existingIndexesMap.delete(indexName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const index of table.indexes || []) {
|
||||||
|
if (
|
||||||
|
!index.indexName ||
|
||||||
|
!index.indexTableFields ||
|
||||||
|
index.indexTableFields.length === 0
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingIndexesMap.has(index.indexName)) {
|
||||||
|
const isVectorIndex =
|
||||||
|
table.isVector ||
|
||||||
|
table.fields?.some(
|
||||||
|
(f) =>
|
||||||
|
f.fieldName === index.indexTableFields![0] && f.isVector,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isVectorIndex) {
|
||||||
|
console.log(`Creating Vector index: ${index.indexName}`);
|
||||||
|
const targetField = MariaDBQuoteGen(index.indexTableFields[0]!);
|
||||||
|
const distanceMetric =
|
||||||
|
(index.indexType as string)?.toLowerCase() === "cosine"
|
||||||
|
? "cosine"
|
||||||
|
: "euclidean";
|
||||||
|
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD VECTOR INDEX ${MariaDBQuoteGen(index.indexName)} (${targetField}) M=8 DISTANCE=${distanceMetric}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`Creating standard index: ${index.indexName}`);
|
||||||
|
const fields = index.indexTableFields
|
||||||
|
.map((field) => MariaDBQuoteGen(field))
|
||||||
|
.join(", ");
|
||||||
|
const typeUpper = index.indexType?.toUpperCase();
|
||||||
|
const isSpecialType =
|
||||||
|
typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
|
||||||
|
const indexPrefix = isSpecialType ? `${typeUpper} ` : "";
|
||||||
|
const indexSuffix =
|
||||||
|
!isSpecialType &&
|
||||||
|
(typeUpper === "BTREE" || typeUpper === "HASH")
|
||||||
|
? ` USING ${typeUpper}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `CREATE ${indexPrefix}INDEX ${MariaDBQuoteGen(index.indexName)} ON ${MariaDBQuoteGen(table.tableName)} (${fields})${indexSuffix}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/lib/schema/update-table.ts
Normal file
225
src/lib/schema/update-table.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import type {
|
||||||
|
BUN_MARIADB_FieldSchemaType,
|
||||||
|
BUN_MARIADB_TableSchemaType,
|
||||||
|
BunMariaDBConfig,
|
||||||
|
} from "../../types";
|
||||||
|
import buildColumnDefinition from "./build-column-definition";
|
||||||
|
import createTable from "./create-table";
|
||||||
|
import getTableColumns from "./get-table-columns";
|
||||||
|
import mapDataType from "./map-data-types";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import recreateTable from "./recreate-table";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare live COLUMN_TYPE with schema-mapped type.
|
||||||
|
* Live types often include display widths (e.g. bigint(20) vs BIGINT).
|
||||||
|
*/
|
||||||
|
function columnTypesMatch(liveType: string, expectedType: string): boolean {
|
||||||
|
const live = liveType.toLowerCase().replace(/\s+/g, "");
|
||||||
|
const expected = expectedType.toLowerCase().replace(/\s+/g, "");
|
||||||
|
|
||||||
|
if (live === expected) return true;
|
||||||
|
// live may include display width: bigint(20) vs bigint
|
||||||
|
if (live.startsWith(`${expected}(`)) return true;
|
||||||
|
// expected may include length live omits in some versions
|
||||||
|
if (expected.startsWith(`${live}(`)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addColumn({
|
||||||
|
tableName,
|
||||||
|
field,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
field: BUN_MARIADB_FieldSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
console.log(`Adding column: ${tableName}.${field.fieldName}`);
|
||||||
|
const columnDef = buildColumnDefinition(field).trim();
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} ADD COLUMN IF NOT EXISTS ${columnDef}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function modifyColumn({
|
||||||
|
tableName,
|
||||||
|
field,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
field: BUN_MARIADB_FieldSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
console.log(`Modifying column: ${tableName}.${field.fieldName}`);
|
||||||
|
const columnDef = buildColumnDefinition(field).trim();
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} MODIFY COLUMN ${columnDef}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dropColumn({
|
||||||
|
tableName,
|
||||||
|
fieldName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
fieldName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
console.log(`Dropping column: ${tableName}.${fieldName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} DROP COLUMN ${MariaDBQuoteGen(fieldName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function updateTable({
|
||||||
|
table,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const existingColumns = await getTableColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingColumns.length === 0) {
|
||||||
|
await createTable({ table, config });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveFieldsMap = new Map(
|
||||||
|
existingColumns.map((col) => [
|
||||||
|
col.name,
|
||||||
|
{ type: col.type.toLowerCase(), comment: col.comment || "" },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const codeFieldsMap = new Map(
|
||||||
|
(table.fields || []).map((f) => [f.fieldName, f]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldsToAdd: BUN_MARIADB_FieldSchemaType[] = [];
|
||||||
|
const fieldsToModify: BUN_MARIADB_FieldSchemaType[] = [];
|
||||||
|
const fieldsToDrop: string[] = [];
|
||||||
|
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
if (!field.fieldName) continue;
|
||||||
|
|
||||||
|
const liveField = liveFieldsMap.get(field.fieldName);
|
||||||
|
|
||||||
|
if (!liveField) {
|
||||||
|
fieldsToAdd.push(field);
|
||||||
|
} else {
|
||||||
|
let typeDiverged = !columnTypesMatch(
|
||||||
|
liveField.type,
|
||||||
|
mapDataType(field),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (field.isVector || field.dataType === "VECTOR") {
|
||||||
|
const dimensions = field.vectorSize || 1536;
|
||||||
|
const expectedNativeToken = `vector(${dimensions})`;
|
||||||
|
typeDiverged = liveField.type.toLowerCase() !== expectedNativeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeDiverged) {
|
||||||
|
fieldsToModify.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const col of existingColumns) {
|
||||||
|
if (!codeFieldsMap.has(col.name)) {
|
||||||
|
fieldsToDrop.push(col.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
fieldsToAdd.length === 0 &&
|
||||||
|
fieldsToModify.length === 0 &&
|
||||||
|
fieldsToDrop.length === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Surgically updating table structure from database layout: ${table.tableName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fieldsToDrop.length > 0) {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
|
||||||
|
const pkRows = await querySchemaRows<{ COLUMN_NAME: string }>({
|
||||||
|
query: `SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE ${schemaCond.where} AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const pkColumnNames = pkRows.map((r) => r.COLUMN_NAME);
|
||||||
|
|
||||||
|
if (fieldsToDrop.some((f) => pkColumnNames.includes(f))) {
|
||||||
|
console.log(
|
||||||
|
`Dropping primary key because a PK column is being dropped`,
|
||||||
|
);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP PRIMARY KEY`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexRows = await querySchemaRows<{
|
||||||
|
INDEX_NAME: string;
|
||||||
|
COLUMN_NAME: string;
|
||||||
|
}>({
|
||||||
|
query: `SELECT INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY'`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
const indexesToDrop = new Set<string>();
|
||||||
|
for (const row of indexRows) {
|
||||||
|
if (fieldsToDrop.includes(row.COLUMN_NAME)) {
|
||||||
|
indexesToDrop.add(row.INDEX_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const indexName of indexesToDrop) {
|
||||||
|
console.log(
|
||||||
|
`Dropping index ${indexName} because it contains a dropped column`,
|
||||||
|
);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of fieldsToAdd) {
|
||||||
|
await addColumn({ tableName: table.tableName, field, config });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of fieldsToModify) {
|
||||||
|
try {
|
||||||
|
await modifyColumn({ tableName: table.tableName, field, config });
|
||||||
|
} catch (err: any) {
|
||||||
|
if (field.isVector || field.dataType === "VECTOR") {
|
||||||
|
console.warn(
|
||||||
|
`[Vector Resize] Re-aligning dimension spaces natively via safe migration schema rebuild.`,
|
||||||
|
);
|
||||||
|
await recreateTable({ table, config });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fieldName of fieldsToDrop) {
|
||||||
|
await dropColumn({ tableName: table.tableName, fieldName, config });
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/lib/schema/upsert-db-manager-table.ts
Normal file
33
src/lib/schema/upsert-db-manager-table.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { AppData } from "../../data/app-data";
|
||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery from "./run-schema-query";
|
||||||
|
|
||||||
|
export default async function upsertDbManagerTable({
|
||||||
|
tableName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `INSERT INTO ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} (table_name, created_at, updated_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)`,
|
||||||
|
values: [tableName, now, now],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeDbManagerTable({
|
||||||
|
tableName,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void> {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DELETE FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} WHERE table_name = ?`,
|
||||||
|
values: [tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -120,7 +120,6 @@ export interface BUN_MARIADB_ChildrenTablesType {
|
|||||||
*/
|
*/
|
||||||
export const TextFieldTypesArray = [
|
export const TextFieldTypesArray = [
|
||||||
{ title: "Plain Text", value: "plain" },
|
{ title: "Plain Text", value: "plain" },
|
||||||
{ title: "Rich Text", value: "richText" },
|
|
||||||
{ title: "Markdown", value: "markdown" },
|
{ title: "Markdown", value: "markdown" },
|
||||||
{ title: "JSON", value: "json" },
|
{ title: "JSON", value: "json" },
|
||||||
{ title: "YAML", value: "yaml" },
|
{ title: "YAML", value: "yaml" },
|
||||||
@ -1544,6 +1543,21 @@ export type BunMariaDBConfig = {
|
|||||||
* File path to the SSL certificate
|
* File path to the SSL certificate
|
||||||
*/
|
*/
|
||||||
ssl_ca?: string;
|
ssl_ca?: string;
|
||||||
|
/**
|
||||||
|
* Extra HTML sanitization allowlists appended to the defaults
|
||||||
|
* when inserting/updating fields marked `html: true`.
|
||||||
|
*/
|
||||||
|
html_sanitize?: {
|
||||||
|
/**
|
||||||
|
* Additional tags to allow (merged with built-in defaults).
|
||||||
|
*/
|
||||||
|
allowed_tags?: string[];
|
||||||
|
/**
|
||||||
|
* Additional attributes to allow per tag (merged with built-in defaults).
|
||||||
|
* Values for each tag are appended to any existing allowed attributes.
|
||||||
|
*/
|
||||||
|
allowed_attributes?: Record<string, string[]>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
99
src/utils/sanitize-html-fields.ts
Normal file
99
src/utils/sanitize-html-fields.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import sanitizeHtml from "sanitize-html";
|
||||||
|
import type {
|
||||||
|
BUN_MARIADB_DatabaseSchemaType,
|
||||||
|
BUN_MARIADB_FieldSchemaType,
|
||||||
|
BunMariaDBConfig,
|
||||||
|
} from "../types";
|
||||||
|
import { readLiveSchema } from "../functions/live-schema";
|
||||||
|
import getSanitizeHtmlOptions from "./sanitize-html-options";
|
||||||
|
|
||||||
|
function grabTableFields(tableName: string): BUN_MARIADB_FieldSchemaType[] {
|
||||||
|
const dbSchema: BUN_MARIADB_DatabaseSchemaType | undefined =
|
||||||
|
global.DB_SCHEMA || readLiveSchema();
|
||||||
|
|
||||||
|
const tableSchema = dbSchema?.tables?.find((t) => t.tableName === tableName);
|
||||||
|
|
||||||
|
return tableSchema?.fields || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only fields with an explicit `html: true` flag are sanitized.
|
||||||
|
* Missing / falsy / non-true values are never sanitized.
|
||||||
|
*/
|
||||||
|
function isExplicitHtmlField(field?: BUN_MARIADB_FieldSchemaType): boolean {
|
||||||
|
return field?.html === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeValue(
|
||||||
|
value: unknown,
|
||||||
|
config?: BunMariaDBConfig,
|
||||||
|
): unknown {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizeHtml(value, getSanitizeHtmlOptions(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize string values ONLY for schema fields with explicit `html: true`.
|
||||||
|
* Other fields (including plain text that happens to contain HTML) are left untouched.
|
||||||
|
*/
|
||||||
|
export default function sanitizeHtmlFields<
|
||||||
|
T extends Record<string, any> = Record<string, any>,
|
||||||
|
>({
|
||||||
|
table,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: string;
|
||||||
|
data: T;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): T {
|
||||||
|
const fields = grabTableFields(table);
|
||||||
|
if (fields.length === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlFieldNames = new Set(
|
||||||
|
fields
|
||||||
|
.filter(isExplicitHtmlField)
|
||||||
|
.map((f) => f.fieldName)
|
||||||
|
.filter((name): name is string => Boolean(name)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// No explicitly marked html fields on this table — skip entirely
|
||||||
|
if (htmlFieldNames.size === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = { ...data } as Record<string, any>;
|
||||||
|
const resolvedConfig = config || global.CONFIG;
|
||||||
|
|
||||||
|
for (const key of Object.keys(sanitized)) {
|
||||||
|
// Only sanitize keys that map to fields with html: true
|
||||||
|
if (!htmlFieldNames.has(key)) continue;
|
||||||
|
sanitized[key] = sanitizeValue(sanitized[key], resolvedConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize an array of row objects for insert.
|
||||||
|
*/
|
||||||
|
export function sanitizeHtmlFieldsBatch<
|
||||||
|
T extends Record<string, any> = Record<string, any>,
|
||||||
|
>({
|
||||||
|
table,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
table: string;
|
||||||
|
data: T[];
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): T[] {
|
||||||
|
return data.map((row) =>
|
||||||
|
sanitizeHtmlFields({ table, data: row, config }),
|
||||||
|
);
|
||||||
|
}
|
||||||
86
src/utils/sanitize-html-options.ts
Normal file
86
src/utils/sanitize-html-options.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import type { IOptions } from "sanitize-html";
|
||||||
|
import type { BunMariaDBConfig } from "../types";
|
||||||
|
|
||||||
|
export const defaultSanitizeHtmlOptions: IOptions = {
|
||||||
|
allowedTags: [
|
||||||
|
"b",
|
||||||
|
"i",
|
||||||
|
"em",
|
||||||
|
"strong",
|
||||||
|
"a",
|
||||||
|
"p",
|
||||||
|
"span",
|
||||||
|
"ul",
|
||||||
|
"ol",
|
||||||
|
"li",
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"img",
|
||||||
|
"div",
|
||||||
|
"button",
|
||||||
|
"pre",
|
||||||
|
"code",
|
||||||
|
"br",
|
||||||
|
"hr",
|
||||||
|
"blockquote",
|
||||||
|
"table",
|
||||||
|
"tr",
|
||||||
|
"td",
|
||||||
|
"th",
|
||||||
|
"thead",
|
||||||
|
"tbody",
|
||||||
|
"tfoot",
|
||||||
|
"caption",
|
||||||
|
"colgroup",
|
||||||
|
"col",
|
||||||
|
],
|
||||||
|
allowedAttributes: {
|
||||||
|
a: ["href", "title", "class", "style", "target", "rel"],
|
||||||
|
img: ["src", "alt", "width", "height", "class", "style"],
|
||||||
|
"*": ["style", "class", "title", "id"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function uniqueStrings(values: string[]): string[] {
|
||||||
|
return Array.from(new Set(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build sanitize-html options, appending any tags/attributes from config.
|
||||||
|
*/
|
||||||
|
export default function getSanitizeHtmlOptions(
|
||||||
|
config?: BunMariaDBConfig,
|
||||||
|
): IOptions {
|
||||||
|
const cfg = config || global.CONFIG;
|
||||||
|
const extra = cfg?.html_sanitize;
|
||||||
|
|
||||||
|
const baseTags = defaultSanitizeHtmlOptions.allowedTags || [];
|
||||||
|
const baseAttrs = {
|
||||||
|
...(defaultSanitizeHtmlOptions.allowedAttributes || {}),
|
||||||
|
} as Record<string, string[]>;
|
||||||
|
|
||||||
|
const allowedTags = uniqueStrings([
|
||||||
|
...(Array.isArray(baseTags) ? baseTags : []),
|
||||||
|
...(extra?.allowed_tags || []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allowedAttributes: Record<string, string[]> = { ...baseAttrs };
|
||||||
|
|
||||||
|
for (const [tag, attrs] of Object.entries(
|
||||||
|
extra?.allowed_attributes || {},
|
||||||
|
)) {
|
||||||
|
allowedAttributes[tag] = uniqueStrings([
|
||||||
|
...(allowedAttributes[tag] || []),
|
||||||
|
...attrs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowedTags,
|
||||||
|
allowedAttributes,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user