Compare commits

...
10 Commits
Author SHA1 Message Date
tben 3bbf00cdb0 Update .gitignore, add dist directory 2026-07-20 21:43:05 +01:00
tben 9f2db66760 Updates 2026-07-14 09:34:25 +01:00
tben 225e3eba4c Updates 2026-07-14 09:34:10 +01:00
tben 3faf4be368 Updates 2026-07-14 09:13:07 +01:00
tben 56462e6cc2 updates 2026-07-14 09:10:30 +01:00
tben 79c5b896fe Grok 5 updates 2026-07-14 08:33:44 +01:00
tben 3b5fba4063 Updates 2026-07-14 08:12:13 +01:00
tben b5e4724e03 First working version. 2026-06-24 17:00:14 +01:00
tben 44f6c18a84 Refactor DB Handler. Use Bun native SQL adapter. 2026-06-24 15:52:19 +01:00
tben eb86e1803d First version without typescript errors 2026-06-21 07:05:49 +01:00
201 changed files with 6536 additions and 2810 deletions
+1
View File
@@ -33,3 +33,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.DS_Store .DS_Store
/test /test
.vscode .vscode
.dump
+367 -283
View File
File diff suppressed because it is too large Load Diff
+47 -40
View File
@@ -8,21 +8,22 @@
"@inquirer/prompts": "^8.3.0", "@inquirer/prompts": "^8.3.0",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"commander": "^14.0.3", "commander": "^14.0.3",
"inquirer": "^13.3.2",
"lodash": "^4.17.23", "lodash": "^4.17.23",
"mariadb": "^3.5.3", "sanitize-html": "^2.17.6",
"mysql": "^2.18.1",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/inquirer": "^9.0.9",
"@types/lodash": "^4.17.24", "@types/lodash": "^4.17.24",
"@types/mysql": "^2.15.27",
"@types/node": "^25.3.3", "@types/node": "^25.3.3",
"@types/sanitize-html": "^2.16.1",
"typescript": "~5.8.0",
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5", "typescript": ">=5",
}, },
"optionalPeers": [
"typescript",
],
}, },
}, },
"packages": { "packages": {
@@ -60,19 +61,11 @@
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/inquirer": ["@types/inquirer@9.0.10", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-vFW2WbXwO9eZpRT5GJGFJ/shgyMNnYozmnjakt9jCQSS1lvqX8pZEQMjJ9RdDPct/YxwciQ8+V8OYn9euIrZDA=="],
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="], "@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
"@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="],
"@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/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="], "@types/sanitize-html": ["@types/sanitize-html@2.16.1", "", { "dependencies": { "htmlparser2": "^10.1" } }, "sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA=="],
"bignumber.js": ["bignumber.js@9.0.0", "", {}, "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
@@ -84,9 +77,21 @@
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"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=="],
@@ -94,48 +99,50 @@
"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=="], "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
"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=="], "launder": ["launder@1.7.1", "", { "dependencies": { "dayjs": "^1.11.7" } }, "sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"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=="],
"mariadb": ["mariadb@3.5.3", "", { "dependencies": { "@types/geojson": "^7946.0.16", "@types/node": ">=20", "denque": "^2.1.0", "iconv-lite": "^0.7.2", "lru-cache": "^11.5.0" } }, "sha512-i053Kc0MgdUv/hu9mCyq67TYfPXFj3/MV8I7ZW5wvJNixIyXC0VztMPUjIVj/449nQo+BsxFD4Fdk/sA/uqKPQ=="],
"mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="],
"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=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "parse-srcset": ["parse-srcset@1.0.2", "", {}, "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="],
"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=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"run-async": ["run-async@4.0.6", "", {}, "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ=="], "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=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"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=="],
"sqlstring": ["sqlstring@2.3.1", "", {}, "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"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=="], "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=="],
} }
} }
+1 -5
View File
@@ -1,6 +1,4 @@
import { Command } from "commander"; import { Command } from "commander";
import init from "../../functions/init";
import grabDBDir from "../../utils/grab-db-dir";
import chalk from "chalk"; import chalk from "chalk";
import { select } from "@inquirer/prompts"; import { select } from "@inquirer/prompts";
import listTables from "./list-tables"; import listTables from "./list-tables";
@@ -9,9 +7,7 @@ export default function () {
return new Command("admin") return new Command("admin")
.description("View Tables and Data, Run SQL Queries, Etc.") .description("View Tables and Data, Run SQL Queries, Etc.")
.action(async () => { .action(async () => {
const { config } = await init(); console.log(chalk.bold(chalk.blue("\nBun MariaDB Admin\n")));
const { db_file_path } = grabDBDir({ config });
console.log(chalk.bold(chalk.blue("\nBun SQLite Admin\n")));
try { try {
while (true) { while (true) {
const paradigm = await select({ const paradigm = await select({
+24 -21
View File
@@ -1,19 +1,18 @@
import { Database } from "bun:sqlite";
import chalk from "chalk"; import chalk from "chalk";
import { select } from "@inquirer/prompts"; import { select } from "@inquirer/prompts";
import { AppData } from "../../data/app-data"; import { AppData } from "../../data/app-data";
import showEntries from "./show-entries"; import showEntries from "./show-entries";
import showFields from "./show-fields"; import showFields from "./show-fields";
import dbHandler from "../../lib/db-handler"; import dbHandler from "../../lib/db-handler";
import MariaDBQuoteGen from "../../lib/schema/mariadb-quote-gen";
export default async function listTables(params) { export default async function listTables(params) {
const tables = (await dbHandler({ const tables = (await dbHandler({
query: `SELECT table_name FROM ${AppData["DbSchemaManagerTableName"]}`, query: `SELECT table_name FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])}`,
})).payload; })).payload;
if (!tables?.length) { if (!tables?.length) {
console.log(chalk.yellow("\nNo tables found.\n")); console.log(chalk.yellow("\nNo tables found.\n"));
return; return;
} }
// Level 1: table selection loop
while (true) { while (true) {
const tableName = await select({ const tableName = await select({
message: "Select a table:", message: "Select a table:",
@@ -30,7 +29,6 @@ export default async function listTables(params) {
break; break;
if (tableName === "__exit__") if (tableName === "__exit__")
return "__exit__"; return "__exit__";
// Level 2: action loop — stays here until "Go Back"
while (true) { while (true) {
const action = await select({ const action = await select({
message: `"${tableName}" — choose an action:`, message: `"${tableName}" — choose an action:`,
@@ -56,23 +54,28 @@ export default async function listTables(params) {
if (result === "__exit__") if (result === "__exit__")
return "__exit__"; return "__exit__";
} }
// if (action === "schema") { if (action === "schema") {
// const columns = db const columns = (await dbHandler({
// .query<ColumnInfo, []>(`PRAGMA table_info("${tableName}")`) query: `SELECT ORDINAL_POSITION, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
// .all(); values: [tableName],
// console.log(`\n${chalk.bold(`Schema for "${tableName}":`)} \n`); })).payload;
// console.table( console.log(`\n${chalk.bold(`Schema for "${tableName}":`)} \n`);
// columns.map((c) => ({ if (columns?.length) {
// "#": c.cid, console.table(columns.map((c) => ({
// Name: c.name, "#": c.ORDINAL_POSITION,
// Type: c.type, Name: c.COLUMN_NAME,
// "Not Null": c.notnull ? "YES" : "NO", Type: c.COLUMN_TYPE,
// Default: c.dflt_value ?? "(none)", Nullable: c.IS_NULLABLE,
// "Primary Key": c.pk ? "YES" : "NO", Default: c.COLUMN_DEFAULT ?? "(none)",
// })), Key: c.COLUMN_KEY || "",
// ); Extra: c.EXTRA || "",
// console.log(); })));
// } }
else {
console.log(chalk.yellow("No columns found."));
}
console.log();
}
} }
} }
} }
+7 -8
View File
@@ -1,4 +1,3 @@
import { Database } from "bun:sqlite";
import { input } from "@inquirer/prompts"; import { input } from "@inquirer/prompts";
import chalk from "chalk"; import chalk from "chalk";
import dbHandler from "../../lib/db-handler"; import dbHandler from "../../lib/db-handler";
@@ -9,15 +8,15 @@ export default async function runSQL(params) {
}); });
try { try {
const res = await dbHandler({ query: sql }); const res = await dbHandler({ query: sql });
if (res.payload) { if (res.payload?.length) {
console.log(`\n${chalk.bold(`Result (${res.payload.length} row${res.payload.length !== 1 ? "s" : ""}):`)} \n`); console.log(`\n${chalk.bold(`Result (${res.payload.length} row${res.payload.length !== 1 ? "s" : ""}):`)} \n`);
if (res.payload.length) console.table(res.payload);
console.table(res.payload);
else
console.log(chalk.yellow("No res returned.\n"));
} }
else if (res.single_res) { else if (res.success) {
console.log(chalk.green(`\nSuccess! Affected rows: ${res.single_res.changes}, Last insert ID: ${res.single_res.lastInsertRowid}\n`)); console.log(chalk.green(`\nSuccess! Affected rows: ${res.insert_return?.affected_rows ?? res.count ?? 0}, Last insert ID: ${res.insert_return?.last_insert_id ?? "—"}\n`));
}
else {
console.error(chalk.red(`\nSQL Error: ${res.error || res.msg}\n`));
} }
} }
catch (error) { catch (error) {
+30 -22
View File
@@ -1,31 +1,32 @@
import { Database } from "bun:sqlite";
import chalk from "chalk"; import chalk from "chalk";
import { select, input } from "@inquirer/prompts"; import { select, input } from "@inquirer/prompts";
import dbHandler from "../../lib/db-handler"; import dbHandler from "../../lib/db-handler";
import MariaDBQuoteGen from "../../lib/schema/mariadb-quote-gen";
const LIMIT = 50; const LIMIT = 50;
export default async function showEntries({ tableName }) { export default async function showEntries({ tableName }) {
let page = 0; let page = 0;
let searchField = null; let searchField = null;
let searchTerm = null; let searchTerm = null;
const quotedTable = MariaDBQuoteGen(tableName);
while (true) { while (true) {
const offset = page * LIMIT; const offset = page * LIMIT;
const rows = searchTerm const rows = searchTerm
? (await dbHandler({ ? (await dbHandler({
query: `SELECT * FROM "${tableName}" WHERE "${searchField}" LIKE ? LIMIT ${LIMIT} OFFSET ${offset}`, query: `SELECT * FROM ${quotedTable} WHERE ${MariaDBQuoteGen(searchField)} LIKE ? LIMIT ${LIMIT} OFFSET ${offset}`,
values: [`%${searchTerm}%`], values: [`%${searchTerm}%`],
})).payload })).payload
: (await dbHandler({ : (await dbHandler({
query: `SELECT * FROM "${tableName}" LIMIT ${LIMIT} OFFSET ${offset}`, query: `SELECT * FROM ${quotedTable} LIMIT ${LIMIT} OFFSET ${offset}`,
})).payload; })).payload;
const countRow = searchTerm const countRow = searchTerm
? (await dbHandler({ ? (await dbHandler({
query: `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${searchField}" LIKE ?`, query: `SELECT COUNT(*) as count FROM ${quotedTable} WHERE ${MariaDBQuoteGen(searchField)} LIKE ?`,
values: [`%${searchTerm}%`], values: [`%${searchTerm}%`],
})).payload })).payload
: (await dbHandler({ : (await dbHandler({
query: `SELECT COUNT(*) as count FROM "${tableName}"`, query: `SELECT COUNT(*) as count FROM ${quotedTable}`,
})).payload; })).payload;
const total = countRow?.[0]?.count; const total = Number(countRow?.[0]?.count || 0);
const searchInfo = searchTerm const searchInfo = searchTerm
? chalk.dim(` · searching "${searchField}" = "${searchTerm}"`) ? chalk.dim(` · searching "${searchField}" = "${searchTerm}"`)
: ""; : "";
@@ -34,8 +35,7 @@ export default async function showEntries({ tableName }) {
} }
console.log(`\n${chalk.bold(tableName)} — Page ${page + 1}${searchInfo} (${rows.length} of ${total}):\n`); console.log(`\n${chalk.bold(tableName)} — Page ${page + 1}${searchInfo} (${rows.length} of ${total}):\n`);
if (rows.length) { if (rows.length) {
console.log(rows); console.table(rows);
// if (rows.length) console.table(rows);
} }
else { else {
console.log(chalk.yellow("No rows found.")); console.log(chalk.yellow("No rows found."));
@@ -65,19 +65,27 @@ export default async function showEntries({ tableName }) {
searchTerm = null; searchTerm = null;
page = 0; page = 0;
} }
// if (action === "search") { if (action === "search") {
// const columns = db const columns = (await dbHandler({
// .query<ColumnInfo, []>(`PRAGMA table_info("${tableName}")`) query: `SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
// .all(); values: [tableName],
// searchField = await select({ })).payload;
// message: "Search by field:", if (!columns?.length) {
// choices: columns.map((c) => ({ name: c.name, value: c.name })), console.log(chalk.yellow("No columns found for search."));
// }); continue;
// searchTerm = await input({ }
// message: `Search term for "${searchField}":`, searchField = await select({
// validate: (v) => v.trim().length > 0 || "Cannot be empty", message: "Search by field:",
// }); choices: columns.map((c) => ({
// page = 0; name: c.COLUMN_NAME,
// } value: c.COLUMN_NAME,
})),
});
searchTerm = await input({
message: `Search term for "${searchField}":`,
validate: (v) => v.trim().length > 0 || "Cannot be empty",
});
page = 0;
}
} }
} }
+56 -65
View File
@@ -1,69 +1,60 @@
import { Database } from "bun:sqlite";
import chalk from "chalk"; import chalk from "chalk";
import { select } from "@inquirer/prompts"; import { select } from "@inquirer/prompts";
import dbHandler from "../../lib/db-handler";
export default async function showFields({ tableName, }) { export default async function showFields({ tableName, }) {
// const columns = db const columns = (await dbHandler({
// .query<ColumnInfo, []>(`PRAGMA table_info("${tableName}")`) query: `SELECT ORDINAL_POSITION, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
// .all(); values: [tableName],
// const indexes = db })).payload;
// .query<IndexInfo, []>(`PRAGMA index_list("${tableName}")`) if (!columns?.length) {
// .all(); console.log(chalk.yellow(`\nNo columns found for "${tableName}".\n`));
// const foreignKeys = db return;
// .query<ForeignKey, []>(`PRAGMA foreign_key_list("${tableName}")`) }
// .all(); const indexes = (await dbHandler({
// const indexedFields = new Map<string, { unique: boolean }>(); query: `SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE, INDEX_TYPE FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
// for (const idx of indexes) { values: [tableName],
// const cols = db })).payload;
// .query<IndexColumn, []>(`PRAGMA index_info("${idx.name}")`) const foreignKeys = (await dbHandler({
// .all(); query: `SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL`,
// for (const col of cols) { values: [tableName],
// indexedFields.set(col.name, { unique: idx.unique === 1 }); })).payload;
// } while (true) {
// } const fieldName = await select({
// while (true) { message: `"${tableName}" — select a field:`,
// const fieldName = await select({ choices: [
// message: `"${tableName}" — select a field:`, ...columns.map((c) => ({
// choices: [ name: c.COLUMN_NAME,
// ...columns.map((c) => ({ name: c.name, value: c.name })), value: c.COLUMN_NAME,
// { name: chalk.dim("← Go Back"), value: "__back__" }, })),
// { name: chalk.dim("✕ Exit"), value: "__exit__" }, { name: chalk.dim("← Go Back"), value: "__back__" },
// ], { name: chalk.dim("✕ Exit"), value: "__exit__" },
// }); ],
// if (fieldName === "__back__") break; });
// if (fieldName === "__exit__") return "__exit__"; if (fieldName === "__back__")
// const col = columns.find((c) => c.name === fieldName)!; break;
// const idx = indexedFields.get(fieldName); if (fieldName === "__exit__")
// const fk = foreignKeys.find((f) => f.from === fieldName); return "__exit__";
// console.log(`\n${chalk.bold(`Field: "${fieldName}"`)}\n`); const col = columns.find((c) => c.COLUMN_NAME === fieldName);
// console.log(` ${chalk.dim("Table")} ${tableName}`); const colIndexes = indexes?.filter((i) => i.COLUMN_NAME === fieldName) || [];
// console.log(` ${chalk.dim("Column #")} ${col.cid}`); const fk = foreignKeys?.find((f) => f.COLUMN_NAME === fieldName);
// console.log( console.log(`\n${chalk.bold(`Field: "${fieldName}"`)}\n`);
// ` ${chalk.dim("Type")} ${col.type || chalk.italic("(none)")}`, console.log(` ${chalk.dim("Table")} ${tableName}`);
// ); console.log(` ${chalk.dim("Column #")} ${col.ORDINAL_POSITION}`);
// console.log( console.log(` ${chalk.dim("Type")} ${col.COLUMN_TYPE}`);
// ` ${chalk.dim("Primary Key")} ${col.pk ? chalk.green("YES") : "NO"}`, console.log(` ${chalk.dim("Primary Key")} ${col.COLUMN_KEY === "PRI" ? chalk.green("YES") : "NO"}`);
// ); console.log(` ${chalk.dim("Not Null")} ${col.IS_NULLABLE === "NO" ? chalk.yellow("YES") : "NO"}`);
// console.log( console.log(` ${chalk.dim("Default")} ${col.COLUMN_DEFAULT ?? chalk.italic("(none)")}`);
// ` ${chalk.dim("Not Null")} ${col.notnull ? chalk.yellow("YES") : "NO"}`, console.log(` ${chalk.dim("Extra")} ${col.EXTRA || chalk.italic("(none)")}`);
// ); console.log(` ${chalk.dim("Indexed")} ${colIndexes.length ? chalk.cyan(colIndexes.map((i) => i.INDEX_NAME).join(", ")) : "NO"}`);
// console.log( if (fk) {
// ` ${chalk.dim("Default")} ${col.dflt_value ?? chalk.italic("(none)")}`, console.log(` ${chalk.dim("Foreign Key")} ${chalk.magenta(`${fk.REFERENCED_TABLE_NAME}(${fk.REFERENCED_COLUMN_NAME})`)}`);
// ); }
// console.log( else {
// ` ${chalk.dim("Indexed")} ${idx ? chalk.cyan("YES") : "NO"}`, console.log(` ${chalk.dim("Foreign Key")} NO`);
// ); }
// console.log( if (col.COLUMN_COMMENT) {
// ` ${chalk.dim("Unique")} ${idx?.unique ? chalk.cyan("YES") : "NO"}`, console.log(` ${chalk.dim("Comment")} ${col.COLUMN_COMMENT}`);
// ); }
// if (fk) { console.log();
// console.log( }
// ` ${chalk.dim("Foreign Key")} ${chalk.magenta(`${fk.table}(${fk.to})`)}`,
// );
// console.log(` ${chalk.dim("On Update")} ${fk.on_update}`);
// console.log(` ${chalk.dim("On Delete")} ${fk.on_delete}`);
// } else {
// console.log(` ${chalk.dim("Foreign Key")} NO`);
// }
// console.log();
// }
} }
+22 -11
View File
@@ -1,22 +1,33 @@
import { Command } from "commander"; import { Command } from "commander";
import init from "../functions/init";
import path from "path"; import path from "path";
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs"; import fs from "fs";
import grabDBDir from "../utils/grab-db-dir";
import grabDBBackupFileName from "../utils/grab-db-backup-file-name"; import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
import chalk from "chalk"; import chalk from "chalk";
import trimBackups from "../utils/trim-backups"; import trimBackups from "../utils/trim-backups";
import { dumpDatabase } from "../utils/mariadb-dump-restore";
export default function () { export default function () {
return new Command("backup") return new Command("backup")
.description("Backup Database") .description("Backup MariaDB database (SQL dump)")
.action(async (opts) => { .action(async () => {
console.log(`Backing up database ...`); console.log(`Backing up database ...`);
const { config } = await init(); const config = global.CONFIG;
const { backup_dir, db_file_path } = grabDBDir({ config }); const { backup_dir } = grabDBDir({ config });
const new_db_file_name = grabDBBackupFileName({ config }); if (!fs.existsSync(backup_dir)) {
fs.cpSync(db_file_path, path.join(backup_dir, new_db_file_name)); fs.mkdirSync(backup_dir, { recursive: true });
trimBackups({ config }); }
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))}`); const backup_file_name = grabDBBackupFileName({ config });
process.exit(); const backup_path = path.join(backup_dir, backup_file_name);
try {
const sql = await dumpDatabase(config);
fs.writeFileSync(backup_path, sql, "utf-8");
trimBackups({ config });
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))}${backup_path}`);
process.exit(0);
}
catch (error) {
console.error(chalk.red(`Backup ERROR => ${error.message}`));
process.exit(1);
}
}); });
} }
+2
View File
@@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;
+62
View File
@@ -0,0 +1,62 @@
import { Command } from "commander";
import path from "path";
import fs from "fs";
import chalk from "chalk";
import grabDBDir from "../utils/grab-db-dir";
import { AppData } from "../data/app-data";
import { dumpDatabase } from "../utils/mariadb-dump-restore";
import { writeExportArchive } from "../utils/export-archive";
import trimExports from "../utils/trim-exports";
function defaultExportFileName(dbName, format) {
return `${dbName}-${Date.now()}.${format}`;
}
export default function () {
return new Command("export")
.description("Export database SQL dump + schema.ts into a portable archive")
.option("-o, --output <path>", "Output archive path (.tar.gz or .zip). Defaults to db exports dir")
.option("-f, --format <format>", "Archive format when --output is omitted: tar.gz | zip", "tar.gz")
.action(async (opts) => {
console.log(`Exporting database ...`);
const config = global.CONFIG;
const { db_dir, export_dir } = grabDBDir({ config });
if (!fs.existsSync(export_dir)) {
fs.mkdirSync(export_dir, { recursive: true });
}
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
if (!fs.existsSync(schemaPath)) {
console.error(chalk.red(`Schema file not found: ${schemaPath}`));
process.exit(1);
}
const formatRaw = String(opts.format || "tar.gz").toLowerCase();
const format = formatRaw === "zip" ? "zip" : "tar.gz";
let outPath;
if (opts.output) {
outPath = path.resolve(opts.output);
const parent = path.dirname(outPath);
if (!fs.existsSync(parent)) {
fs.mkdirSync(parent, { recursive: true });
}
}
else {
outPath = path.join(export_dir, defaultExportFileName(config.db_name, format));
}
try {
const sql = await dumpDatabase(config);
const schemaTs = fs.readFileSync(schemaPath, "utf-8");
await writeExportArchive({
contents: { sql, schemaTs },
outPath,
});
if (path.dirname(outPath) === export_dir) {
trimExports({ config });
}
console.log(`${chalk.bold(chalk.green(`DB Export Success!`))}${outPath}`);
console.log(chalk.dim(`Contains: dump.sql + ${AppData.DbSchemaFileName}`));
process.exit(0);
}
catch (error) {
console.error(chalk.red(`Export ERROR => ${error.message}`));
process.exit(1);
}
});
}
+2
View File
@@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;
+86
View File
@@ -0,0 +1,86 @@
import { Command } from "commander";
import path from "path";
import fs from "fs";
import chalk from "chalk";
import { select } from "@inquirer/prompts";
import grabDBDir from "../utils/grab-db-dir";
import grabSortedExports from "../utils/grab-sorted-exports";
import grabBackupData from "../utils/grab-backup-data";
import { AppData } from "../data/app-data";
import { restoreDatabase } from "../utils/mariadb-dump-restore";
import { isArchivePath, isSqlPath, readExportArchive, } from "../utils/export-archive";
function formatChoice(name, index) {
const { backup_date } = grabBackupData({ backup_name: name });
const time = Number.isNaN(backup_date.getTime())
? "unknown date"
: `${backup_date.toDateString()} ${backup_date.getHours()}:${backup_date.getMinutes()}:${backup_date.getSeconds().toString().padStart(2, "0")}`;
return `#${index + 1}: ${name} (${time})`;
}
export default function () {
return new Command("import")
.description("Import an SQL dump, or a full export archive (SQL + schema.ts)")
.argument("[file]", "Path to .sql file or export archive (.tar.gz / .tar / .zip)")
.option("--sql-only", "When importing an archive, restore SQL only (skip writing schema.ts)")
.option("--schema-only", "When importing an archive, write schema.ts only (skip SQL restore)")
.action(async (fileArg, opts) => {
console.log(`Importing database ...`);
const config = global.CONFIG;
const { db_dir, export_dir } = grabDBDir({ config });
try {
let filePath = fileArg ? path.resolve(fileArg) : "";
if (!filePath) {
const candidates = grabSortedExports({ config }).filter((b) => isArchivePath(b));
if (!candidates[0]) {
console.error(`No export archives found in \`${export_dir}\`. Use \`export\`, or pass a file path.`);
process.exit(1);
}
const selected = await select({
message: "Select an export to import:",
choices: candidates.map((b, i) => ({
name: formatChoice(b, i),
value: b,
})),
});
filePath = path.join(export_dir, selected);
}
if (!fs.existsSync(filePath)) {
console.error(chalk.red(`File not found: ${filePath}`));
process.exit(1);
}
if (isSqlPath(filePath)) {
if (opts.schemaOnly) {
console.error(chalk.red(`--schema-only requires an export archive, not a plain .sql file.`));
process.exit(1);
}
const sql = fs.readFileSync(filePath, "utf-8");
await restoreDatabase(config, sql);
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))}${filePath}`);
process.exit(0);
}
if (!isArchivePath(filePath)) {
console.error(chalk.red(`Unsupported file type: ${filePath}. Use .sql, .tar.gz, .tar, or .zip.`));
process.exit(1);
}
if (opts.sqlOnly && opts.schemaOnly) {
console.error(chalk.red(`Cannot combine --sql-only and --schema-only.`));
process.exit(1);
}
const { sql, schemaTs } = await readExportArchive(filePath);
if (!opts.schemaOnly) {
await restoreDatabase(config, sql);
console.log(chalk.green(`SQL restored from archive`));
}
if (!opts.sqlOnly) {
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
fs.writeFileSync(schemaPath, schemaTs, "utf-8");
console.log(chalk.green(`Schema written → ${schemaPath}`));
}
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))}${filePath}`);
process.exit(0);
}
catch (error) {
console.error(chalk.red(`Import ERROR => ${error.message}`));
process.exit(1);
}
});
}
-4
View File
@@ -1,6 +1,2 @@
#!/usr/bin/env bun #!/usr/bin/env bun
/**
* # Declare Global Variables
*/
declare global { }
export {}; export {};
+6
View File
@@ -4,7 +4,11 @@ import schema from "./schema";
import typedef from "./typedef"; import typedef from "./typedef";
import backup from "./backup"; import backup from "./backup";
import restore from "./restore"; import restore from "./restore";
import exportCmd from "./export";
import importCmd from "./import";
import admin from "./admin"; import admin from "./admin";
import init from "../functions/init";
init();
/** /**
* # Describe Program * # Describe Program
*/ */
@@ -19,6 +23,8 @@ program.addCommand(schema());
program.addCommand(typedef()); program.addCommand(typedef());
program.addCommand(backup()); program.addCommand(backup());
program.addCommand(restore()); program.addCommand(restore());
program.addCommand(exportCmd());
program.addCommand(importCmd());
program.addCommand(admin()); program.addCommand(admin());
/** /**
* # Handle Unavailable Commands * # Handle Unavailable Commands
+18 -12
View File
@@ -1,5 +1,4 @@
import { Command } from "commander"; import { Command } from "commander";
import init from "../functions/init";
import grabDBDir from "../utils/grab-db-dir"; import grabDBDir from "../utils/grab-db-dir";
import fs from "fs"; import fs from "fs";
import chalk from "chalk"; import chalk from "chalk";
@@ -7,16 +6,17 @@ import grabSortedBackups from "../utils/grab-sorted-backups";
import { select } from "@inquirer/prompts"; import { select } from "@inquirer/prompts";
import grabBackupData from "../utils/grab-backup-data"; import grabBackupData from "../utils/grab-backup-data";
import path from "path"; import path from "path";
import { restoreDatabase } from "../utils/mariadb-dump-restore";
export default function () { export default function () {
return new Command("restore") return new Command("restore")
.description("Restore Database") .description("Restore MariaDB database from an SQL dump")
.action(async (opts) => { .action(async () => {
console.log(`Restoring up database ...`); console.log(`Restoring database ...`);
const { config } = await init(); const config = global.CONFIG;
const { backup_dir, db_file_path } = grabDBDir({ config }); const { backup_dir } = grabDBDir({ config });
const backups = grabSortedBackups({ config }); const backups = grabSortedBackups({ config }).filter((b) => b.endsWith(".sql"));
if (!backups?.[0]) { if (!backups?.[0]) {
console.error(`No Backups to restore. Use the \`backup\` command to create a backup`); console.error(`No SQL backups to restore. Use the \`backup\` command to create a backup.`);
process.exit(1); process.exit(1);
} }
try { try {
@@ -32,13 +32,19 @@ export default function () {
}; };
}), }),
}); });
fs.cpSync(path.join(backup_dir, selected_backup), db_file_path); const backup_path = path.join(backup_dir, selected_backup);
console.log(`${chalk.bold(chalk.green(`DB Restore Success!`))}`); if (!fs.existsSync(backup_path)) {
process.exit(); console.error(`Backup file not found: ${backup_path}`);
process.exit(1);
}
const sql = fs.readFileSync(backup_path, "utf-8");
await restoreDatabase(config, sql);
console.log(`${chalk.bold(chalk.green(`DB Restore Success!`))}${selected_backup}`);
process.exit(0);
} }
catch (error) { catch (error) {
console.error(`Backup Restore ERROR => ${error.message}`); console.error(`Backup Restore ERROR => ${error.message}`);
process.exit(); process.exit(1);
} }
}); });
} }
+9 -19
View File
@@ -1,38 +1,29 @@
import { Command } from "commander"; import { Command } from "commander";
import { MariaDBSchemaManager } from "../lib/mariadb/db-schema-manager";
import init from "../functions/init";
import grabDirNames from "../data/grab-dir-names"; import grabDirNames from "../data/grab-dir-names";
import path from "path"; import path from "path";
import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef"; import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef";
import _ from "lodash";
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema"; import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
import chalk from "chalk"; import chalk from "chalk";
import { writeLiveSchema } from "../functions/live-schema"; import { writeLiveSchema } from "../functions/live-schema";
import grabDBDir from "../utils/grab-db-dir"; import createDBSchema from "../lib/schema/create-db-schema";
import { cpSync } from "fs";
export default function () { export default function () {
return new Command("schema") return new Command("schema")
.description("Build DB From Schema") .description("Build DB From Schema")
.option("-v, --vector", "Recreate Vector Tables. This will drop and rebuild all vector tables")
.option("-t, --typedef", "Generate typescript type definitions") .option("-t, --typedef", "Generate typescript type definitions")
.action(async (opts) => { .action(async (opts) => {
console.log(`Starting process ...`); console.log(`Starting process ...`);
const { config, dbSchema } = await init(); const config = global.CONFIG;
const { ROOT_DIR, BUN_MARIADB_TEMP_DB_FILE_PATH } = grabDirNames(); const dbSchema = global.DB_SCHEMA;
const { db_file_path } = grabDBDir({ config }); const { ROOT_DIR } = grabDirNames();
cpSync(db_file_path, BUN_MARIADB_TEMP_DB_FILE_PATH);
try { try {
const isVector = Boolean(opts.vector || opts.v);
const isTypeDef = Boolean(opts.typedef || opts.t); const isTypeDef = Boolean(opts.typedef || opts.t);
const finaldbSchema = appendDefaultFieldsToDbSchema({ const finaldbSchema = appendDefaultFieldsToDbSchema({
dbSchema, dbSchema,
}); });
const manager = new MariaDBSchemaManager({ await createDBSchema({
schema: finaldbSchema, db_schema: finaldbSchema,
recreate_vector_table: isVector, config,
}); });
await manager.syncSchema();
manager.close();
if (isTypeDef && config.typedef_file_path) { if (isTypeDef && config.typedef_file_path) {
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path); const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
dbSchemaToTypeDef({ dbSchemaToTypeDef({
@@ -43,11 +34,10 @@ export default function () {
} }
writeLiveSchema({ schema: finaldbSchema }); writeLiveSchema({ schema: finaldbSchema });
console.log(`${chalk.bold(chalk.green(`DB Schema setup success!`))}`); console.log(`${chalk.bold(chalk.green(`DB Schema setup success!`))}`);
process.exit(); process.exit(0);
} }
catch (error) { catch (error) {
console.log(error); console.error(error);
cpSync(BUN_MARIADB_TEMP_DB_FILE_PATH, db_file_path);
process.exit(1); process.exit(1);
} }
}); });
+13 -15
View File
@@ -1,5 +1,4 @@
import { Command } from "commander"; import { Command } from "commander";
import init from "../functions/init";
import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef"; import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef";
import path from "path"; import path from "path";
import grabDirNames from "../data/grab-dir-names"; import grabDirNames from "../data/grab-dir-names";
@@ -7,25 +6,24 @@ import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-
import chalk from "chalk"; import chalk from "chalk";
export default function () { export default function () {
return new Command("typedef") return new Command("typedef")
.description("Build DB From Schema") .description("Generate TypeScript type definitions from the schema")
.action(async (opts) => { .action(async () => {
console.log(`Creating Type Definition From DB Schema ...`); console.log(`Creating Type Definition From DB Schema ...`);
const { config, dbSchema } = await init(); const config = global.CONFIG;
const dbSchema = global.DB_SCHEMA;
const { ROOT_DIR } = grabDirNames(); const { ROOT_DIR } = grabDirNames();
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema }); const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
if (config.typedef_file_path) { if (!config.typedef_file_path) {
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path); console.error(`\`typedef_file_path\` is required in bun-mariadb.config.ts to generate types.`);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
config,
});
}
else {
console.error(``);
process.exit(1); process.exit(1);
} }
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
config,
});
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`); console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
process.exit(); process.exit(0);
}); });
} }
+6 -1
View File
@@ -1,6 +1,11 @@
export declare const AppData: { export declare const AppData: {
readonly ConfigFileName: "bun-sqlite.config.ts"; readonly ConfigFileName: "bun-mariadb.config.ts";
readonly MaxBackups: 10; readonly MaxBackups: 10;
readonly MaxExports: 10;
readonly DefaultBackupDirName: ".backups"; readonly DefaultBackupDirName: ".backups";
readonly DefaultExportDirName: ".exports";
readonly DbSchemaManagerTableName: "__db_schema_manager__"; readonly DbSchemaManagerTableName: "__db_schema_manager__";
readonly DbSchemaFileName: "schema.ts";
readonly MaxInitRetries: 50;
readonly InitRetryIntervalMilliseconds: 5000;
}; };
+6 -1
View File
@@ -1,6 +1,11 @@
export const AppData = { export const AppData = {
ConfigFileName: "bun-sqlite.config.ts", ConfigFileName: "bun-mariadb.config.ts",
MaxBackups: 10, MaxBackups: 10,
MaxExports: 10,
DefaultBackupDirName: ".backups", DefaultBackupDirName: ".backups",
DefaultExportDirName: ".exports",
DbSchemaManagerTableName: "__db_schema_manager__", DbSchemaManagerTableName: "__db_schema_manager__",
DbSchemaFileName: "schema.ts",
MaxInitRetries: 50,
InitRetryIntervalMilliseconds: 5000,
}; };
+2 -3
View File
@@ -1,12 +1,11 @@
import type { BunSQLiteConfig } from "../types"; import type { BunMariaDBConfig } from "../types";
type Params = { type Params = {
config?: BunSQLiteConfig; config?: BunMariaDBConfig;
}; };
export default function grabDirNames(params?: Params): { export default function grabDirNames(params?: Params): {
ROOT_DIR: string; ROOT_DIR: string;
BUN_MARIADB_DIR: string; BUN_MARIADB_DIR: string;
BUN_MARIADB_TEMP_DIR: string; BUN_MARIADB_TEMP_DIR: string;
BUN_MARIADB_LIVE_SCHEMA: string; BUN_MARIADB_LIVE_SCHEMA: string;
BUN_MARIADB_TEMP_DB_FILE_PATH: string;
}; };
export {}; export {};
+1 -3
View File
@@ -1,15 +1,13 @@
import path from "path"; import path from "path";
export default function grabDirNames(params) { export default function grabDirNames(params) {
const ROOT_DIR = process.cwd(); const ROOT_DIR = process.cwd();
const BUN_MARIADB_DIR = path.join(ROOT_DIR, ".bun-sqlite"); const BUN_MARIADB_DIR = path.join(ROOT_DIR, ".bun-mariadb");
const BUN_MARIADB_TEMP_DIR = path.join(BUN_MARIADB_DIR, ".tmp"); const BUN_MARIADB_TEMP_DIR = path.join(BUN_MARIADB_DIR, ".tmp");
const BUN_MARIADB_TEMP_DB_FILE_PATH = path.join(BUN_MARIADB_TEMP_DIR, "temp.db");
const BUN_MARIADB_LIVE_SCHEMA = path.join(BUN_MARIADB_DIR, "live-schema.json"); const BUN_MARIADB_LIVE_SCHEMA = path.join(BUN_MARIADB_DIR, "live-schema.json");
return { return {
ROOT_DIR, ROOT_DIR,
BUN_MARIADB_DIR, BUN_MARIADB_DIR,
BUN_MARIADB_TEMP_DIR, BUN_MARIADB_TEMP_DIR,
BUN_MARIADB_LIVE_SCHEMA, BUN_MARIADB_LIVE_SCHEMA,
BUN_MARIADB_TEMP_DB_FILE_PATH,
}; };
} }
+6
View File
@@ -0,0 +1,6 @@
import { type BunMariaDBConfig } from "../types";
type Params = {
config: BunMariaDBConfig;
};
export default function grabMariaDBClient({ config, }: Params): Bun.SQL | undefined;
export {};
+48
View File
@@ -0,0 +1,48 @@
import path from "path";
import grabDirNames from "../data/grab-dir-names";
import {} from "../types";
import { SQL } from "bun";
/** Reuse one client per database name to avoid exhausting MariaDB connections. */
const clientCache = new Map();
export default function grabMariaDBClient({ config, }) {
const cacheKey = config.db_name || "__default__";
const cached = clientCache.get(cacheKey);
if (cached) {
return cached;
}
// Prefer the process-wide client when it matches the requested database
if (global.MARIADB_CLIENT &&
(!global.CONFIG?.db_name || global.CONFIG.db_name === config.db_name)) {
clientCache.set(cacheKey, global.MARIADB_CLIENT);
return global.MARIADB_CLIENT;
}
const { ROOT_DIR } = grabDirNames();
try {
const tls = config.ssl_ca
? {
ca: Bun.file(path.resolve(ROOT_DIR, config.ssl_ca)),
rejectUnauthorized: false,
}
: {
rejectUnauthorized: false,
};
const MariaDBClient = new SQL({
hostname: process.env.BUN_MARIADB_SERVER_HOST,
username: process.env.BUN_MARIADB_SERVER_USERNAME,
password: process.env.BUN_MARIADB_SERVER_PASSWORD,
database: config.db_name,
port: process.env.BUN_MARIADB_SERVER_PORT
? Number(process.env.BUN_MARIADB_SERVER_PORT)
: undefined,
...config.db_config,
tls,
adapter: "mariadb",
});
clientCache.set(cacheKey, MariaDBClient);
return MariaDBClient;
}
catch (error) {
console.error(`Couldn't grab MariaDB Client => ` + error.message);
return;
}
}
+10 -2
View File
@@ -1,2 +1,10 @@
import type { BunSQLiteConfigReturn } from "../types"; import { type BunMariaDBConfig, type BUN_MARIADB_DatabaseSchemaType } from "../types";
export default function init(): Promise<BunSQLiteConfigReturn>; /**
* # Declare Global Variables
*/
declare global {
var CONFIG: BunMariaDBConfig;
var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType;
var MARIADB_CLIENT: Bun.SQL;
}
export default function init(): void;
+48 -14
View File
@@ -2,42 +2,76 @@ import path from "path";
import fs from "fs"; import fs from "fs";
import { AppData } from "../data/app-data"; import { AppData } from "../data/app-data";
import grabDirNames from "../data/grab-dir-names"; import grabDirNames from "../data/grab-dir-names";
export default async function init() { import { RequiredENVs, } from "../types";
import setMariaDBClient from "./set-mariadb-client";
export default function init() {
try { try {
const { ROOT_DIR } = grabDirNames(); const { ROOT_DIR } = grabDirNames();
const { ConfigFileName } = AppData; const { ConfigFileName } = AppData;
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName); const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
if (!fs.existsSync(ConfigFilePath)) { if (!fs.existsSync(ConfigFilePath)) {
console.log("ConfigFilePath", ConfigFilePath);
console.error(`Please create a \`${ConfigFileName}\` file at the root of your project.`); console.error(`Please create a \`${ConfigFileName}\` file at the root of your project.`);
process.exit(1); process.exit(1);
} }
const ConfigImport = await import(ConfigFilePath); const ConfigImport = require(ConfigFilePath);
const Config = ConfigImport["default"]; const Config = ConfigImport["default"];
if (!Config) {
console.error(`No default export from \`${ConfigFilePath}\`. Please export a default module.`);
process.exit(1);
}
if (!Config.db_name) { if (!Config.db_name) {
console.error(`\`db_name\` is required in your config`); console.error(`\`db_name\` is required in your config`);
process.exit(1); process.exit(1);
} }
if (!Config.db_schema_file_name) { RequiredENVs.forEach((env_key) => {
console.error(`\`db_schema_file_name\` is required in your config`); if (env_key == "BUN_MARIADB_SERVER_PORT" ||
env_key == "BUN_MARIADB_SERVER_SSL_KEY_PATH") {
return;
}
if (!process.env[env_key]) {
console.error(`\`${env_key}\` env variable is required.`);
process.exit(1);
}
});
if (!Config.db_dir) {
console.error(`\`db_dir\` is required in your config. This directory holds all database related configuration. Also note that a \`${AppData["DbSchemaFileName"]}\` file is also required in this directory to define your database schema`);
process.exit(1); process.exit(1);
} }
let db_dir = ROOT_DIR; const db_dir = path.resolve(ROOT_DIR, Config.db_dir);
if (Config.db_dir) { if (!fs.existsSync(db_dir)) {
db_dir = path.resolve(ROOT_DIR, Config.db_dir); fs.mkdirSync(db_dir, { recursive: true });
if (!fs.existsSync(Config.db_dir)) {
fs.mkdirSync(Config.db_dir, { recursive: true });
}
} }
const DBSchemaFilePath = path.join(db_dir, Config.db_schema_file_name); const DBSchemaFilePath = path.join(db_dir, AppData["DbSchemaFileName"]);
const DbSchemaImport = await import(DBSchemaFilePath); if (!fs.existsSync(DBSchemaFilePath)) {
console.error(`Please create a schema file at \`${DBSchemaFilePath}\`. Don't forget to export a default module from this file.`);
process.exit(1);
}
const DbSchemaImport = require(DBSchemaFilePath);
const DbSchema = DbSchemaImport["default"]; const DbSchema = DbSchemaImport["default"];
const backup_dir = Config.db_backup_dir || AppData["DefaultBackupDirName"]; const backup_dir = Config.db_backup_dir || AppData["DefaultBackupDirName"];
const BackupDir = path.resolve(db_dir, backup_dir); const BackupDir = path.resolve(db_dir, backup_dir);
if (!fs.existsSync(BackupDir)) { if (!fs.existsSync(BackupDir)) {
fs.mkdirSync(BackupDir, { recursive: true }); fs.mkdirSync(BackupDir, { recursive: true });
} }
return { config: Config, dbSchema: DbSchema }; const ExportDir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
if (!fs.existsSync(ExportDir)) {
fs.mkdirSync(ExportDir, { recursive: true });
}
global.CONFIG = Config;
global.DB_SCHEMA = DbSchema;
if (!global.CONFIG) {
console.error(`Couldn't grab global Config.`);
process.exit(1);
}
if (!global.DB_SCHEMA) {
console.error(`Couldn't grab Database Schema.`);
process.exit(1);
}
setMariaDBClient({ config: Config });
if (!global.MARIADB_CLIENT) {
console.error(`Couldn't set MariaDB Client.`);
process.exit(1);
}
} }
catch (error) { catch (error) {
console.error(`Initialization ERROR => ` + error.message); console.error(`Initialization ERROR => ` + error.message);
+6
View File
@@ -0,0 +1,6 @@
import { type BunMariaDBConfig } from "../types";
type Params = {
config: BunMariaDBConfig;
};
export default function setMariaDBClient({ config }: Params): void;
export {};
+21
View File
@@ -0,0 +1,21 @@
import {} from "../types";
import grabMariaDBClient from "./grab-mariadb-client";
export default function setMariaDBClient({ config }) {
try {
const MariaDBClient = grabMariaDBClient({ config });
if (!MariaDBClient) {
throw new Error(`Couldn't grab MariaDB Client`);
}
global.MARIADB_CLIENT = MariaDBClient;
// const test = await MariaDBClient.unsafe(`SHOW DATABASES`);
// if (!test.count) {
// console.error(`MariaDBClient Error: Database not ready.`);
// console.log(test);
// process.exit(1);
// }
}
catch (error) {
console.error(`ERROR setting MariaDB Client => ` + error.message);
process.exit(1);
}
}
+1
View File
@@ -17,3 +17,4 @@ declare const BunMariaDB: {
}; };
}; };
export default BunMariaDB; export default BunMariaDB;
export type { BunMariaDBConfig, BUN_MARIADB_DatabaseSchemaType, BUN_MARIADB_TableSchemaType, BUN_MARIADB_FieldSchemaType, BUN_MARIADB_IndexSchemaType, BUN_MARIADB_UniqueConstraintSchemaType, BUN_MARIADB_ForeignKeyType, DBResponseObject, ServerQueryParam, } from "./types";
+2
View File
@@ -1,3 +1,4 @@
import init from "./functions/init";
import DbDelete from "./lib/mariadb/db-delete"; import DbDelete from "./lib/mariadb/db-delete";
import DbInsert from "./lib/mariadb/db-insert"; import DbInsert from "./lib/mariadb/db-insert";
import DbSelect from "./lib/mariadb/db-select"; import DbSelect from "./lib/mariadb/db-select";
@@ -5,6 +6,7 @@ import DbSQL from "./lib/mariadb/db-sql";
import DbUpdate from "./lib/mariadb/db-update"; import DbUpdate from "./lib/mariadb/db-update";
import grabDbSchema from "./utils/grab-db-schema"; import grabDbSchema from "./utils/grab-db-schema";
import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object"; import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object";
init();
const BunMariaDB = { const BunMariaDB = {
select: DbSelect, select: DbSelect,
insert: DbInsert, insert: DbInsert,
+4 -9
View File
@@ -1,20 +1,15 @@
import type { ConnectionConfig } from "mariadb"; import type { BunMariaDBConfig, DBResponseObject } from "../types";
import type { BUN_MARIADB_TableSchemaType, DBResponseObject } from "../types";
type Param = { type Param = {
query: string; query: string;
values?: string[] | object; values?: any[];
noErrorLogs?: boolean; config?: BunMariaDBConfig;
database?: string;
tableSchema?: BUN_MARIADB_TableSchemaType;
config?: ConnectionConfig;
}; };
/** /**
* # Main DB Handler Function * # Main DB Handler Function
* @requires DSQL_DB_CONN - Gobal Variable for Datasquirel Database
*/ */
export default function dbHandler<T extends { export default function dbHandler<T extends {
[k: string]: any; [k: string]: any;
} = { } = {
[k: string]: any; [k: string]: any;
}>({ query, values, noErrorLogs, database, config, }: Param): Promise<DBResponseObject>; }>({ query, values, config }: Param): Promise<DBResponseObject<T>>;
export {}; export {};
+47 -32
View File
@@ -1,46 +1,61 @@
import grabDBConnection from "./grab-db-connection"; import grabMariaDBClient from "../functions/grab-mariadb-client";
/**
* Resolve a shared MariaDB client. Prefer the global client so schema sync
* and CRUD do not open a new connection per query.
*/
function resolveClient(config) {
const resolvedConfig = config || global.CONFIG;
// Same database as the global client → reuse it
if (global.MARIADB_CLIENT &&
(!resolvedConfig?.db_name ||
!global.CONFIG?.db_name ||
resolvedConfig.db_name === global.CONFIG.db_name)) {
return global.MARIADB_CLIENT;
}
if (resolvedConfig) {
return grabMariaDBClient({ config: resolvedConfig });
}
return global.MARIADB_CLIENT;
}
/** /**
* # Main DB Handler Function * # Main DB Handler Function
* @requires DSQL_DB_CONN - Gobal Variable for Datasquirel Database
*/ */
export default async function dbHandler({ query, values, noErrorLogs, database, config, }) { export default async function dbHandler({ query, values, config }) {
let CONNECTION;
let results = null;
try { try {
CONNECTION = await grabDBConnection({ database, config }); const CLIENT = resolveClient(config);
if (query && values) { if (!CLIENT) {
const queryResults = await CONNECTION.query(query, values); throw new Error(`Couldn't grab MariaDB Client.`);
results = queryResults[0];
} }
else { const res = await CLIENT.unsafe(query, values);
const queryResults = await CONNECTION.query(query); const count = res.count;
results = queryResults[0]; const res_array = (() => {
} try {
} return JSON.parse(JSON.stringify(res));
catch (error) { }
if (error.message && catch (error) {
typeof error.message == "string" && return undefined;
error.message.match(/Access denied for user.*password/i)) { }
throw new Error("Authentication Failed!"); })();
} const last_insert_id = res_array?.[0] ? res_array[0]?.id : undefined;
if (!noErrorLogs) { const affected_rows = res_array?.[0] ? res_array.length : undefined;
console.log(error); const insert_return = {
} count,
results = null; last_insert_id,
} affected_rows,
finally { };
await CONNECTION?.end();
}
if (results) {
return { return {
success: true, success: true,
payload: Array.isArray(results) ? results : undefined, payload: res_array,
single_res: Array.isArray(results) ? undefined : results, single_res: res_array?.[0],
insert_return,
count,
}; };
} }
else { catch (error) {
return { return {
success: false, success: false,
error: "DB Handler Error => " + error.message,
msg: "DB Handler Error => " + error.message,
}; };
} }
} }
-6
View File
@@ -1,6 +0,0 @@
import type { Connection } from "mariadb";
import type { DsqlConnectionParam } from "../types";
/**
* # Grab General CONNECTION for DSQL
*/
export default function grabDBConnection(param?: DsqlConnectionParam): Promise<Connection>;
-15
View File
@@ -1,15 +0,0 @@
import * as mariadb from "mariadb";
import grabDSQLConnectionConfig from "./grab-dsql-connection-config";
/**
* # Grab General CONNECTION for DSQL
*/
export default async function grabDBConnection(param) {
const config = grabDSQLConnectionConfig(param);
try {
return await mariadb.createConnection(config);
}
catch (error) {
console.log(`Error Grabbing DSQL Connection =>`, config);
throw error;
}
}
-7
View File
@@ -1,7 +0,0 @@
import type { ConnectionConfig } from "mariadb";
type Return = ConnectionConfig["ssl"] | undefined;
/**
* # Grab SSL
*/
export default function grabDbSSL(): Return;
export {};
-20
View File
@@ -1,20 +0,0 @@
import fs from "fs";
import path from "path";
/**
* # Grab SSL
*/
export default function grabDbSSL() {
const caProivdedPath = process.env.DSQL_SSL_CA_CERT;
if (!caProivdedPath?.match(/./)) {
return undefined;
}
const caFilePath = path.resolve(process.cwd(), caProivdedPath);
if (!fs.existsSync(caFilePath)) {
console.log(`${caFilePath} does not exist`);
return undefined;
}
return {
ca: fs.readFileSync(caFilePath),
rejectUnauthorized: false,
};
}
-6
View File
@@ -1,6 +0,0 @@
import { type ConnectionConfig } from "mariadb";
import type { DsqlConnectionParam } from "../types";
/**
* # Grab General CONNECTION for DSQL
*/
export default function grabDSQLConnectionConfig(param?: DsqlConnectionParam): ConnectionConfig;
-29
View File
@@ -1,29 +0,0 @@
import {} from "mariadb";
import grabDbSSL from "./grab-db-ssl";
/**
* # Grab General CONNECTION for DSQL
*/
export default function grabDSQLConnectionConfig(param) {
const CONN_TIMEOUT = 10000;
const config = {
host: process.env.DSQL_DB_HOST,
user: process.env.DSQL_DB_USERNAME,
password: process.env.DSQL_DB_PASSWORD,
// database: param?.dbFullName,
port: process.env.DSQL_DB_PORT
? Number(process.env.DSQL_DB_PORT)
: undefined,
charset: "utf8mb4",
ssl: grabDbSSL(),
bigIntAsNumber: true,
supportBigNumbers: true,
bigNumberStrings: false,
dateStrings: true,
metaAsArray: true,
socketTimeout: CONN_TIMEOUT,
connectTimeout: CONN_TIMEOUT,
compress: true,
...param?.config,
};
return config;
}
+1 -1
View File
@@ -3,5 +3,5 @@ type Params = {
table: string; table: string;
data: any | any[]; data: any | any[];
}; };
export default function ({ sql: passed_sql, table, data }: Params): Promise<string>; export default function ({ sql: passedSql, table, data }: Params): Promise<string>;
export {}; export {};
+23 -22
View File
@@ -1,24 +1,25 @@
import init from "../functions/init"; function quoteIdentifier(identifier) {
export default async function ({ sql: passed_sql, table, data }) { return `\`${identifier.replace(/`/g, "``")}\``;
let sql = passed_sql; }
const { dbSchema } = await init(); export default async function ({ sql: passedSql, table, data }) {
const table_schema = dbSchema.tables.find((t) => t.tableName == table); const config = global.CONFIG;
const now = Date.now(); const dbSchema = global.DB_SCHEMA;
if (table_schema?.tableName) { const tableSchema = dbSchema?.tables.find((t) => t.tableName == table);
const set_sql_arr = Object.keys(Array.isArray(data) ? data[0] : data).map((field) => `${field} = excluded.${field}`); if (!tableSchema?.tableName) {
set_sql_arr.push(`updated_at = ${now}`); return passedSql;
const set_sql = set_sql_arr.join(", ");
const unique_fields = table_schema.fields.filter((f) => f.unique);
for (let i = 0; i < unique_fields.length; i++) {
const field = unique_fields[i];
sql += ` ON CONFLICT(${field?.fieldName}) DO UPDATE SET ${set_sql}`;
}
if (table_schema.uniqueConstraints?.[0]) {
for (let i = 0; i < table_schema.uniqueConstraints.length; i++) {
const constraint = table_schema.uniqueConstraints[i];
sql += ` ON CONFLICT(${constraint?.constraintTableFields?.map((c) => c.value)?.join(", ")}) DO UPDATE SET ${set_sql}`;
}
}
} }
return sql; const dataObject = Array.isArray(data) ? data[0] : data;
const dataKeys = Object.keys(dataObject || {});
if (dataKeys.length === 0) {
return passedSql;
}
const hasUniqueField = tableSchema.fields.some((field) => field.unique) || Boolean(tableSchema.uniqueConstraints?.length);
if (!hasUniqueField) {
return passedSql;
}
const setSql = dataKeys
.filter((field) => field !== "updated_at")
.map((field) => `${quoteIdentifier(field)} = VALUES(${quoteIdentifier(field)})`);
setSql.push(`updated_at = ${Date.now()}`);
return `${passedSql} ON DUPLICATE KEY UPDATE ${setSql.join(", ")}`;
} }
+3 -2
View File
@@ -1,4 +1,4 @@
import type { APIResponseObject, ServerQueryParam } from "../../types"; import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types";
type Params<Schema extends { type Params<Schema extends {
[k: string]: any; [k: string]: any;
} = { } = {
@@ -7,10 +7,11 @@ type Params<Schema extends {
table: Table; table: Table;
query?: ServerQueryParam<Schema>; query?: ServerQueryParam<Schema>;
targetId?: number | string; targetId?: number | string;
config?: BunMariaDBConfig;
}; };
export default function DbDelete<Schema extends { export default function DbDelete<Schema extends {
[k: string]: any; [k: string]: any;
} = { } = {
[k: string]: any; [k: string]: any;
}, Table extends string = string>({ table, query, targetId, }: Params<Schema, Table>): Promise<APIResponseObject>; }, Table extends string = string>({ table, query, targetId, config, }: Params<Schema, Table>): Promise<DBResponseObject>;
export {}; export {};
+31 -19
View File
@@ -1,7 +1,10 @@
import DbClient from "."; import dbHandler from "../db-handler";
import _ from "lodash"; import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator"; import sqlGenerator from "../../utils/sql-generator";
export default async function DbDelete({ table, query, targetId, }) { function quoteIdentifier(identifier) {
return `\`${identifier.replace(/`/g, "``")}\``;
}
export default async function DbDelete({ table, query, targetId, config, }) {
let sqlObj = null; let sqlObj = null;
try { try {
let finalQuery = query || {}; let finalQuery = query || {};
@@ -15,26 +18,11 @@ export default async function DbDelete({ table, query, targetId, }) {
}); });
} }
sqlObj = sqlGenerator({ sqlObj = sqlGenerator({
tableName: table, tableName: quoteIdentifier(table),
genObject: finalQuery, genObject: finalQuery,
}); });
const whereClause = sqlObj.string.match(/WHERE .*/)?.[0]; const whereClause = sqlObj.string.match(/WHERE .*/)?.[0];
if (whereClause) { if (!whereClause) {
let sql = `DELETE FROM ${table} ${whereClause}`;
sqlObj.string = sql;
const res = DbClient.run(sql, sqlObj.values);
return {
success: Boolean(res.changes),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sqlObj,
},
};
}
else {
return { return {
success: false, success: false,
msg: `No WHERE clause`, msg: `No WHERE clause`,
@@ -43,6 +31,30 @@ export default async function DbDelete({ table, query, targetId, }) {
}, },
}; };
} }
let sql = `DELETE FROM ${quoteIdentifier(table)} ${whereClause}`;
sql += ` RETURNING *`;
sqlObj.string = sql;
const res = await dbHandler({
query: sql,
values: sqlObj.values,
config,
});
if (!res.success) {
return {
success: false,
msg: "Database delete failed",
debug: {
sqlObj,
},
};
}
return {
..._.omit(res, ["payload"]),
success: Boolean(res.insert_return?.last_insert_id),
debug: {
sqlObj,
},
};
} }
catch (error) { catch (error) {
return { return {
+3 -2
View File
@@ -1,4 +1,4 @@
import type { APIResponseObject } from "../../types"; import type { BunMariaDBConfig, DBResponseObject } from "../../types";
type Params<Schema extends { type Params<Schema extends {
[k: string]: any; [k: string]: any;
} = { } = {
@@ -7,10 +7,11 @@ type Params<Schema extends {
table: Table; table: Table;
data: Schema[]; data: Schema[];
update_on_duplicate?: boolean; update_on_duplicate?: boolean;
config?: BunMariaDBConfig;
}; };
export default function DbInsert<Schema extends { export default function DbInsert<Schema extends {
[k: string]: any; [k: string]: any;
} = { } = {
[k: string]: any; [k: string]: any;
}, Table extends string = string>({ table, data, update_on_duplicate, }: Params<Schema, Table>): Promise<APIResponseObject>; }, Table extends string = string>({ table, data, update_on_duplicate, config, }: Params<Schema, Table>): Promise<DBResponseObject>;
export {}; export {};
+34 -11
View File
@@ -1,10 +1,16 @@
import DbClient from "."; import dbHandler from "../db-handler";
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";
export default async function DbInsert({ table, data, update_on_duplicate, }) { export default async function DbInsert({ table, data, update_on_duplicate, config, }) {
let sqlObj = null; let sqlObj = null;
try { try {
const finalData = data.map((d) => ({ const sanitizedData = sanitizeHtmlFieldsBatch({
table,
data,
config,
});
const finalData = sanitizedData.map((d) => ({
created_at: Date.now(), created_at: Date.now(),
updated_at: Date.now(), updated_at: Date.now(),
...d, ...d,
@@ -14,18 +20,35 @@ export default async function DbInsert({ table, data, update_on_duplicate, }) {
tableName: table, tableName: table,
data: finalData, data: finalData,
}) || null; }) || null;
let sql = sqlObj?.query || ""; if (!sqlObj) {
return {
success: false,
msg: "No insert SQL generated",
};
}
let sql = sqlObj.query;
if (update_on_duplicate && data[0]) { if (update_on_duplicate && data[0]) {
sql = await grabDuplicateSafeInsertSql({ data, table, sql }); sql = await grabDuplicateSafeInsertSql({ data, table, sql });
} }
(sqlObj || {}).query = sql; sql += ` RETURNING *`;
const res = DbClient.run(sql, sqlObj?.values || []); const res = await dbHandler({
query: sql,
values: sqlObj.values,
config,
});
if (!res.success) {
return {
success: false,
msg: "Database insert failed",
debug: {
sqlObj,
},
};
}
return { return {
success: Boolean(Number(res.lastInsertRowid)), ...res,
postInsertReturn: { success: Boolean(res?.insert_return?.affected_rows ||
affectedRows: res.changes, res.insert_return?.last_insert_id),
insertId: Number(res.lastInsertRowid),
},
debug: { debug: {
sqlObj, sqlObj,
}, },
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env bun
import type { BUN_MARIADB_DatabaseSchemaType } from "../../types";
declare class MariaDBSchemaManager {
private db_manager_table_name;
private recreate_vector_table;
private db_schema;
constructor({ schema, recreate_vector_table, }: {
schema: BUN_MARIADB_DatabaseSchemaType;
recreate_vector_table?: boolean;
});
syncSchema(): Promise<void>;
private getDatabaseName;
private quoteIdentifier;
private tableSchemaWhere;
private run;
private query;
private createDbManagerTable;
private insertDbManagerTable;
private removeDbManagerTable;
private getExistingTables;
private getLiveTableNames;
private dropRemovedTables;
private syncTable;
private resolveTable;
private createTable;
private buildTableOptions;
private updateTable;
private getTableColumns;
private addColumn;
private checkIfTableExists;
private recreateTable;
private insertRows;
private buildColumnDefinition;
private mapDataType;
private buildForeignKeyConstraint;
private syncIndexes;
close(): void;
}
export { MariaDBSchemaManager };
-419
View File
@@ -1,419 +0,0 @@
#!/usr/bin/env bun
import _ from "lodash";
import dbHandler from "../db-handler";
import { AppData } from "../../data/app-data";
import { readLiveSchema } from "../../functions/live-schema";
class MariaDBSchemaManager {
db_manager_table_name;
recreate_vector_table;
db_schema;
constructor({ schema, recreate_vector_table = false, }) {
this.db_manager_table_name = AppData["DbSchemaManagerTableName"];
this.recreate_vector_table = recreate_vector_table;
this.db_schema = schema;
}
async syncSchema() {
console.log("Starting schema synchronization...");
await this.createDbManagerTable();
const existingTables = await this.getExistingTables();
const schemaTables = this.db_schema.tables.map((t) => t.tableName);
for (const table of this.db_schema.tables) {
await this.syncTable(table, existingTables);
}
await this.dropRemovedTables(existingTables, schemaTables);
console.log("Schema synchronization complete!");
}
getDatabaseName() {
return this.db_schema.dbName || this.db_schema.dbSlug;
}
quoteIdentifier(identifier) {
return `\`${identifier.replace(/`/g, "``")}\``;
}
tableSchemaWhere(tableName) {
const databaseName = this.getDatabaseName();
if (databaseName) {
return {
where: "TABLE_SCHEMA = ?",
values: [databaseName, tableName],
};
}
return {
where: "TABLE_SCHEMA = DATABASE()",
values: [tableName],
};
}
async run(query, values) {
const res = await dbHandler({
query,
values: values,
config: this.getDatabaseName()
? { database: this.getDatabaseName() }
: undefined,
});
if (!res.success) {
throw new Error(`Database query failed: ${query}`);
}
}
async query(query, values) {
const res = await dbHandler({
query,
values: values,
config: this.getDatabaseName()
? { database: this.getDatabaseName() }
: undefined,
});
if (!res.success) {
throw new Error(`Database query failed: ${query}`);
}
return (res.payload || []);
}
async createDbManagerTable() {
await this.run(`
CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(this.db_manager_table_name)} (
table_name VARCHAR(255) NOT NULL PRIMARY KEY,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci
`);
}
async insertDbManagerTable(tableName) {
const now = Date.now();
await this.run(`INSERT INTO ${this.quoteIdentifier(this.db_manager_table_name)} (table_name, created_at, updated_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)`, [tableName, now, now]);
}
async removeDbManagerTable(tableName) {
await this.run(`DELETE FROM ${this.quoteIdentifier(this.db_manager_table_name)} WHERE table_name = ?`, [tableName]);
}
async getExistingTables() {
const rows = await this.query(`SELECT table_name FROM ${this.quoteIdentifier(this.db_manager_table_name)}`);
return rows.map((row) => row.table_name);
}
async getLiveTableNames() {
const tableSchemaWhere = this.tableSchemaWhere("");
const rows = await this.query(`SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${tableSchemaWhere.where} AND TABLE_TYPE = 'BASE TABLE'`, tableSchemaWhere.values);
return rows.map((row) => row.TABLE_NAME);
}
async dropRemovedTables(existingTables, schemaTables) {
console.log(`Cleaning up tables ...`);
const tablesToDrop = existingTables.filter((tableName) => !schemaTables.includes(tableName) &&
!schemaTables.some((schemaTable) => tableName.startsWith(`${schemaTable}_`)));
const currentSchema = readLiveSchema();
if (currentSchema?.tables?.[0]) {
for (const table of currentSchema.tables) {
if (!table?.tableName)
continue;
const doesTableExist = schemaTables.find((tableName) => tableName === table.tableName);
if (!doesTableExist) {
tablesToDrop.push(table.tableName);
}
}
}
for (const tableName of tablesToDrop) {
console.log(`Dropping table: ${tableName}`);
await this.run(`DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`);
await this.removeDbManagerTable(tableName);
}
}
async syncTable(table, existingTables) {
let tableExists = existingTables.includes(table.tableName);
const liveTables = await this.getLiveTableNames();
if (table.tableNameOld && table.tableNameOld !== table.tableName) {
if (liveTables.includes(table.tableNameOld)) {
console.log(`Renaming table: ${table.tableNameOld} -> ${table.tableName}`);
await this.run(`RENAME TABLE ${this.quoteIdentifier(table.tableNameOld)} TO ${this.quoteIdentifier(table.tableName)}`);
await this.insertDbManagerTable(table.tableName);
await this.removeDbManagerTable(table.tableNameOld);
tableExists = true;
}
}
if (!tableExists) {
await this.createTable(table);
await this.insertDbManagerTable(table.tableName);
}
else {
await this.updateTable(table);
await this.insertDbManagerTable(table.tableName);
}
await this.syncIndexes(table);
}
resolveTable(table) {
if (!table.parentTableName) {
return _.cloneDeep(table);
}
const parentTable = this.db_schema.tables.find((schemaTable) => schemaTable.tableName === table.parentTableName);
if (!parentTable) {
throw new Error(`Parent table \`${table.parentTableName}\` not found for \`${table.tableName}\``);
}
return _.merge({}, parentTable, {
tableName: table.tableName,
tableDescription: table.tableDescription,
collation: table.collation,
fields: [...(parentTable.fields || []), ...(table.fields || [])],
indexes: [...(parentTable.indexes || []), ...(table.indexes || [])],
uniqueConstraints: [
...(parentTable.uniqueConstraints || []),
...(table.uniqueConstraints || []),
],
});
}
async createTable(table) {
if (!table.tableName.match(/_temp_\d+$/)) {
console.log(`Creating table: ${table.tableName}`);
}
const newTable = this.resolveTable(table);
const columnDefinitions = [];
const foreignKeys = [];
for (const field of newTable.fields || []) {
columnDefinitions.push(this.buildColumnDefinition(field));
if (field.foreignKey && !newTable.isVector) {
foreignKeys.push(this.buildForeignKeyConstraint(field));
}
}
if (newTable.uniqueConstraints) {
for (const constraint of newTable.uniqueConstraints) {
if (constraint.constraintTableFields &&
constraint.constraintTableFields.length > 0) {
const fields = constraint.constraintTableFields
.map((field) => this.quoteIdentifier(field.value))
.join(", ");
const constraintName = constraint.constraintName ||
`unique_${fields.replace(/`/g, "")}`;
columnDefinitions.push(`CONSTRAINT ${this.quoteIdentifier(constraintName)} UNIQUE (${fields})`);
}
}
}
const sql = `CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(newTable.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${this.buildTableOptions(newTable)}`;
await this.run(sql);
}
buildTableOptions(table) {
const options = ["ENGINE=InnoDB"];
if (table.collation) {
options.push("DEFAULT CHARSET=utf8mb4", `COLLATE ${table.collation}`);
}
return ` ${options.join(" ")}`;
}
async updateTable(table) {
console.log(`Updating table: ${table.tableName}`);
await this.recreateTable(table);
}
async getTableColumns(tableName) {
const tableSchemaWhere = this.tableSchemaWhere(tableName);
const rows = await this.query(`SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type FROM information_schema.COLUMNS WHERE ${tableSchemaWhere.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`, tableSchemaWhere.values);
return rows.map((row) => ({
name: row.COLUMN_NAME,
type: row.COLUMN_TYPE,
}));
}
async addColumn(tableName, field) {
console.log(`Adding column: ${tableName}.${field.fieldName}`);
const columnDef = this.buildColumnDefinition(field)
.replace(/PRIMARY KEY/gi, "")
.replace(/AUTO_INCREMENT/gi, "")
.replace(/UNIQUE/gi, "")
.trim();
await this.run(`ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${columnDef}`);
}
async checkIfTableExists(table) {
const tableSchemaWhere = this.tableSchemaWhere(table);
const row = await this.query(`SELECT 1 AS exists FROM information_schema.TABLES WHERE ${tableSchemaWhere.where} AND TABLE_NAME = ? LIMIT 1`, tableSchemaWhere.values);
return Boolean(row[0]?.exists);
}
async recreateTable(table) {
if (table.isVector && !this.recreate_vector_table) {
return;
}
const doesTableExist = await this.checkIfTableExists(table.tableName);
if (table.isVector) {
let existingRows = [];
if (doesTableExist) {
existingRows = await this.query(`SELECT * FROM ${this.quoteIdentifier(table.tableName)}`);
await this.run(`DROP TABLE ${this.quoteIdentifier(table.tableName)}`);
}
await this.createTable(table);
if (existingRows.length > 0) {
await this.insertRows(table.tableName, existingRows);
}
return;
}
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
const existingColumns = await this.getTableColumns(table.tableName);
const columnsToKeep = table.fields
.filter((field) => existingColumns.some((column) => column.name === field.fieldName))
.map((field) => field.fieldName)
.filter((fieldName) => Boolean(fieldName));
await this.createTable({ ...table, tableName: tempTableName });
if (columnsToKeep.length > 0) {
const columnList = columnsToKeep
.map((column) => this.quoteIdentifier(column))
.join(", ");
await this.run(`INSERT INTO ${this.quoteIdentifier(tempTableName)} (${columnList}) SELECT ${columnList} FROM ${this.quoteIdentifier(table.tableName)}`);
}
await this.run(`DROP TABLE ${this.quoteIdentifier(table.tableName)}`);
await this.run(`RENAME TABLE ${this.quoteIdentifier(tempTableName)} TO ${this.quoteIdentifier(table.tableName)}`);
}
async insertRows(tableName, rows) {
for (const row of rows) {
const columns = Object.keys(row);
if (columns.length === 0) {
continue;
}
const values = columns.map((column) => row[column]);
const columnList = columns
.map((column) => this.quoteIdentifier(column))
.join(", ");
const placeholders = columns.map(() => "?").join(", ");
await this.run(`INSERT INTO ${this.quoteIdentifier(tableName)} (${columnList}) VALUES (${placeholders})`, values);
}
}
buildColumnDefinition(field) {
if (!field.fieldName) {
throw new Error("Field name is required");
}
const parts = [this.quoteIdentifier(field.fieldName)];
parts.push(this.mapDataType(field));
if (field.primaryKey) {
parts.push("PRIMARY KEY");
if (field.autoIncrement) {
parts.push("AUTO_INCREMENT");
}
}
if (field.notNullValue || field.primaryKey) {
if (!field.primaryKey) {
parts.push("NOT NULL");
}
}
if (field.unique && !field.primaryKey) {
parts.push("UNIQUE");
}
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "string") {
parts.push(`DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`);
}
else {
parts.push(`DEFAULT ${field.defaultValue}`);
}
}
else if (field.defaultValueLiteral) {
parts.push(`DEFAULT ${field.defaultValueLiteral}`);
}
if (field.onUpdate) {
parts.push(`ON UPDATE ${field.onUpdate}`);
}
else if (field.onUpdateLiteral) {
parts.push(`ON UPDATE ${field.onUpdateLiteral}`);
}
return parts.join(" ");
}
mapDataType(field) {
const dataType = field.dataType?.toLowerCase() || "text";
const vectorSize = field.vectorSize || 1536;
if (field.isVector) {
return `LONGTEXT COMMENT 'vector_size=${vectorSize}'`;
}
if (dataType.includes("int") ||
dataType === "bigint" ||
dataType === "smallint" ||
dataType === "tinyint") {
if (field.integerLength) {
return `INT(${field.integerLength})`;
}
return "INT";
}
if (dataType === "bigint") {
if (field.integerLength) {
return `BIGINT(${field.integerLength})`;
}
return "BIGINT";
}
if (dataType === "smallint") {
if (field.integerLength) {
return `SMALLINT(${field.integerLength})`;
}
return "SMALLINT";
}
if (dataType === "tinyint") {
if (field.integerLength) {
return `TINYINT(${field.integerLength})`;
}
return "TINYINT";
}
if (dataType.includes("double")) {
return "DOUBLE";
}
if (dataType.includes("float")) {
return "FLOAT";
}
if (dataType.includes("decimal") || dataType.includes("numeric")) {
if (field.integerLength && field.decimals) {
return `DECIMAL(${field.integerLength}, ${field.decimals})`;
}
return "DECIMAL";
}
if (dataType.includes("blob") || dataType.includes("binary")) {
return "BLOB";
}
if (dataType === "boolean" || dataType === "bool") {
return "TINYINT(1)";
}
if (dataType.includes("timestamp")) {
return "TIMESTAMP";
}
if (dataType.includes("datetime")) {
return "DATETIME";
}
if (dataType.includes("date") || dataType.includes("time")) {
return "DATETIME";
}
if (dataType.includes("varchar")) {
if (field.integerLength) {
return `VARCHAR(${field.integerLength})`;
}
return "VARCHAR(255)";
}
return "TEXT";
}
buildForeignKeyConstraint(field) {
const fk = field.foreignKey;
const constraintName = fk.foreignKeyName
? `CONSTRAINT ${this.quoteIdentifier(fk.foreignKeyName)} `
: "";
let constraint = `${constraintName}FOREIGN KEY (${this.quoteIdentifier(field.fieldName)}) REFERENCES ${this.quoteIdentifier(fk.destinationTableName)}(${this.quoteIdentifier(fk.destinationTableColumnName)})`;
if (fk.cascadeDelete) {
constraint += " ON DELETE CASCADE";
}
if (fk.cascadeUpdate) {
constraint += " ON UPDATE CASCADE";
}
return constraint;
}
async syncIndexes(table) {
if (!table.indexes || table.indexes.length === 0) {
return;
}
const tableSchemaWhere = this.tableSchemaWhere(table.tableName);
const rows = await this.query(`SELECT INDEX_NAME AS name FROM information_schema.STATISTICS WHERE ${tableSchemaWhere.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY' GROUP BY INDEX_NAME ORDER BY INDEX_NAME`, tableSchemaWhere.values);
const existingIndexes = rows.map((row) => row.name);
for (const indexName of existingIndexes) {
const stillExists = table.indexes.some((index) => index.indexName === indexName);
if (!stillExists) {
console.log(`Dropping index: ${indexName}`);
await this.run(`DROP INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteIdentifier(table.tableName)}`);
}
}
for (const index of table.indexes) {
if (!index.indexName ||
!index.indexTableFields ||
index.indexTableFields.length === 0) {
continue;
}
if (!existingIndexes.includes(index.indexName)) {
console.log(`Creating index: ${index.indexName}`);
const fields = index.indexTableFields
.map((field) => this.quoteIdentifier(field))
.join(", ");
await this.run(`CREATE INDEX ${this.quoteIdentifier(index.indexName)} ON ${this.quoteIdentifier(table.tableName)} (${fields})`);
}
}
}
close() { }
}
export { MariaDBSchemaManager };
+2 -2
View File
@@ -1,7 +1,7 @@
import type { BUN_MARIADB_DatabaseSchemaType, BunSQLiteConfig } from "../../types"; import type { BUN_MARIADB_DatabaseSchemaType, BunMariaDBConfig } from "../../types";
type Params = { type Params = {
dbSchema: BUN_MARIADB_DatabaseSchemaType; dbSchema: BUN_MARIADB_DatabaseSchemaType;
config: BunSQLiteConfig; config: BunMariaDBConfig;
}; };
export default function dbSchemaToType({ config, dbSchema, }: Params): string[] | undefined; export default function dbSchemaToType({ config, dbSchema, }: Params): string[] | undefined;
export {}; export {};
+1 -1
View File
@@ -4,7 +4,7 @@ export default function dbSchemaToType({ config, dbSchema, }) {
let datasquirelSchema = dbSchema; let datasquirelSchema = dbSchema;
if (!datasquirelSchema) if (!datasquirelSchema)
return; return;
let tableNames = `export const BunSQLiteTables = [\n${datasquirelSchema.tables let tableNames = `export const BunMariaDBTables = [\n${datasquirelSchema.tables
.map((tbl) => ` "${tbl.tableName}",`) .map((tbl) => ` "${tbl.tableName}",`)
.join("\n")}\n] as const`; .join("\n")}\n] as const`;
const dbTablesSchemas = datasquirelSchema.tables; const dbTablesSchemas = datasquirelSchema.tables;
+3 -2
View File
@@ -1,4 +1,4 @@
import type { APIResponseObject, ServerQueryParam } from "../../types"; import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types";
type Params<Schema extends { type Params<Schema extends {
[k: string]: any; [k: string]: any;
} = { } = {
@@ -8,10 +8,11 @@ type Params<Schema extends {
table: Table; table: Table;
count?: boolean; count?: boolean;
targetId?: number | string; targetId?: number | string;
config?: BunMariaDBConfig;
}; };
export default function DbSelect<Schema extends { export default function DbSelect<Schema extends {
[k: string]: any; [k: string]: any;
} = { } = {
[k: string]: any; [k: string]: any;
}, Table extends string = string>({ table, query, count, targetId, }: Params<Schema, Table>): Promise<APIResponseObject<Schema>>; }, Table extends string = string>({ table, query, count, targetId, config, }: Params<Schema, Table>): Promise<DBResponseObject<Schema>>;
export {}; export {};
+54 -18
View File
@@ -1,8 +1,10 @@
import mysql from "mysql"; import dbHandler from "../db-handler";
import DbClient from ".";
import _ from "lodash"; import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator"; import sqlGenerator from "../../utils/sql-generator";
export default async function DbSelect({ table, query, count, targetId, }) { function quoteIdentifier(identifier) {
return `\`${identifier.replace(/`/g, "``")}\``;
}
export default async function DbSelect({ table, query, count, targetId, config, }) {
let sqlObj = null; let sqlObj = null;
try { try {
let finalQuery = query || {}; let finalQuery = query || {};
@@ -16,33 +18,67 @@ export default async function DbSelect({ table, query, count, targetId, }) {
}); });
} }
sqlObj = sqlGenerator({ sqlObj = sqlGenerator({
tableName: table, tableName: quoteIdentifier(table),
genObject: finalQuery, genObject: finalQuery,
}); });
let sql = mysql.format(sqlObj.string, sqlObj.values); const res = await dbHandler({
const res = DbClient.query(sql); query: sqlObj.string,
const batchRes = res.all(); values: sqlObj.values,
config,
});
if (!res.success) {
return {
success: false,
msg: "Database select failed",
debug: {
sqlObj,
sql: sqlObj.string,
},
};
}
const batchRes = (res.payload || []);
let resp = { let resp = {
success: Boolean(batchRes[0]), success: true,
payload: batchRes, payload: batchRes,
singleRes: batchRes[0], single_res: batchRes[0],
debug: { debug: {
sqlObj, sqlObj,
sql, sql: sqlObj.string,
}, },
}; };
if (count) { if (count) {
let count_sql_object = sqlGenerator({ const countSqlObject = sqlGenerator({
tableName: table, tableName: quoteIdentifier(table),
genObject: finalQuery, genObject: finalQuery,
count, count,
}); });
let count_sql = mysql.format(count_sql_object.string, count_sql_object.values); const countSql = `SELECT COUNT(*) AS count FROM (${countSqlObject.string}) AS c`;
count_sql = `SELECT COUNT(*) FROM (${count_sql}) as c`; const countRes = await dbHandler({
const count_res = DbClient.query(count_sql).all(); query: countSql,
const count_val = count_res[0]?.["COUNT(*)"]; values: countSqlObject.values,
resp["count"] = Number(count_val); config,
resp["debug"]["count_sql"] = count_sql; });
if (!countRes.success) {
return {
...resp,
success: false,
msg: "Database count failed",
debug: {
...resp.debug,
count_sql: countSql,
},
};
}
const countRows = countRes.payload || [];
const countVal = countRows[0]?.count ?? countRows[0]?.["count"];
resp = {
...resp,
count: Number(countVal),
debug: {
...resp.debug,
count_sql: countSql,
},
};
} }
return resp; return resp;
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import type { APIResponseObject, SQLInsertGenValueType } from "../../types"; import type { DBResponseObject, SQLInsertGenValueType } from "../../types";
type Params = { type Params = {
sql: string; sql: string;
values?: SQLInsertGenValueType[]; values?: SQLInsertGenValueType[];
@@ -7,5 +7,5 @@ export default function DbSQL<T extends {
[k: string]: any; [k: string]: any;
} = { } = {
[k: string]: any; [k: string]: any;
}>({ sql, values }: Params): Promise<APIResponseObject<T>>; }>({ sql, values }: Params): Promise<DBResponseObject<T>>;
export {}; export {};
+26 -15
View File
@@ -1,24 +1,35 @@
import DbClient from "."; import dbHandler from "../db-handler";
import _ from "lodash";
export default async function DbSQL({ sql, values }) { export default async function DbSQL({ sql, values }) {
try { try {
const trimmed_sql = sql.trim(); const trimmedSql = sql.trim();
const res = trimmed_sql.match(/^select/i) const isSelect = trimmedSql.match(/^select/i);
? DbClient.query(trimmed_sql).all(...(values || [])) const res = await dbHandler({
: DbClient.run(trimmed_sql, values || []); query: trimmedSql,
values: values,
});
if (!res.success) {
return {
success: false,
msg: "Database query failed",
debug: {
sqlObj: {
sql: trimmedSql,
values,
},
sql,
},
};
}
const payload = isSelect ? (res.payload || []) : undefined;
const single_res = isSelect ? payload?.[0] : res.single_res;
const singleRaw = res.single_res;
return { return {
success: true, success: true,
payload: Array.isArray(res) ? res : undefined, payload,
singleRes: Array.isArray(res) ? res?.[0] : undefined, single_res,
postInsertReturn: Array.isArray(res)
? undefined
: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: { debug: {
sqlObj: { sqlObj: {
sql: trimmed_sql, sql: trimmedSql,
values, values,
}, },
sql, sql,
+3 -2
View File
@@ -1,4 +1,4 @@
import type { APIResponseObject, ServerQueryParam } from "../../types"; import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types";
type Params<Schema extends { type Params<Schema extends {
[k: string]: any; [k: string]: any;
} = { } = {
@@ -8,10 +8,11 @@ type Params<Schema extends {
data: Schema; data: Schema;
query?: ServerQueryParam<Schema>; query?: ServerQueryParam<Schema>;
targetId?: number | string; targetId?: number | string;
config?: BunMariaDBConfig;
}; };
export default function DbUpdate<Schema extends { export default function DbUpdate<Schema extends {
[k: string]: any; [k: string]: any;
} = { } = {
[k: string]: any; [k: string]: any;
}, Table extends string = string>({ table, data, query, targetId, }: Params<Schema, Table>): Promise<APIResponseObject>; }, Table extends string = string>({ table, data, query, targetId, config, }: Params<Schema, Table>): Promise<DBResponseObject>;
export {}; export {};
+75 -40
View File
@@ -1,7 +1,11 @@
import DbClient from "."; import dbHandler from "../db-handler";
import _ from "lodash"; import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator"; import sqlGenerator from "../../utils/sql-generator";
export default async function DbUpdate({ table, data, query, targetId, }) { import sanitizeHtmlFields from "../../utils/sanitize-html-fields";
function quoteIdentifier(identifier) {
return `\`${identifier.replace(/`/g, "``")}\``;
}
export default async function DbUpdate({ table, data, query, targetId, config, }) {
let sqlObj = { string: "", values: [] }; let sqlObj = { string: "", values: [] };
try { try {
let finalQuery = query || {}; let finalQuery = query || {};
@@ -15,52 +19,83 @@ export default async function DbUpdate({ table, data, query, targetId, }) {
}); });
} }
const sqlQueryObj = sqlGenerator({ const sqlQueryObj = sqlGenerator({
tableName: table, tableName: quoteIdentifier(table),
genObject: finalQuery, genObject: finalQuery,
}); });
let values = [];
const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0]; const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0];
if (whereClause) { if (!whereClause) {
let sql = `UPDATE ${table} SET`;
const finalData = {
updated_at: Date.now(),
...data,
};
const keys = Object.keys(finalData);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!key)
continue;
const isLast = i == keys.length - 1;
sql += ` ${key}=?`;
const value = finalData[key];
values.push(value || null);
if (!isLast) {
sql += `,`;
}
}
sql += ` ${whereClause}`;
values = [...values, ...sqlQueryObj.values];
sqlObj.string = sql;
sqlObj.values = values;
const res = DbClient.run(sql, values);
return {
success: Boolean(res.changes),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sqlObj,
},
};
}
else {
return { return {
success: false, success: false,
msg: `No WHERE clause`, msg: `No WHERE clause`,
}; };
} }
let values = [];
let sql = ``;
sql += `UPDATE ${quoteIdentifier(table)} SET`;
const sanitizedData = sanitizeHtmlFields({
table,
data,
config,
});
const finalData = {
updated_at: Date.now(),
...sanitizedData,
};
const keys = Object.keys(finalData);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!key)
continue;
const isLast = i == keys.length - 1;
sql += ` ${quoteIdentifier(key)}=?`;
values.push(finalData[key] ?? null);
if (!isLast) {
sql += `,`;
}
}
sql += ` ${whereClause}`;
values = [...values, ...sqlQueryObj.values];
const res = await dbHandler({
query: sql,
values: values,
config,
});
sqlObj.string = sql;
sqlObj.values = values;
let updated_sql = ``;
let updated_sql_values = [];
updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`;
updated_sql_values = [...updated_sql_values, ...sqlQueryObj.values];
updated_sql += ` AND `;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!key)
continue;
if (key == "updated_at")
continue;
const isLast = i == keys.length - 1;
updated_sql += ` ${quoteIdentifier(key)}=?`;
updated_sql_values.push(finalData[key] ?? null);
if (!isLast) {
updated_sql += ` AND `;
}
}
const updated_res = await dbHandler({
query: updated_sql,
values: updated_sql_values,
config,
});
const affected_rows = updated_res.payload?.length;
return {
...res,
success: Boolean(affected_rows),
insert_return: {
affected_rows,
},
debug: {
sqlObj,
},
};
} }
catch (error) { catch (error) {
return { return {
+2 -2
View File
@@ -1,8 +1,8 @@
import type { BUN_MARIADB_DatabaseSchemaType, BunSQLiteConfig } from "../../types"; import type { BUN_MARIADB_DatabaseSchemaType, BunMariaDBConfig } from "../../types";
type Params = { type Params = {
dbSchema: BUN_MARIADB_DatabaseSchemaType; dbSchema: BUN_MARIADB_DatabaseSchemaType;
dst_file: string; dst_file: string;
config: BunSQLiteConfig; config: BunMariaDBConfig;
}; };
export default function dbSchemaToTypeDef({ dbSchema, dst_file, config, }: Params): void; export default function dbSchemaToTypeDef({ dbSchema, dst_file, config, }: Params): void;
export {}; export {};
-2
View File
@@ -1,2 +0,0 @@
import type { BUN_MARIADB_DatabaseSchemaType } from "../../types";
export declare const DbSchema: BUN_MARIADB_DatabaseSchemaType;
-5
View File
@@ -1,5 +0,0 @@
import _ from "lodash";
export const DbSchema = {
dbName: "travis-ai",
tables: [],
};
+2
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
export default function buildColumnDefinition(field: BUN_MARIADB_FieldSchemaType): string;
+43
View File
@@ -0,0 +1,43 @@
import isVectorField from "./is-vector-field";
import mapDataType from "./map-data-types";
import MariaDBQuoteGen from "./mariadb-quote-gen";
export default function buildColumnDefinition(field) {
if (!field.fieldName) {
throw new Error("Field name is required");
}
const parts = [MariaDBQuoteGen(field.fieldName)];
parts.push(mapDataType(field));
if (field.autoIncrement) {
parts.push("AUTO_INCREMENT");
}
// Vector columns used in VECTOR INDEX must be NOT NULL
if (field.notNullValue ||
field.primaryKey ||
isVectorField(field)) {
if (!field.primaryKey) {
parts.push("NOT NULL");
}
}
// VECTOR columns cannot be UNIQUE in the usual sense
if (field.unique && !field.primaryKey && !isVectorField(field)) {
parts.push("UNIQUE");
}
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "string") {
parts.push(`DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`);
}
else {
parts.push(`DEFAULT ${field.defaultValue}`);
}
}
else if (field.defaultValueLiteral) {
parts.push(`DEFAULT ${field.defaultValueLiteral}`);
}
if (field.onUpdate) {
parts.push(`ON UPDATE ${field.onUpdate}`);
}
else if (field.onUpdateLiteral) {
parts.push(`ON UPDATE ${field.onUpdateLiteral}`);
}
return parts.join(" ");
}
+2
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
export default function buildForeignKeyConstraint(field: BUN_MARIADB_FieldSchemaType): string;
+15
View File
@@ -0,0 +1,15 @@
import MariaDBQuoteGen from "./mariadb-quote-gen";
export default function buildForeignKeyConstraint(field) {
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;
}
+2
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_TableSchemaType } from "../../types";
export default function buildTableOptions(table: BUN_MARIADB_TableSchemaType): string;
+7
View File
@@ -0,0 +1,7 @@
export default function buildTableOptions(table) {
const options = ["ENGINE=InnoDB"];
if (table.collation) {
options.push("DEFAULT CHARSET=utf8mb4", `COLLATE ${table.collation}`);
}
return ` ${options.join(" ")}`;
}
+2
View File
@@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function createDBManagerTable({ db_schema, config, }: CreateDBSchemaParams): Promise<void>;
+15
View File
@@ -0,0 +1,15 @@
import { AppData } from "../../data/app-data";
import dbHandler from "../db-handler";
import MariaDBQuoteGen from "./mariadb-quote-gen";
export default async function createDBManagerTable({ db_schema, config, }) {
let sql = ``;
sql += `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} (\n`;
sql += ` table_name VARCHAR(255) NOT NULL PRIMARY KEY,\n`;
sql += ` created_at BIGINT NOT NULL,\n`;
sql += ` updated_at BIGINT NOT NULL\n`;
sql += `) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci\n`;
await dbHandler({
query: sql,
config,
});
}
+2
View File
@@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function createDBSchema(params: CreateDBSchemaParams): Promise<void>;
+17
View File
@@ -0,0 +1,17 @@
import createDBManagerTable from "./create-db-manager-table";
import handleDBSchemaTables from "./handle-db-schema-tables";
import orderDBSchema from "./order-db-schema";
export default async function createDBSchema(params) {
/**
* Create Schema Manager Table
*/
await createDBManagerTable(params);
/**
* Reorder Tables (parents before children with FKs)
*/
const ordered_db_schema = await orderDBSchema(params);
/**
* Handle Tables (create / update / drop)
*/
await handleDBSchemaTables({ ...params, db_schema: ordered_db_schema });
}
+5
View File
@@ -0,0 +1,5 @@
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
export default function createTable({ table, config, }: {
table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig;
}): Promise<void>;
+41
View File
@@ -0,0 +1,41 @@
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, }) {
if (!table.tableName.match(/_temp_\d+$/)) {
console.log(`Creating table: ${table.tableName}`);
}
const columnDefinitions = [];
const foreignKeys = [];
const primaryKeys = [];
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 });
}
@@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function getExistingTablesFromTablesManagerTable({ config, }: CreateDBSchemaParams): Promise<(string | undefined)[]>;
@@ -0,0 +1,10 @@
import { AppData } from "../../data/app-data";
import dbHandler from "../db-handler";
import MariaDBQuoteGen from "./mariadb-quote-gen";
export default async function getExistingTablesFromTablesManagerTable({ config, }) {
const rows = await dbHandler({
query: `SELECT table_name FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])}`,
config,
});
return rows.payload?.map((row) => row.table_name) || [];
}
+10
View File
@@ -0,0 +1,10 @@
import type { BunMariaDBConfig } from "../../types";
export type ColumnInfoRow = {
name: string;
type: string;
comment?: string;
};
export default function getTableColumns({ tableName, config, }: {
tableName: string;
config?: BunMariaDBConfig;
}): Promise<ColumnInfoRow[]>;
+15
View File
@@ -0,0 +1,15 @@
import { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
export default async function getTableColumns({ tableName, config, }) {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows({
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,
}));
}
+2
View File
@@ -0,0 +1,2 @@
import type { CreateDBSchemaTableHandlerParams } from "../../types";
export default function handleDBSchemaTable({ db_schema, config, table, db_manager_table_name, existing_live_table, }: CreateDBSchemaTableHandlerParams): Promise<void>;
+62
View File
@@ -0,0 +1,62 @@
import createTable from "./create-table";
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({ db_schema, config, table, db_manager_table_name, existing_live_table, }) {
const resolvedTable = resolveTable(table, db_schema);
let tableExistsTracked = Boolean(db_manager_table_name);
let tableExistsLive = Boolean(existing_live_table?.TABLE_NAME);
let wasRenamed = false;
if (resolvedTable.tableNameOld &&
resolvedTable.tableNameOld !== resolvedTable.tableName) {
// Only hit information_schema when a rename is declared
const schemaCond = schemaCondition(config);
const liveTables = await querySchemaRows({
query: `SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = ?`,
values: [...schemaCond.values, resolvedTable.tableNameOld],
config,
});
if (liveTables.length > 0) {
console.log(`Renaming table: ${resolvedTable.tableNameOld} -> ${resolvedTable.tableName}`);
await runSchemaQuery({
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;
}
}
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 });
}
+2
View File
@@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function handleDBSchemaTables(params: CreateDBSchemaParams): Promise<void>;
+93
View File
@@ -0,0 +1,93 @@
import _ from "lodash";
import { AppData } from "../../data/app-data";
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(params) {
const { db_schema, config } = params;
/**
* Grab Tables that have been recorded in the Schema
* Manager Table
*/
const existing_schema_tables = await getExistingTablesFromTablesManagerTable(params);
const schemaCond = schemaCondition(config);
const existing_live_tables = await querySchemaRows({
query: `SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME != ?`,
values: [...schemaCond.values, AppData["DbSchemaManagerTableName"]],
config,
});
for (let i = 0; i < db_schema.tables.length; i++) {
const table = db_schema.tables[i];
if (!table)
continue;
const existing_table = existing_schema_tables.find((t) => t == table.tableName);
const existing_live_table = existing_live_tables.find((t) => t.TABLE_NAME == table.tableName);
await handleDBSchemaTable({
...params,
table,
db_manager_table_name: existing_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) => Boolean(tableName) && !schemaTableNames.includes(tableName)));
if (tablesToDrop.length === 0) {
return;
}
const fkRows = await querySchemaRows({
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();
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 = [];
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 });
}
}
+5
View File
@@ -0,0 +1,5 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
/**
* True when a field is a MariaDB native vector column.
*/
export default function isVectorField(field?: BUN_MARIADB_FieldSchemaType): boolean;
+8
View File
@@ -0,0 +1,8 @@
/**
* True when a field is a MariaDB native vector column.
*/
export default function isVectorField(field) {
if (!field)
return false;
return field.isVector === true || field.dataType === "VECTOR";
}
+2
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
export default function mapDataType(field: BUN_MARIADB_FieldSchemaType): string;
+93
View File
@@ -0,0 +1,93 @@
export default function mapDataType(field) {
const dataType = field.dataType?.toUpperCase() || "TEXT";
const vectorSize = field.vectorSize || 1536;
// Native MariaDB VECTOR type (11.7+). Prefer this over LONGTEXT storage.
if (field.isVector || dataType === "VECTOR") {
return `VECTOR(${vectorSize})`;
}
switch (dataType) {
case "CHAR":
return `CHAR(${field.integerLength || 255})`;
case "VARCHAR":
return `VARCHAR(${field.integerLength || 255})`;
case "TEXT":
return "TEXT";
case "TINYTEXT":
return "TINYTEXT";
case "MEDIUMTEXT":
return "MEDIUMTEXT";
case "LONGTEXT":
return "LONGTEXT";
case "TINYINT":
return field.integerLength
? `TINYINT(${field.integerLength})`
: "TINYINT";
case "SMALLINT":
return field.integerLength
? `SMALLINT(${field.integerLength})`
: "SMALLINT";
case "MEDIUMINT":
return field.integerLength
? `MEDIUMINT(${field.integerLength})`
: "MEDIUMINT";
case "INT":
return field.integerLength ? `INT(${field.integerLength})` : "INT";
case "BIGINT":
return field.integerLength
? `BIGINT(${field.integerLength})`
: "BIGINT";
case "FLOAT":
return "FLOAT";
case "DOUBLE":
return "DOUBLE";
case "DECIMAL":
if (field.integerLength && field.decimals) {
return `DECIMAL(${field.integerLength}, ${field.decimals})`;
}
return "DECIMAL(10,2)";
case "BINARY":
return `BINARY(${field.integerLength || 1})`;
case "VARBINARY":
return `VARBINARY(${field.integerLength || 255})`;
case "BLOB":
return "BLOB";
case "TINYBLOB":
return "TINYBLOB";
case "MEDIUMBLOB":
return "MEDIUMBLOB";
case "LONGBLOB":
return "LONGBLOB";
case "DATE":
return "DATE";
case "TIME":
return "TIME";
case "DATETIME":
return "DATETIME";
case "TIMESTAMP":
return "TIMESTAMP";
case "YEAR":
return "YEAR";
case "UUID":
return "CHAR(36)"; // MariaDB does not have a native UUID type
case "JSON":
return "JSON";
case "INET6":
return "INET6";
case "BOOLEAN":
return "TINYINT(1)";
case "ENUM": {
const enumVals = (field.options || [])
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
.join(", ");
return `ENUM(${enumVals || "''"})`;
}
case "SET": {
const setVals = (field.options || [])
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
.join(", ");
return `SET(${setVals || "''"})`;
}
default:
return "TEXT";
}
}
+1
View File
@@ -0,0 +1 @@
export default function MariaDBQuoteGen(str: string): string;
+3
View File
@@ -0,0 +1,3 @@
export default function MariaDBQuoteGen(str) {
return `\`${str.replace(/`/g, "``")}\``;
}
+2
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_DatabaseSchemaType, CreateDBSchemaParams } from "../../types";
export default function orderDBSchema({ db_schema, }: CreateDBSchemaParams): Promise<BUN_MARIADB_DatabaseSchemaType>;
+57
View File
@@ -0,0 +1,57 @@
import _ from "lodash";
export default async function orderDBSchema({ db_schema, }) {
let new_db_schema = _.cloneDeep(db_schema);
const tables = new_db_schema.tables;
let new_tables_set = new Set();
let new_tables_start_set = new Set();
let new_tables_end_set = new Set();
function setParentTable(table) {
const fields = table.fields;
let does_table_have_foreign_keys = false;
for (let f = 0; f < fields.length; f++) {
const field = fields[f];
if (!field)
continue;
const dst_table_name = field.foreignKey?.destinationTableName;
if (dst_table_name) {
const fk_table = tables.find((tb) => tb.tableName == dst_table_name);
if (fk_table &&
!new_tables_start_set.has(fk_table) &&
!new_tables_end_set.has(fk_table)) {
setParentTable(fk_table);
}
new_tables_end_set.add(table);
if (fk_table) {
new_tables_start_set.add(fk_table);
}
does_table_have_foreign_keys = true;
}
}
return { does_table_have_foreign_keys };
}
for (let i = 0; i < tables.length; i++) {
const table = tables[i];
if (!table) {
continue;
}
let { does_table_have_foreign_keys } = setParentTable(table);
if (does_table_have_foreign_keys) {
new_tables_end_set.add(table);
}
else {
new_tables_start_set.add(table);
}
}
const parsed_tables = [
...Array.from(new_tables_start_set),
...Array.from(new_tables_end_set),
];
for (let nt = 0; nt < parsed_tables.length; nt++) {
const new_table = parsed_tables[nt];
if (new_table) {
new_tables_set.add(new_table);
}
}
new_db_schema.tables = Array.from(new_tables_set);
return new_db_schema;
}
+9
View File
@@ -0,0 +1,9 @@
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
/**
* Full table rebuild. For `isVector` tables this drops and recreates in place
* (preserving rows when possible). For regular tables it uses a temp-table swap.
*/
export default function recreateTable({ table, config, }: {
table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig;
}): Promise<void>;
+111
View File
@@ -0,0 +1,111 @@
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, }) {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows({
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);
}
/**
* Full table rebuild. For `isVector` tables this drops and recreates in place
* (preserving rows when possible). For regular tables it uses a temp-table swap.
*/
export default async function recreateTable({ table, config, }) {
const doesTableExist = await checkIfTableExists({
tableName: table.tableName,
config,
});
if (!doesTableExist) {
await createTable({ table, config });
return;
}
/**
* Vector tables: drop + recreate + reinsert (MariaDB VECTOR INDEX / dim
* changes are not reliably alterable in place).
*/
if (table.isVector) {
console.log(`Recreating vector table: ${table.tableName}`);
const existingRows = await querySchemaRows({
query: `SELECT * FROM ${MariaDBQuoteGen(table.tableName)}`,
config,
});
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
try {
await runSchemaQuery({
query: `DROP TABLE IF EXISTS ${MariaDBQuoteGen(table.tableName)}`,
config,
});
await createTable({ table, config });
}
finally {
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
}
if (existingRows.length > 0) {
const schemaFieldNames = new Set((table.fields || [])
.map((f) => f.fieldName)
.filter((n) => Boolean(n)));
for (const row of existingRows) {
const columns = Object.keys(row).filter((c) => schemaFieldNames.has(c));
if (columns.length === 0)
continue;
const placeholders = columns.map(() => "?").join(", ");
const columnList = columns
.map((c) => MariaDBQuoteGen(c))
.join(", ");
await runSchemaQuery({
query: `INSERT INTO ${MariaDBQuoteGen(table.tableName)} (${columnList}) VALUES (${placeholders})`,
values: columns.map((c) => row[c] ?? null),
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) => 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 });
}
}
+2
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_DatabaseSchemaType, BUN_MARIADB_TableSchemaType } from "../../types";
export default function resolveTable(table: BUN_MARIADB_TableSchemaType, db_schema: BUN_MARIADB_DatabaseSchemaType): BUN_MARIADB_TableSchemaType;
+34
View File
@@ -0,0 +1,34 @@
import _ from "lodash";
export default function resolveTable(table, db_schema) {
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();
(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 || []),
],
};
}
+11
View File
@@ -0,0 +1,11 @@
import type { BunMariaDBConfig } from "../../types";
export default function runSchemaQuery({ query, values, config, }: {
query: string;
values?: any[];
config?: BunMariaDBConfig;
}): Promise<void>;
export declare function querySchemaRows<T extends Record<string, any>>({ query, values, config, }: {
query: string;
values?: any[];
config?: BunMariaDBConfig;
}): Promise<T[]>;
+22
View File
@@ -0,0 +1,22 @@
import dbHandler from "../db-handler";
export default async function runSchemaQuery({ query, values, config, }) {
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({ query, values, config, }) {
const res = await dbHandler({
query,
values,
config,
});
if (!res.success) {
throw new Error(`Database query failed: ${query} ... ERROR: ${res.error || res.msg}`);
}
return (res.payload || []);
}
+5
View File
@@ -0,0 +1,5 @@
import type { BunMariaDBConfig } from "../../types";
export default function schemaCondition(config?: BunMariaDBConfig): {
where: string;
values: string[];
};
+13
View File
@@ -0,0 +1,13 @@
export default function schemaCondition(config) {
const databaseName = config?.db_name || global.CONFIG?.db_name;
if (databaseName) {
return {
where: "TABLE_SCHEMA = ?",
values: [databaseName],
};
}
return {
where: "TABLE_SCHEMA = DATABASE()",
values: [],
};
}
+5
View File
@@ -0,0 +1,5 @@
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
export default function syncIndexes({ table, config, }: {
table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig;
}): Promise<void>;
+131
View File
@@ -0,0 +1,131 @@
import isVectorField from "./is-vector-field";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
function isVectorIndexDef(index, table) {
if (index.indexType === "VECTOR")
return true;
if (table.isVector)
return true;
const firstFieldName = index.indexTableFields?.[0];
if (!firstFieldName)
return false;
const field = table.fields?.find((f) => f.fieldName === firstFieldName);
return isVectorField(field);
}
function vectorDistanceMetric(index) {
if (index.vectorDistanceMetric === "cosine")
return "cosine";
if (index.vectorDistanceMetric === "euclidean")
return "euclidean";
return "euclidean";
}
export default async function syncIndexes({ table, config, }) {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows({
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({
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();
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) {
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)) {
if (isVectorIndexDef(index, table)) {
console.log(`Creating Vector index: ${index.indexName}`);
const targetField = MariaDBQuoteGen(index.indexTableFields[0]);
const distanceMetric = vectorDistanceMetric(index);
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,
});
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
export default function updateTable({ table, config, }: {
table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig;
}): Promise<void>;
+177
View File
@@ -0,0 +1,177 @@
import buildColumnDefinition from "./build-column-definition";
import createTable from "./create-table";
import getTableColumns from "./get-table-columns";
import isVectorField from "./is-vector-field";
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, expectedType) {
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;
}
function vectorTypeDiverged(liveType, liveComment, field) {
const dimensions = field.vectorSize || 1536;
const expectedNative = `vector(${dimensions})`;
const live = liveType.toLowerCase().replace(/\s+/g, "");
if (live === expectedNative)
return false;
// Legacy LONGTEXT storage with vector_size comment
if (live.startsWith("longtext") || live.startsWith("text")) {
const match = liveComment.match(/vector_size\s*=\s*(\d+)/i);
if (match && Number(match[1]) === dimensions) {
// Still legacy storage — treat as diverged so we can migrate to VECTOR
return true;
}
return true;
}
return true;
}
async function addColumn({ tableName, field, config, }) {
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, }) {
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, }) {
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, }) {
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 = [];
const fieldsToModify = [];
const fieldsToDrop = [];
let needsVectorRecreate = false;
for (const field of table.fields || []) {
if (!field.fieldName)
continue;
const liveField = liveFieldsMap.get(field.fieldName);
if (!liveField) {
// Adding a new vector column can require rebuild if VECTOR INDEX
// constraints conflict; still try surgical add first.
fieldsToAdd.push(field);
}
else {
let typeDiverged = !columnTypesMatch(liveField.type, mapDataType(field));
if (isVectorField(field)) {
typeDiverged = vectorTypeDiverged(liveField.type, liveField.comment, field);
if (typeDiverged) {
needsVectorRecreate = true;
}
}
if (typeDiverged) {
fieldsToModify.push(field);
}
}
}
// Vector dimension / storage type changes → full rebuild automatically
if (needsVectorRecreate) {
console.log(`Vector column change detected on \`${table.tableName}\`; recreating table`);
await recreateTable({ table, config });
return;
}
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({
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({
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();
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) {
if (isVectorField(field)) {
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 });
}
}
+9
View File
@@ -0,0 +1,9 @@
import type { BunMariaDBConfig } from "../../types";
export default function upsertDbManagerTable({ tableName, config, }: {
tableName: string;
config?: BunMariaDBConfig;
}): Promise<void>;
export declare function removeDbManagerTable({ tableName, config, }: {
tableName: string;
config?: BunMariaDBConfig;
}): Promise<void>;
+18
View File
@@ -0,0 +1,18 @@
import { AppData } from "../../data/app-data";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import runSchemaQuery from "./run-schema-query";
export default async function upsertDbManagerTable({ tableName, config, }) {
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, }) {
await runSchemaQuery({
query: `DELETE FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} WHERE table_name = ?`,
values: [tableName],
config,
});
}
+153 -130
View File
@@ -1,5 +1,4 @@
import type { RequestOptions } from "https"; import type { RequestOptions } from "https";
import type { ConnectionConfig } from "mariadb";
/** /**
* Fully-qualified database name used when a database needs to be referenced * Fully-qualified database name used when a database needs to be referenced
* across local and remote operations. * across local and remote operations.
@@ -15,18 +14,8 @@ export declare const UsersOmitedFields: readonly ["password", "social_id", "veri
* database metadata. * database metadata.
*/ */
export interface BUN_MARIADB_DatabaseSchemaType { export interface BUN_MARIADB_DatabaseSchemaType {
id?: string | number;
dbName?: string;
dbSlug?: string;
dbFullName?: string;
dbDescription?: string;
dbImage?: string;
tables: BUN_MARIADB_TableSchemaType[]; tables: BUN_MARIADB_TableSchemaType[];
childrenDatabases?: BUN_MARIADB_ChildrenDatabaseObject[]; collation?: MariaDbCollationsType;
childDatabase?: boolean;
childDatabaseDbId?: string | number;
updateData?: boolean;
collation?: (typeof MariaDBCollations)[number];
} }
/** /**
* Minimal reference to a child database linked to a parent database schema. * Minimal reference to a child database linked to a parent database schema.
@@ -39,6 +28,9 @@ export interface BUN_MARIADB_ChildrenDatabaseObject {
* level. * level.
*/ */
export declare const MariaDBCollations: readonly ["utf8mb4_bin", "utf8mb4_unicode_520_ci"]; export declare const MariaDBCollations: readonly ["utf8mb4_bin", "utf8mb4_unicode_520_ci"];
export type MariaDbCollationsType = (typeof MariaDBCollations)[number];
export declare const MariaDBCharsets: readonly ["utf8mb4"];
export type MariaDbCharsetType = (typeof MariaDBCharsets)[number];
/** /**
* Describes a single table within a database schema, including its fields, * Describes a single table within a database schema, including its fields,
* indexes, unique constraints, and parent/child table relationships. * indexes, unique constraints, and parent/child table relationships.
@@ -83,13 +75,9 @@ export interface BUN_MARIADB_TableSchemaType {
childTableDbId?: string | number; childTableDbId?: string | number;
collation?: (typeof MariaDBCollations)[number]; collation?: (typeof MariaDBCollations)[number];
/** /**
* If this is a vector table * If this is a vector-oriented table (native MariaDB VECTOR columns/indexes)
*/ */
isVector?: boolean; isVector?: boolean;
/**
* Type of vector. Defaults to `vec0`
*/
vectorType?: string;
} }
/** /**
* Reference object used to link a table to one of its child tables. * Reference object used to link a table to one of its child tables.
@@ -104,9 +92,6 @@ export interface BUN_MARIADB_ChildrenTablesType {
export declare const TextFieldTypesArray: readonly [{ export declare const TextFieldTypesArray: readonly [{
readonly title: "Plain Text"; readonly title: "Plain Text";
readonly value: "plain"; readonly value: "plain";
}, {
readonly title: "Rich Text";
readonly value: "richText";
}, { }, {
readonly title: "Markdown"; readonly title: "Markdown";
readonly value: "markdown"; readonly value: "markdown";
@@ -133,16 +118,72 @@ export declare const TextFieldTypesArray: readonly [{
readonly value: "code"; readonly value: "code";
}]; }];
/** /**
* Core SQLite column types supported by the schema builder. * Native MariaDB column types supported by the schema builder.
*/ */
export declare const BUN_MARIADB_DATATYPES: readonly [{ export declare const BUN_MARIADB_DATATYPES: readonly [{
readonly value: "CHAR";
}, {
readonly value: "VARCHAR";
}, {
readonly value: "TEXT"; readonly value: "TEXT";
}, { }, {
readonly value: "INTEGER"; readonly value: "TINYTEXT";
}, {
readonly value: "MEDIUMTEXT";
}, {
readonly value: "LONGTEXT";
}, {
readonly value: "TINYINT";
}, {
readonly value: "SMALLINT";
}, {
readonly value: "MEDIUMINT";
}, {
readonly value: "INT";
}, {
readonly value: "BIGINT";
}, {
readonly value: "FLOAT";
}, {
readonly value: "DOUBLE";
}, {
readonly value: "DECIMAL";
}, {
readonly value: "BINARY";
}, {
readonly value: "VARBINARY";
}, { }, {
readonly value: "BLOB"; readonly value: "BLOB";
}, { }, {
readonly value: "REAL"; readonly value: "TINYBLOB";
}, {
readonly value: "MEDIUMBLOB";
}, {
readonly value: "LONGBLOB";
}, {
readonly value: "DATE";
}, {
readonly value: "TIME";
}, {
readonly value: "DATETIME";
}, {
readonly value: "TIMESTAMP";
}, {
readonly value: "YEAR";
}, {
readonly value: "BOOLEAN";
}, {
readonly value: "UUID";
}, {
readonly value: "JSON";
}, {
readonly value: "INET6";
}, {
readonly value: "ENUM";
}, {
readonly value: "SET";
}, {
readonly value: "VECTOR";
}]; }];
/** /**
* Describes a table column, including SQL constraints, text editor options, * Describes a table column, including SQL constraints, text editor options,
@@ -178,43 +219,6 @@ export type BUN_MARIADB_FieldSchemaType = {
options?: (string | number)[]; options?: (string | number)[];
isVector?: boolean; isVector?: boolean;
vectorSize?: number; vectorSize?: number;
/**
* ### Adds a `+` prefix to colums
* In sqlite-vec, the + prefix is a specialized syntax for Virtual Table Columns. It essentially tells the database: "Keep this data associated with the vector, but don't try to index it for math."
Here is the breakdown of why they matter and how they work:
1. Performance Separation
In a standard table, adding a massive TEXT column (like a 2,000-word article) slows down full-table scans. In a vec0 virtual table, columns prefixed with + are stored in a separate internal side-car table.
The Vector Index: Stays lean and fast for "Nearest Neighbor" math.
The Content: Is only fetched after the vector search identifies the winning rows.
2. The "No Join" Convenience
Normally, you would store vectors in one table and the actual text content in another, linking them with a FOREIGN KEY.
Without + columns: You must JOIN two tables to get the text after finding the vector.
With + columns: You can SELECT content directly from the virtual table. It handles the "join" logic internally, making your code cleaner.
3. Syntax Example
When defining your schema, the + is only used in the CREATE statement. When querying or inserting, you treat it like a normal name.
```sql
-- SCHEMA DEFINITION
CREATE VIRTUAL TABLE documents USING vec0(
embedding float, -- The vector (indexed)
+title TEXT, -- Side-car metadata (not indexed)
+raw_body TEXT -- Side-car "heavy" data (not indexed)
);
-- INSERTING (Notice: No '+' here)
INSERT INTO documents(embedding, title, raw_body)
VALUES (vec_f32(?), 'Bun Docs', 'Bun is a fast JavaScript runtime...');
-- QUERYING (Notice: No '+' here)
SELECT title, raw_body
FROM documents
WHERE embedding MATCH ? AND k = 1;
```
*/
sideCar?: boolean;
/**
* Eg. `cosine`
*/
vectorDistanceMetric?: string;
} & { } & {
[key in (typeof TextFieldTypesArray)[number]["value"]]?: boolean; [key in (typeof TextFieldTypesArray)[number]["value"]]?: boolean;
}; };
@@ -229,6 +233,8 @@ export interface BUN_MARIADB_ForeignKeyType {
cascadeDelete?: boolean; cascadeDelete?: boolean;
cascadeUpdate?: boolean; cascadeUpdate?: boolean;
} }
export declare const MariaDBIndexTypes: readonly ["BTREE", "HASH", "FULLTEXT", "SPATIAL", "VECTOR"];
export type MariaDBIndexType = (typeof MariaDBIndexTypes)[number];
/** /**
* Describes a table index and the fields it covers. * Describes a table index and the fields it covers.
*/ */
@@ -238,7 +244,22 @@ export interface BUN_MARIADB_IndexSchemaType {
* `idx_user_id_index` * `idx_user_id_index`
*/ */
indexName?: string; indexName?: string;
/**
* The columns included in the index.
*/
indexTableFields?: string[]; indexTableFields?: string[];
/**
* Under the hood index type (BTREE, HASH) or modifier (FULLTEXT, SPATIAL, VECTOR)
*/
indexType?: MariaDBIndexType;
/**
* Distance metric for VECTOR indexes (`euclidean` | `cosine`). Defaults to euclidean.
*/
vectorDistanceMetric?: "euclidean" | "cosine";
/**
* Optional documentation or tuning note inside the DB metadata
*/
comment?: string;
} }
/** /**
* Describes a multi-field uniqueness rule for a table. * Describes a multi-field uniqueness rule for a table.
@@ -1158,47 +1179,6 @@ export type ResponseQueryObject = {
sql?: string; sql?: string;
params?: (string | number)[]; params?: (string | number)[];
}; };
/**
* Common API response wrapper used by data, auth, media, and utility
* endpoints.
*/
export type APIResponseObject<T extends {
[k: string]: any;
} = {
[k: string]: any;
}> = {
success: boolean;
payload?: T[] | null;
singleRes?: T | null;
stringRes?: string | null;
numberRes?: number | null;
postInsertReturn?: PostInsertReturn | null;
payloadBase64?: string;
payloadThumbnailBase64?: string;
payloadURL?: string;
payloadThumbnailURL?: string;
error?: any;
msg?: string;
queryObject?: ResponseQueryObject;
countQueryObject?: ResponseQueryObject;
status?: number;
count?: number;
errors?: BUNSQLITEErrorObject[];
debug?: any;
batchPayload?: any[][] | null;
errorData?: any;
token?: string;
csrf?: string;
cookieNames?: any;
key?: string;
userId?: string | number;
code?: string;
createdAt?: number;
email?: string;
requestOptions?: RequestOptions;
logoutUser?: boolean;
redirect?: string;
};
/** /**
* # Docker Compose Types * # Docker Compose Types
*/ */
@@ -1308,7 +1288,7 @@ export declare const IndexTypes: readonly ["regular", "full_text", "vector"];
/** /**
* Structured error payload captured alongside generated SQL. * Structured error payload captured alongside generated SQL.
*/ */
export type BUNSQLITEErrorObject = { export type BunMariaDBErrorObject = {
sql?: string; sql?: string;
sqlValues?: any[]; sqlValues?: any[];
error?: string; error?: string;
@@ -1347,23 +1327,22 @@ export type SQLInsertGenParams = {
dbFullName?: string; dbFullName?: string;
}; };
/** /**
* Library configuration used to locate the SQLite database, schema file, * Library configuration used to locate the MariaDB database, schema file,
* generated types, and backup settings. * generated types, and backup settings.
*/ */
export type BunSQLiteConfig = { export type BunMariaDBConfig = {
db_name: string; db_name: string;
/**
* The Name of the Database Schema File. Eg `db_schema.ts`. This is
* relative to `db_dir`, or root dir if `db_dir` is not provided
*/
db_schema_file_name: string;
/** /**
* The Directory for backups. Relative to db_dir. * The Directory for backups. Relative to db_dir.
*/ */
db_backup_dir?: string; db_backup_dir?: string;
max_backups?: number; max_backups?: number;
/** /**
* The Root Directory for the DB file and schema * Max export archives to keep in `{db_dir}/.exports` (default: 10)
*/
max_exports?: number;
/**
* Root directory for schema, types, and local artifacts (relative to project root)
*/ */
db_dir?: string; db_dir?: string;
/** /**
@@ -1372,40 +1351,53 @@ export type BunSQLiteConfig = {
*/ */
typedef_file_path?: string; typedef_file_path?: string;
/** /**
* Whether to enable `WAL` mode * Configuration for the MariaDB Connection
*/ */
wal_mode?: boolean; db_config?: Bun.SQL.Options;
/**
* Database charset. Defaults to `utf8mb4`
*/
charset?: MariaDbCharsetType;
/**
* Database Connection timeout. Defaults to `10000`
*/
connection_timeout?: number;
/**
* File path to the SSL certificate
*/
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[]>;
};
}; };
/** /**
* Resolved Bun SQLite config paired with the loaded database schema. * Resolved Bun MariaDB config paired with the loaded database schema.
*/ */
export type BunSQLiteConfigReturn = { export type BunMariaDBConfigReturn = {
config: BunSQLiteConfig; config: BunMariaDBConfig;
dbSchema: BUN_MARIADB_DatabaseSchemaType; dbSchema: BUN_MARIADB_DatabaseSchemaType;
}; };
/** /**
* Default fields automatically suggested for new tables. * Default fields automatically suggested for new tables.
*/ */
export declare const DefaultFields: BUN_MARIADB_FieldSchemaType[]; export declare const DefaultFields: BUN_MARIADB_FieldSchemaType[];
export type BunSQLiteQueryFieldValues<F extends string = string, T extends string = string> = { export type BunMariaDBQueryFieldValues<F extends string = string, T extends string = string> = {
field: F; field: F;
table?: T; table?: T;
}; };
export type QueryRawValueType = string | number | null | undefined; export type QueryRawValueType = string | number | null | undefined;
export type DsqlConnectionParam = {
/**
* No Database Connection
*/
noDb?: boolean;
/**
* Database Name
*/
database?: string;
/**
* Debug
*/
config?: ConnectionConfig;
};
export type DBResponseObject<T extends { export type DBResponseObject<T extends {
[k: string]: any; [k: string]: any;
} = { } = {
@@ -1414,4 +1406,35 @@ export type DBResponseObject<T extends {
success: boolean; success: boolean;
payload?: T[]; payload?: T[];
single_res?: T; single_res?: T;
insert_return?: DBInsertReturn;
error?: any;
msg?: string;
debug?: any;
count?: number;
};
export type DBInsertReturn = {
count?: number;
last_insert_id?: number;
affected_rows?: number;
};
export declare const RequiredENVs: readonly ["BUN_MARIADB_SERVER_HOST", "BUN_MARIADB_SERVER_PORT", "BUN_MARIADB_SERVER_USERNAME", "BUN_MARIADB_SERVER_PASSWORD", "BUN_MARIADB_SERVER_SSL_KEY_PATH"];
export type CreateDBSchemaParams = {
db_schema: BUN_MARIADB_DatabaseSchemaType;
config?: BunMariaDBConfig;
};
export type CreateDBSchemaTableHandlerParams = CreateDBSchemaParams & {
table: BUN_MARIADB_TableSchemaType;
db_manager_table_name?: string;
existing_live_table?: BUN_MARIADB_INFORMATION_SCHEMA_TABLES;
};
export type BUN_MARIADB_DB_TABLE_MANAGER_TABLE = {
table_name?: string;
created_at?: number;
updated_at?: number;
};
export type BUN_MARIADB_INFORMATION_SCHEMA_TABLES = {
TABLE_NAME?: string;
TABLE_TYPE?: "BASE TABLE";
ENGINE?: "InnoDB";
VERSION?: number;
}; };

Some files were not shown because too many files have changed in this diff Show More