From da78648b6d061d3bb6d88c2e78e25463ed7ae708 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Tue, 26 Dec 2023 12:45:32 -0500 Subject: [PATCH 01/29] initial commit to pgtyped branch --- package-lock.json | 903 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + 2 files changed, 895 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index e657f6e..7c3453a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.6.0", "license": "AGPL-3.0-or-later", "dependencies": { + "@pgtyped/runtime": "^2.3.0", "@tsconfig/node-lts": "^20.1.0", "@types/node": "^20.10.0", "discord.js": "^14.14.0", @@ -16,6 +17,7 @@ "typescript": "^5.3.3" }, "devDependencies": { + "@pgtyped/cli": "^2.3.0", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "commander": "^11.1.0", @@ -35,6 +37,12 @@ "node": ">=0.10.0" } }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -242,6 +250,102 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -302,6 +406,147 @@ "node": ">= 8" } }, + "node_modules/@pgtyped/cli": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@pgtyped/cli/-/cli-2.3.0.tgz", + "integrity": "sha512-mSCx3BQW4IkfMyAOdCJZSYo2+G6rRaP05TkIMCLxTl+qiAyDyPiTwnYHCfcLV9ZHvloZ03UEUMLyOySapq5eyw==", + "dev": true, + "dependencies": { + "@pgtyped/parser": "^2.3.0", + "@pgtyped/query": "^2.3.0", + "@pgtyped/wire": "^2.3.0", + "camel-case": "^4.1.1", + "chalk": "^4.0.0", + "chokidar": "^3.3.1", + "debug": "^4.1.1", + "fp-ts": "^2.5.3", + "fs-extra": "^11.0.0", + "glob": "^10.3.7", + "io-ts": "^2.2.20", + "io-ts-reporters": "^2.0.1", + "nunjucks": "3.2.4", + "pascal-case": "^3.1.1", + "piscina": "^4.0.0", + "tinypool": "^0.8.0", + "ts-parse-database-url": "^1.0.3", + "yargs": "^17.0.1" + }, + "bin": { + "pgtyped": "lib/index.js" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "typescript": "3.1 - 5" + } + }, + "node_modules/@pgtyped/cli/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@pgtyped/cli/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@pgtyped/cli/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@pgtyped/parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@pgtyped/parser/-/parser-2.3.0.tgz", + "integrity": "sha512-KJ/rXbHnq0aRdyVo4RjIivXJMnOJjaxmk060uAqLVzgOVy2xF2lZ2GAykH1JSCRpRbpikUOtb4QE7dkbownB6g==", + "dependencies": { + "antlr4ts": "0.5.0-alpha.4", + "chalk": "^4.1.0", + "debug": "^4.1.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@pgtyped/query": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@pgtyped/query/-/query-2.3.0.tgz", + "integrity": "sha512-Ko+JqkeU+bNlQK1PiL/+KitNj4fBNIvgJPBeRJGbNyRidTNuMSBFnd+DLjnhWP8bEu8e0b+mm9nECcmbbBNrpA==", + "dev": true, + "dependencies": { + "@pgtyped/runtime": "^2.3.0", + "@pgtyped/wire": "^2.3.0", + "chalk": "^4.1.0", + "debug": "^4.1.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@pgtyped/runtime": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@pgtyped/runtime/-/runtime-2.3.0.tgz", + "integrity": "sha512-B8RMUeX+zsaXfKOuR3w3Vku5YLSQ8rw+YUYc2IyAFzlQJZpAQDkkAVM0abgGNQE8bOK1wuE+nHJawWuVy+I8bA==", + "dependencies": { + "@pgtyped/parser": "^2.3.0", + "chalk": "^4.1.0", + "debug": "^4.1.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@pgtyped/wire": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@pgtyped/wire/-/wire-2.3.0.tgz", + "integrity": "sha512-bUJXVeSphcZvk8nSyIz42oZhthQK/zvDWC6JPgLZ3zjTknOGTFLIyLJomTpbfy0CHdEpNMEeF64hh4rU1uQyiA==", + "dev": true, + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sapphire/async-queue": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.1.tgz", @@ -332,6 +577,13 @@ "npm": ">=7.0.0" } }, + "node_modules/@scarf/scarf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.3.0.tgz", + "integrity": "sha512-lHKK8M5CTcpFj2hZDB3wIjb0KAbEOgDmiJGDv1WBRfQgRm/a8/XMEkG/N1iM01xgbUDsPQwi42D+dFo1XPAKew==", + "dev": true, + "hasInstallScript": true + }, "node_modules/@tsconfig/node-lts": { "version": "20.1.0", "resolved": "https://registry.npmjs.org/@tsconfig/node-lts/-/node-lts-20.1.0.tgz", @@ -625,6 +877,12 @@ "npm": ">=7.0.0" } }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", @@ -684,7 +942,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -695,6 +952,24 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antlr4ts": { + "version": "0.5.0-alpha.4", + "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", + "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -828,6 +1103,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -846,6 +1127,35 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -891,11 +1201,20 @@ "node": ">=6" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -907,11 +1226,63 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -922,8 +1293,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/commander": { "version": "11.1.0", @@ -970,7 +1340,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1093,6 +1462,18 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/es-abstract": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", @@ -1187,6 +1568,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1622,12 +2012,62 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fp-ts": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", + "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1664,6 +2104,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -1789,6 +2238,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -1808,7 +2263,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1876,6 +2330,23 @@ "node": ">= 0.4" } }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -1940,6 +2411,28 @@ "node": ">= 0.4" } }, + "node_modules/io-ts": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz", + "integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==", + "dev": true, + "peerDependencies": { + "fp-ts": "^2.5.0" + } + }, + "node_modules/io-ts-reporters": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/io-ts-reporters/-/io-ts-reporters-2.0.1.tgz", + "integrity": "sha512-RVpLstYBsmTGgCW9wJ5KVyN/eRnRUDp87Flt4D1O3aJ7oAnd8csq8aXuu7ZeNK8qEDKmjUl9oUuzfwikaNAMKQ==", + "dev": true, + "dependencies": { + "@scarf/scarf": "^1.1.1" + }, + "peerDependencies": { + "fp-ts": "^2.10.5", + "io-ts": "^2.2.16" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -1966,6 +2459,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -2031,6 +2536,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2185,6 +2699,24 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2228,6 +2760,18 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2281,6 +2825,15 @@ "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2348,11 +2901,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mongodb-uri": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/mongodb-uri/-/mongodb-uri-0.9.7.tgz", + "integrity": "sha512-s6BdnqNoEYfViPJgkH85X5Nw5NpzxN8hoflKLweNa7vBxt2V7kaS06d74pAtqDxde8fn4r9h4dNdLiFGoNV0KA==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2360,6 +2930,93 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, + "node_modules/node-gyp-build": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.1.tgz", + "integrity": "sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nunjucks": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "dev": true, + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "chokidar": "^3.3.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/nunjucks/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -2515,6 +3172,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2527,6 +3190,16 @@ "node": ">=6" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2561,6 +3234,31 @@ "dev": true, "peer": true }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -2582,6 +3280,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/piscina": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.2.1.tgz", + "integrity": "sha512-LShp0+lrO+WIzB9LXO+ZmO4zGHxtTJNZhEO56H9SSu+JPaUQb6oLcTCzWi5IL2DS8/vIkCE88ElahuSSw4TAkA==", + "dev": true, + "dependencies": { + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2635,6 +3346,18 @@ } ] }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -2652,6 +3375,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -2838,6 +3570,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -2847,6 +3591,35 @@ "node": ">=8" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", @@ -2904,6 +3677,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -2930,7 +3716,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -2957,6 +3742,15 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinypool": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz", + "integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3029,6 +3823,15 @@ } } }, + "node_modules/ts-parse-database-url": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-parse-database-url/-/ts-parse-database-url-1.0.3.tgz", + "integrity": "sha512-7AIP9EZyKsgaeGpu+Yhu6xDQtwbKDfkw5zBUsuYXju79tFRj6u8w2W+5Ag5wtCS6LM1jOB4iIqDNyFYX758wVQ==", + "dev": true, + "dependencies": { + "mongodb-uri": "^0.9.7" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -3179,6 +3982,15 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3244,6 +4056,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3270,12 +4117,48 @@ } } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 0afc069..4bbb3a3 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "appcmd-cli": "ts-node src/util/AppCommandsCLI.ts" }, "dependencies": { + "@pgtyped/runtime": "^2.3.0", "@tsconfig/node-lts": "^20.1.0", "@types/node": "^20.10.0", "discord.js": "^14.14.0", @@ -54,6 +55,7 @@ "typescript": "^5.3.3" }, "devDependencies": { + "@pgtyped/cli": "^2.3.0", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "commander": "^11.1.0", From abbcb66942e907327c9f2c6e8b7c7f25a25c394e Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Tue, 26 Dec 2023 18:12:41 -0500 Subject: [PATCH 02/29] initial database development - added postgres service to docker-compose - started using Docker secrets to manage database password, bot token - add barebones "configure" command to experiment with commands - installed and configured pgtyped --- .dockerignore | 1 + .env.example | 6 +- .eslintrc.cjs | 25 --------- .gitignore | 4 ++ docker-compose.example.yml | 55 +++++++++++++++++-- package.json | 6 +- pgtyped.config.cjs | 45 +++++++++++++++ secrets/.gitkeep | 0 src/{ => bot}/@types/CustomCommand.d.ts | 0 src/{ => bot}/@types/Discord.d.ts | 0 src/bot/commands/Configure/index.ts | 27 +++++++++ src/bot/commands/Configure/setup.queries.mts | 34 ++++++++++++ src/bot/commands/Configure/setup.sql | 2 + src/{ => bot}/commands/Help.ts | 0 src/{ => bot}/commands/Invite.ts | 0 src/{ => bot}/commands/Vote.ts | 0 src/{ => bot}/commands/index.ts | 2 + src/{ => bot}/index.ts | 29 +++++++++- src/{ => bot}/replacements/BaseReplacement.ts | 0 .../replacements/InstagramReplacement.ts | 0 .../replacements/RedditReplacement.ts | 0 .../replacements/TikTokReplacement.ts | 0 .../replacements/TwitterReplacement.ts | 0 .../replacements/YouTubeReplacement.ts | 0 src/{ => bot}/replacements/index.ts | 0 src/{util => cli}/AppCommandsCLI.ts | 55 +++++++++++-------- src/sql/schema.sql | 24 ++++++++ tsconfig.json | 4 +- tsconfig.prod.json | 2 +- 29 files changed, 259 insertions(+), 62 deletions(-) delete mode 100644 .eslintrc.cjs create mode 100644 pgtyped.config.cjs create mode 100644 secrets/.gitkeep rename src/{ => bot}/@types/CustomCommand.d.ts (100%) rename src/{ => bot}/@types/Discord.d.ts (100%) create mode 100644 src/bot/commands/Configure/index.ts create mode 100644 src/bot/commands/Configure/setup.queries.mts create mode 100644 src/bot/commands/Configure/setup.sql rename src/{ => bot}/commands/Help.ts (100%) rename src/{ => bot}/commands/Invite.ts (100%) rename src/{ => bot}/commands/Vote.ts (100%) rename src/{ => bot}/commands/index.ts (80%) rename src/{ => bot}/index.ts (75%) rename src/{ => bot}/replacements/BaseReplacement.ts (100%) rename src/{ => bot}/replacements/InstagramReplacement.ts (100%) rename src/{ => bot}/replacements/RedditReplacement.ts (100%) rename src/{ => bot}/replacements/TikTokReplacement.ts (100%) rename src/{ => bot}/replacements/TwitterReplacement.ts (100%) rename src/{ => bot}/replacements/YouTubeReplacement.ts (100%) rename src/{ => bot}/replacements/index.ts (100%) rename src/{util => cli}/AppCommandsCLI.ts (88%) create mode 100644 src/sql/schema.sql diff --git a/.dockerignore b/.dockerignore index 81fcca9..0bfaa12 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ .github/ .vscode/ media/ +secrets/ src/util *.log diff --git a/.env.example b/.env.example index bb21b11..8f8c14a 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,8 @@ -DISCORD_BOT_TOKEN=put.your.token.here +DISCORD_BOT_TOKEN_FILE=./secrets/discord-bot-token.txt +DISCORD_APP_ID=385950397493280805 + +POSTGRES_USER=linkfix +POSTGRES_PASSWORD_FILE=./secrets/postgres-password.txt LINKFIX_DEBUG=0 diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index ff3c93a..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = { - extends: [ - 'airbnb-typescript/base', - 'eslint:recommended', - 'plugin:@typescript-eslint/strict-type-checked', - ], - plugins: [ - '@typescript-eslint', - 'import' - ], - parser: '@typescript-eslint/parser', - parserOptions: { - project: './tsconfig.json' - }, - root: true, - ignorePatterns: [ - '*.js', - '*.cjs', - 'dist/**' - ], - rules: { - '@typescript-eslint/indent': 'off', - '@typescript-eslint/quotes': 'off', - } -}; diff --git a/.gitignore b/.gitignore index 779f073..617aaef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ ### Docker / Environment files .env* !.env.example + docker-compose*.y*ml !docker-compose.example.y*ml +secrets/ +!secrets/.gitkeep + ### vscode .vscode diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 86ab1c1..36d3879 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -1,18 +1,61 @@ -# 1) Copy this file from docker-compose.example.yml to docker-compose.yml -# 2) Edit the `environment` section to use your bot's token -# i.e. `DISCORD_BOT_TOKEN=123456.789.10` -# 3) execute `docker compose up` to start the bot +# 1. Copy this file from docker-compose.example.yml to docker-compose.yml +# 2. In the 'secrets/' directory, create two files: +# - discord-bot-token.txt +# - postgres-password.txt +# 3. Populate files with the following: +# - discord-bot-token.txt: your bot's Discord token +# - postgres-password.txt: a secure password for Postgres +# 4. execute `docker compose up [-d]` to start the bot + +version: "3.8" + +secrets: + discord-bot-token: + file: secrets/discord-bot-token.txt + postgres-password: + file: secrets/postgres-password.txt services: linkfix_bot: + # replace 'image: ' with 'build: .' for local dev environments image: ghcr.io/podaboutlist/linkfix-for-discord:latest - # Use SIGKILL to emulate Ctrl+C (discord.js does not handle SIGTERM) + # Use Ctrl+C to kill LinkFix (discord.js doesn't catch SIGTERM) stop_signal: SIGKILL + secrets: + - discord-bot-token + - postgres-password environment: - - DISCORD_BOT_TOKEN=your.bot.token.here + - DISCORD_BOT_TOKEN_FILE=/run/secrets/discord-bot-token + - POSTGRES_PASSWORD_FILE=/run/secrets/postgres-password - LINKFIX_DEBUG=0 - TWITTER_FIX_URL=fxtwitter.com - YOUTUBE_FIX_URL=youtu.be - INSTAGRAM_FIX_URL=ddinstagram.com - TIKTOK_FIX_URL=vxtiktok.com - REDDIT_FIX_URL=vxreddit.com + depends_on: + postgres: + condition: service_healthy + + postgres: + image: postgres:16 + restart: always + secrets: + - postgres-password + environment: + - POSTGRES_USER=linkfix + - POSTGRES_PASSWORD_FILE=/run/secrets/postgres-password + volumes: + - pgdata:/var/lib/postgresql/data + - ./src/sql/:/docker-entrypoint-initdb.d + ports: + - 15432:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_USER} -U $${POSTGRES_USER}"] + interval: 5s + timeout: 2s + retries: 3 + start_period: 5s + +volumes: + pgdata: diff --git a/package.json b/package.json index 4bbb3a3..9694add 100644 --- a/package.json +++ b/package.json @@ -39,12 +39,12 @@ ], "repository": "github:podaboutlist/linkfix-for-discord", "scripts": { - "start": "node dist/index.js", + "start": "node dist/bot/index.js", "build": "rm -rf dist && tsc --project ./tsconfig.json", "build-prod": "rm -rf dist && tsc --project ./tsconfig.prod.json --listEmittedFiles", - "lint": "eslint src/ && prettier src/ --check", + "lint": "eslint && prettier src/ --check", "format": "eslint src/ --fix && prettier src/ --write", - "appcmd-cli": "ts-node src/util/AppCommandsCLI.ts" + "appcmd-cli": "ts-node src/cli/AppCommandsCLI.ts" }, "dependencies": { "@pgtyped/runtime": "^2.3.0", diff --git a/pgtyped.config.cjs b/pgtyped.config.cjs new file mode 100644 index 0000000..6b8aad2 --- /dev/null +++ b/pgtyped.config.cjs @@ -0,0 +1,45 @@ +const dotenv = require("dotenv"); +const fs = require("node:fs"); + +dotenv.config(); + +let pgPass; + +try { + pgPass = fs.readFileSync("./secrets/postgres-password.txt", {encoding: "utf8"}); +} catch (err) { + console.error("Could not read Postgres password from file."); +} + +pgPass = pgPass.replace(/\n/g, ""); + +const config = { + transforms: [ + { + mode: "sql", + include: "**/*.sql", + emitTemplate: "{{dir}}/{{name}}.queries.mts" + }, + { + mode: "ts", + include: "**/action.ts", + emitTemplate: "{{dir}}/{{name}}.types.mts" + } + ], + srcDir: "./src/bot/", + failOnError: false, + camelCaseColumnNames: false, + // PostgreSQL environment variables will override these settings + // https://pgtyped.dev/docs/cli#environment-variables + // Use `docker compose up -d postgres` to run the database in the background + db: { + dbName: "linkfix", + user: "linkfix", + password: pgPass ? pgPass : "linkfix", + host: "127.0.0.1", + port: 15432, + ssl: false + } +}; + +module.exports = config; diff --git a/secrets/.gitkeep b/secrets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/@types/CustomCommand.d.ts b/src/bot/@types/CustomCommand.d.ts similarity index 100% rename from src/@types/CustomCommand.d.ts rename to src/bot/@types/CustomCommand.d.ts diff --git a/src/@types/Discord.d.ts b/src/bot/@types/Discord.d.ts similarity index 100% rename from src/@types/Discord.d.ts rename to src/bot/@types/Discord.d.ts diff --git a/src/bot/commands/Configure/index.ts b/src/bot/commands/Configure/index.ts new file mode 100644 index 0000000..c7ad5b0 --- /dev/null +++ b/src/bot/commands/Configure/index.ts @@ -0,0 +1,27 @@ +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; +import { CustomCommand } from "../../@types/CustomCommand"; + +// TODO: Implement database logic and this command lol + +const commandData = new SlashCommandBuilder() + .setName("configure") + .addBooleanOption((option) => + option + .setName("delete-on-reply") + .setDescription( + "Should LinkFix delete messages when it replies to them?", + ), + ) + // run SetDescription last becasue add<>Option functions don't return a + // SlashCommandBuilder + .setDescription("Configure how LinkFix behaves in your server."); + +export const ConfigureCommand: CustomCommand = { + data: commandData, + execute: async (interaction: CommandInteraction) => { + await interaction.reply({ + content: "i am going to write a message here", + ephemeral: true, + }); + }, +}; diff --git a/src/bot/commands/Configure/setup.queries.mts b/src/bot/commands/Configure/setup.queries.mts new file mode 100644 index 0000000..3600561 --- /dev/null +++ b/src/bot/commands/Configure/setup.queries.mts @@ -0,0 +1,34 @@ +/** Types generated for queries found in "src/bot/commands/Setup/setup.sql" */ +import { PreparedQuery } from '@pgtyped/runtime'; + +export type NumberOrString = number | string; + +/** 'GetServerByGuildId' parameters type */ +export interface IGetServerByGuildIdParams { + guildId?: NumberOrString | null | void; +} + +/** 'GetServerByGuildId' return type */ +export interface IGetServerByGuildIdResult { + id: number; + /** Discord Guild ID */ + native_id: string; +} + +/** 'GetServerByGuildId' query type */ +export interface IGetServerByGuildIdQuery { + params: IGetServerByGuildIdParams; + result: IGetServerByGuildIdResult; +} + +const getServerByGuildIdIR: any = {"usedParamSet":{"guildId":true},"params":[{"name":"guildId","required":false,"transform":{"type":"scalar"},"locs":[{"a":39,"b":46}]}],"statement":"SELECT * FROM guilds WHERE native_id = :guildId"}; + +/** + * Query generated from SQL: + * ``` + * SELECT * FROM guilds WHERE native_id = :guildId + * ``` + */ +export const getServerByGuildId = new PreparedQuery(getServerByGuildIdIR); + + diff --git a/src/bot/commands/Configure/setup.sql b/src/bot/commands/Configure/setup.sql new file mode 100644 index 0000000..444eb09 --- /dev/null +++ b/src/bot/commands/Configure/setup.sql @@ -0,0 +1,2 @@ +/* @name GetServerByGuildId */ +SELECT * FROM guilds WHERE native_id = :guildId; diff --git a/src/commands/Help.ts b/src/bot/commands/Help.ts similarity index 100% rename from src/commands/Help.ts rename to src/bot/commands/Help.ts diff --git a/src/commands/Invite.ts b/src/bot/commands/Invite.ts similarity index 100% rename from src/commands/Invite.ts rename to src/bot/commands/Invite.ts diff --git a/src/commands/Vote.ts b/src/bot/commands/Vote.ts similarity index 100% rename from src/commands/Vote.ts rename to src/bot/commands/Vote.ts diff --git a/src/commands/index.ts b/src/bot/commands/index.ts similarity index 80% rename from src/commands/index.ts rename to src/bot/commands/index.ts index 95b334c..474a0b9 100644 --- a/src/commands/index.ts +++ b/src/bot/commands/index.ts @@ -1,4 +1,5 @@ import { CustomCommand } from "../@types/CustomCommand"; +import { ConfigureCommand } from "./Configure"; import { HelpCommand } from "./Help"; import { InviteCommand } from "./Invite"; import { VoteCommand } from "./Vote"; @@ -6,5 +7,6 @@ import { VoteCommand } from "./Vote"; export const Commands: Array = [ HelpCommand, InviteCommand, + ConfigureCommand, VoteCommand, ]; diff --git a/src/index.ts b/src/bot/index.ts similarity index 75% rename from src/index.ts rename to src/bot/index.ts index 21de2ea..f8314aa 100644 --- a/src/index.ts +++ b/src/bot/index.ts @@ -3,9 +3,36 @@ import { Client, Collection, Events, GatewayIntentBits } from "discord.js"; import { Commands } from "./commands"; import { replacements } from "./replacements"; import { CustomCommand } from "./@types/CustomCommand"; +import fs from "node:fs"; dotenv.config(); +const getDiscordToken: () => string = () => { + if (process.env.DISCORD_BOT_TOKEN) { + console.debug("[getDiscordToken]\tBot token found in environment."); + return process.env.DISCORD_BOT_TOKEN; + } + + if (process.env.DISCORD_BOT_TOKEN_FILE) { + console.debug("[getDiscordToken]\tReading bot token from disk."); + try { + const token = fs.readFileSync(process.env.DISCORD_BOT_TOKEN_FILE, { + encoding: "utf8", + }); + return token.replaceAll(/\n/g, ""); + } catch (err) { + throw Error( + `Could not read contents of ${process.env.DISCORD_BOT_TOKEN_FILE}\n` + + err, + ); + } + } + + throw Error( + "DISCORD_BOT_TOKEN and DISCORD_BOT_TOKEN_FILE are both undefined.", + ); +}; + const replacementsEntries = Object.entries(replacements); const client = new Client({ @@ -91,4 +118,4 @@ client.on(Events.MessageCreate, (message) => { }); }); -void client.login(process.env.DISCORD_BOT_TOKEN); +void client.login(getDiscordToken()); diff --git a/src/replacements/BaseReplacement.ts b/src/bot/replacements/BaseReplacement.ts similarity index 100% rename from src/replacements/BaseReplacement.ts rename to src/bot/replacements/BaseReplacement.ts diff --git a/src/replacements/InstagramReplacement.ts b/src/bot/replacements/InstagramReplacement.ts similarity index 100% rename from src/replacements/InstagramReplacement.ts rename to src/bot/replacements/InstagramReplacement.ts diff --git a/src/replacements/RedditReplacement.ts b/src/bot/replacements/RedditReplacement.ts similarity index 100% rename from src/replacements/RedditReplacement.ts rename to src/bot/replacements/RedditReplacement.ts diff --git a/src/replacements/TikTokReplacement.ts b/src/bot/replacements/TikTokReplacement.ts similarity index 100% rename from src/replacements/TikTokReplacement.ts rename to src/bot/replacements/TikTokReplacement.ts diff --git a/src/replacements/TwitterReplacement.ts b/src/bot/replacements/TwitterReplacement.ts similarity index 100% rename from src/replacements/TwitterReplacement.ts rename to src/bot/replacements/TwitterReplacement.ts diff --git a/src/replacements/YouTubeReplacement.ts b/src/bot/replacements/YouTubeReplacement.ts similarity index 100% rename from src/replacements/YouTubeReplacement.ts rename to src/bot/replacements/YouTubeReplacement.ts diff --git a/src/replacements/index.ts b/src/bot/replacements/index.ts similarity index 100% rename from src/replacements/index.ts rename to src/bot/replacements/index.ts diff --git a/src/util/AppCommandsCLI.ts b/src/cli/AppCommandsCLI.ts similarity index 88% rename from src/util/AppCommandsCLI.ts rename to src/cli/AppCommandsCLI.ts index 16b2911..d869bde 100644 --- a/src/util/AppCommandsCLI.ts +++ b/src/cli/AppCommandsCLI.ts @@ -4,11 +4,40 @@ import { RESTPostAPIChatInputApplicationCommandsJSONBody, Routes, } from "discord.js"; -import { Commands } from "../commands"; +import { Commands } from "../bot/commands"; /* eslint-disable-next-line import/no-extraneous-dependencies -- * HACK: I should really break this CLI script out into its own project. */ import { Command, Option } from "commander"; +import fs from "node:fs"; + +// HACK: I just copy/pasted this from src/bot/index.ts. Should break it out into +// its own file for importing +const getDiscordToken: () => string = () => { + if (process.env.DISCORD_BOT_TOKEN) { + console.debug("[getDiscordToken]\tBot token found in environment."); + return process.env.DISCORD_BOT_TOKEN; + } + + if (process.env.DISCORD_BOT_TOKEN_FILE) { + console.debug("[getDiscordToken]\tReading bot token from disk."); + try { + const token = fs.readFileSync(process.env.DISCORD_BOT_TOKEN_FILE, { + encoding: "utf8", + }); + return token.replaceAll(/\n/g, ""); + } catch (err) { + throw Error( + `Could not read contents of ${process.env.DISCORD_BOT_TOKEN_FILE}\n` + + err, + ); + } + } + + throw Error( + "DISCORD_BOT_TOKEN and DISCORD_BOT_TOKEN_FILE are both undefined.", + ); +}; /** * Helper function for delaying async functions. @@ -58,18 +87,6 @@ const validateDeletionScope: (args: { return true; }; -/** - * Ensure valid Discord bot token is pulled from ENV - * @returns true (valid token) | false (invalid token) - */ -const validateToken: () => boolean = () => { - if (typeof process.env.DISCORD_BOT_TOKEN !== "string") { - console.error("process.env.DISCORD_BOT_TOKEN is undefined!"); - return false; - } - return true; -}; - /** * Synchronize application commands used by the bot with Discord's REST API * @param {{clientId: string, global: boolean, guildId: string | undefined}} args - CLI args passed to the program @@ -85,11 +102,7 @@ const syncCommands: (args: { const restClient = new REST(); - if (validateToken()) { - restClient.setToken(process.env.DISCORD_BOT_TOKEN); - } else { - return; - } + restClient.setToken(getDiscordToken()); const commandsJSON: Array = []; @@ -149,11 +162,7 @@ const deleteCommands: (args: { const restClient = new REST(); - if (validateToken()) { - restClient.setToken(process.env.DISCORD_BOT_TOKEN); - } else { - return; - } + restClient.setToken(getDiscordToken()); if (args.deleteAll && args.global) { const timeout = 5; diff --git a/src/sql/schema.sql b/src/sql/schema.sql new file mode 100644 index 0000000..9754cd4 --- /dev/null +++ b/src/sql/schema.sql @@ -0,0 +1,24 @@ +CREATE TABLE "guilds" ( + "id" integer PRIMARY KEY, + "native_id" bigint UNIQUE NOT NULL +); + +CREATE TABLE "settings" ( + "id" integer PRIMARY KEY, + "guild_id" integer UNIQUE NOT NULL, + "delete_original" boolean NOT NULL DEFAULT false, + "mention_user" boolean NOT NULL DEFAULT false, + "fix_instagram" boolean NOT NULL DEFAULT true, + "fix_reddit" boolean NOT NULL DEFAULT true, + "fix_tiktok" boolean NOT NULL DEFAULT true, + "fix_twitter" boolean NOT NULL DEFAULT true, + "fix_yt_shorts" boolean NOT NULL DEFAULT true +); + +CREATE UNIQUE INDEX ON "guilds" ("native_id"); + +CREATE UNIQUE INDEX ON "settings" ("guild_id"); + +COMMENT ON COLUMN "guilds"."native_id" IS 'Discord Guild ID'; + +ALTER TABLE "guilds" ADD FOREIGN KEY ("id") REFERENCES "settings" ("guild_id"); diff --git a/tsconfig.json b/tsconfig.json index e1adef1..3ef212c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,10 @@ { "extends": "@tsconfig/node-lts/tsconfig.json", - "include": ["src/**/*.ts", "src/*.ts"], + "include": ["src/**/*.ts", "src/*.ts", "src/*.mts", "src/**/*.mts"], "compilerOptions": { - "typeRoots": ["src/@types", "node_modules/@types"], + "typeRoots": ["src/bot/@types", "node_modules/@types"], "declaration": true, "outDir": "dist", diff --git a/tsconfig.prod.json b/tsconfig.prod.json index ad17235..72a5cf5 100644 --- a/tsconfig.prod.json +++ b/tsconfig.prod.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["src/util"] + "exclude": ["src/cli"] } From 3ad402d5863f8b1ad736b09a28218dec566c7157 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Wed, 27 Dec 2023 15:08:14 -0500 Subject: [PATCH 03/29] fix: re-add .eslintrc, configure prettier realized I accidentally deleted .eslintrc with the previous commit so I took some time to re-configure it to properly work with all the files in our repo - update .eslintrc.js - add prettier config and ignore files - update pre-commit configuration to parity with eslintrc and prettierrc --- .eslintrc.js | 28 +++++++++++++++++++ .pre-commit-config.yaml | 8 +++--- .prettierignore | 12 +++++++++ .prettierrc.js | 14 ++++++++++ package-lock.json | 59 +++++++++++++++++++++++++---------------- package.json | 6 +++-- 6 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 .eslintrc.js create mode 100644 .prettierignore create mode 100644 .prettierrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..b3025d6 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,28 @@ +module.exports = { + extends: ["prettier", "eslint:recommended"], + plugins: ["import"], + env: { + node: true, + es6: true, + }, + root: true, + ignorePatterns: ["/dist"], + overrides: [ + { + files: ["*.ts"], + extends: [ + "airbnb-typescript/base", + "plugin:@typescript-eslint/strict-type-checked", + ], + plugins: ["@typescript-eslint"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + }, + rules: { + "@typescript-eslint/indent": "off", + "@typescript-eslint/quotes": "off", + }, + }, + ], +}; diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55a1108..6aef5b3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: v8.56.0 hooks: - id: eslint - files: '^src/.*\.ts$' + files: \.[jt]s$ additional_dependencies: - "eslint@8.56.0" - "@typescript-eslint/eslint-plugin@6.15.0" @@ -13,7 +13,7 @@ repos: rev: v3.1.0 hooks: - id: prettier - files: '^src/.*\.ts$' + exclude: \.(json|md|mts|sql)$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 @@ -24,11 +24,11 @@ repos: rev: v2.12.0.3 hooks: - id: hadolint - name: check dockerfile + name: lint dockerfile - repo: https://github.com/iamthefij/docker-pre-commit rev: v3.0.1 hooks: - id: docker-compose-check - name: check docker-compose + name: lint docker-compose files: (docker-)?compose(\..*)?\.ya?ml$ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9c3403d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +# ignore folders +.vscode +dist +media +secrets + +# ignore files +**/*.json +**/*.md +# .mts files are generated by PgTyped. Shouldn't be touched otherwise. +**/*.mts +**/*.sql diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..312b26d --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,14 @@ +module.exports = { + // Slightly longer line length. I like Black's choice of 88 (10% extra) + printWidth: 88, + + // TypeScript parsin' + overrides: [ + { + files: "*.ts", + options: { + parser: "typescript", + }, + }, + ], +}; diff --git a/package-lock.json b/package-lock.json index 7c3453a..ff4ea95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "eslint": "^8.56.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^9.1.0", "prettier": "^3.1.1", "ts-node": "^10.9.2" } @@ -177,29 +178,6 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint/js": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", @@ -1687,6 +1665,18 @@ "eslint-plugin-import": "^2.25.3" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -1830,6 +1820,29 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", diff --git a/package.json b/package.json index 9694add..f8c26cf 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,9 @@ "start": "node dist/bot/index.js", "build": "rm -rf dist && tsc --project ./tsconfig.json", "build-prod": "rm -rf dist && tsc --project ./tsconfig.prod.json --listEmittedFiles", - "lint": "eslint && prettier src/ --check", - "format": "eslint src/ --fix && prettier src/ --write", + "prettier": "prettier --check .", + "eslint": "eslint .", + "format": "eslint . --fix && prettier . --write", "appcmd-cli": "ts-node src/cli/AppCommandsCLI.ts" }, "dependencies": { @@ -62,6 +63,7 @@ "eslint": "^8.56.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^9.1.0", "prettier": "^3.1.1", "ts-node": "^10.9.2" } From d75492ad79038960d991e185148d6c030692081c Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Wed, 27 Dec 2023 15:10:54 -0500 Subject: [PATCH 04/29] style: formatted with prettier --- .github/dependabot.yml | 4 --- .github/workflows/release.yml | 6 ++-- src/bot/commands/Configure/index.ts | 4 +-- src/bot/index.ts | 4 +-- src/bot/replacements/BaseReplacement.ts | 48 ++++++++++++------------- src/bot/replacements/index.ts | 4 +-- src/cli/AppCommandsCLI.ts | 46 ++++++++---------------- 7 files changed, 42 insertions(+), 74 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eee181c..ce13475 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,9 +4,6 @@ updates: - package-ecosystem: "npm" directory: "/" open-pull-requests-limit: 2 - #allow: - # - dependency-name: "discord.js" - # - dependency-name: "typescript" assignees: - "ralphorama" schedule: @@ -30,4 +27,3 @@ updates: prefix: "dependabot (npm)" prefix-development: "dependabot (npm dev)" include: "scope" - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea03852..cf64942 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,9 +35,9 @@ jobs: - name: Remove 'v' From SemVer Tags run: | - export CALCULATED_TAG=$(echo ${{ env.CALCULATED_IMAGE_TAG }} | sed 's/^v//g') - echo "Calculated image tag is $CALCULATED_TAG" - echo "CALCULATED_IMAGE_TAG=$CALCULATED_TAG" >> "$GITHUB_ENV" + export CALCULATED_TAG=$(echo ${{ env.CALCULATED_IMAGE_TAG }} | sed 's/^v//g') + echo "Calculated image tag is $CALCULATED_TAG" + echo "CALCULATED_IMAGE_TAG=$CALCULATED_TAG" >> "$GITHUB_ENV" - name: Push Docker Image uses: docker/build-push-action@v5 diff --git a/src/bot/commands/Configure/index.ts b/src/bot/commands/Configure/index.ts index c7ad5b0..19fce80 100644 --- a/src/bot/commands/Configure/index.ts +++ b/src/bot/commands/Configure/index.ts @@ -8,9 +8,7 @@ const commandData = new SlashCommandBuilder() .addBooleanOption((option) => option .setName("delete-on-reply") - .setDescription( - "Should LinkFix delete messages when it replies to them?", - ), + .setDescription("Should LinkFix delete messages when it replies to them?"), ) // run SetDescription last becasue add<>Option functions don't return a // SlashCommandBuilder diff --git a/src/bot/index.ts b/src/bot/index.ts index f8314aa..66bc72b 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -28,9 +28,7 @@ const getDiscordToken: () => string = () => { } } - throw Error( - "DISCORD_BOT_TOKEN and DISCORD_BOT_TOKEN_FILE are both undefined.", - ); + throw Error("DISCORD_BOT_TOKEN and DISCORD_BOT_TOKEN_FILE are both undefined."); }; const replacementsEntries = Object.entries(replacements); diff --git a/src/bot/replacements/BaseReplacement.ts b/src/bot/replacements/BaseReplacement.ts index f70a092..6155e83 100644 --- a/src/bot/replacements/BaseReplacement.ts +++ b/src/bot/replacements/BaseReplacement.ts @@ -43,35 +43,31 @@ export default class BaseReplacement { * @param domainFilter Used when one instance of a Replacement handles multiple domains. * @returns A message to post as a response in discord or null if we made no replacements. */ - public replaceURLs: ( - messageContent: string, - domainFilter?: string, - ) => string | null = (messageContent, domainFilter?) => { - const urls = this.getURLs(messageContent)?.filter((url) => { - return domainFilter ? url.includes(domainFilter) : url; - }); + public replaceURLs: (messageContent: string, domainFilter?: string) => string | null = + (messageContent, domainFilter?) => { + const urls = this.getURLs(messageContent)?.filter((url) => { + return domainFilter ? url.includes(domainFilter) : url; + }); - // idk if we'll ever hit this second case but better safe than sorry - if (urls === undefined || urls.length < 1) { - return null; - } + // idk if we'll ever hit this second case but better safe than sorry + if (urls === undefined || urls.length < 1) { + return null; + } - return urls - .map((url) => { - let c = url.replace(this.replaceRegex, `${this.newDomain}/`); + return urls + .map((url) => { + let c = url.replace(this.replaceRegex, `${this.newDomain}/`); - if (this.stripQueryString) { - c = c.replace(/\?\w+=.*$/gm, ""); - } + if (this.stripQueryString) { + c = c.replace(/\?\w+=.*$/gm, ""); + } - if (process.env.LINKFIX_DEBUG) { - console.debug( - `[${this.constructor.name}]\treplaceURLs()\t${url}\t${c}`, - ); - } + if (process.env.LINKFIX_DEBUG) { + console.debug(`[${this.constructor.name}]\treplaceURLs()\t${url}\t${c}`); + } - return c; - }) - .join("\n"); - }; + return c; + }) + .join("\n"); + }; } diff --git a/src/bot/replacements/index.ts b/src/bot/replacements/index.ts index 4ff1c21..e92395e 100644 --- a/src/bot/replacements/index.ts +++ b/src/bot/replacements/index.ts @@ -37,9 +37,7 @@ export const replacements: { return youtubeReplacer ? youtubeReplacer.replaceURLs(messageContent) : null; }, "instagram.com/": (messageContent) => { - return instagramReplacer - ? instagramReplacer.replaceURLs(messageContent) - : null; + return instagramReplacer ? instagramReplacer.replaceURLs(messageContent) : null; }, "tiktok.com/": (messageContent) => { return tiktokReplacer ? tiktokReplacer.replaceURLs(messageContent) : null; diff --git a/src/cli/AppCommandsCLI.ts b/src/cli/AppCommandsCLI.ts index d869bde..f1465dc 100644 --- a/src/cli/AppCommandsCLI.ts +++ b/src/cli/AppCommandsCLI.ts @@ -34,9 +34,7 @@ const getDiscordToken: () => string = () => { } } - throw Error( - "DISCORD_BOT_TOKEN and DISCORD_BOT_TOKEN_FILE are both undefined.", - ); + throw Error("DISCORD_BOT_TOKEN and DISCORD_BOT_TOKEN_FILE are both undefined."); }; /** @@ -104,8 +102,7 @@ const syncCommands: (args: { restClient.setToken(getDiscordToken()); - const commandsJSON: Array = - []; + const commandsJSON: Array = []; for (const cmd of Commands) { commandsJSON.push(cmd.data.toJSON()); } @@ -134,9 +131,7 @@ const syncCommands: (args: { // restClient.put returns an array of objects for application/json requests console.log( - `Successfully synced ${ - (>data).length - } application commands.`, + `Successfully synced ${(>data).length} application commands.`, ); } catch (error) { console.error(error); @@ -184,9 +179,9 @@ const deleteCommands: (args: { } console.log( - `Deleting ${ - args.deleteAll ? "**ALL** commands" : "command " + args.commandId - } ${args.global ? "**GLOBALLY**" : "in guild " + args.guildId}...`, + `Deleting ${args.deleteAll ? "**ALL** commands" : "command " + args.commandId} ${ + args.global ? "**GLOBALLY**" : "in guild " + args.guildId + }...`, ); if (args.commandId) { @@ -194,9 +189,7 @@ const deleteCommands: (args: { restClient .delete(Routes.applicationCommand(args.clientId, args.commandId)) .then(() => { - console.log( - `Successfully deleted command ${args.commandId} globally.`, - ); + console.log(`Successfully deleted command ${args.commandId} globally.`); }) .catch(console.error); } else { @@ -226,14 +219,11 @@ const deleteCommands: (args: { .catch(console.error); } else { restClient - .put( - Routes.applicationGuildCommands(args.clientId, args.guildId), - { body: [] }, - ) + .put(Routes.applicationGuildCommands(args.clientId, args.guildId), { + body: [], + }) .then(() => { - console.log( - `Successfully deleted all commands in guild ${args.guildId}.`, - ); + console.log(`Successfully deleted all commands in guild ${args.guildId}.`); }) .catch(console.error); } @@ -264,10 +254,7 @@ const deleteCommands: (args: { .default(false) .conflicts("guildId"), ) - .option( - "--guild-id ", - "Update application commands for a specific guild", - ) + .option("--guild-id ", "Update application commands for a specific guild") .action( async (args: { clientId: string; @@ -280,19 +267,14 @@ const deleteCommands: (args: { program .command("delete") - .description( - "Delete one or all application commands to a guild or globally.", - ) + .description("Delete one or all application commands to a guild or globally.") .requiredOption("--client-id ", "The bot's client ID") .addOption( new Option("--global", "Update application commands for all guilds") .default(false) .conflicts("guildId"), ) - .option( - "--guild-id ", - "Update application commands for a specific guild", - ) + .option("--guild-id ", "Update application commands for a specific guild") .addOption( new Option("--delete-all", "Delete all Application Commands") .default(false) From 06c4e6faeb8a8d5cbf6db1f26cbf999c64c4257b Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Wed, 27 Dec 2023 15:11:24 -0500 Subject: [PATCH 05/29] pgtyped: renamed config and formatted with prettier --- pgtyped.config.cjs => pgtyped.config.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename pgtyped.config.cjs => pgtyped.config.js (79%) diff --git a/pgtyped.config.cjs b/pgtyped.config.js similarity index 79% rename from pgtyped.config.cjs rename to pgtyped.config.js index 6b8aad2..ad763c9 100644 --- a/pgtyped.config.cjs +++ b/pgtyped.config.js @@ -6,7 +6,7 @@ dotenv.config(); let pgPass; try { - pgPass = fs.readFileSync("./secrets/postgres-password.txt", {encoding: "utf8"}); + pgPass = fs.readFileSync("./secrets/postgres-password.txt", { encoding: "utf8" }); } catch (err) { console.error("Could not read Postgres password from file."); } @@ -18,13 +18,13 @@ const config = { { mode: "sql", include: "**/*.sql", - emitTemplate: "{{dir}}/{{name}}.queries.mts" + emitTemplate: "{{dir}}/{{name}}.queries.mts", }, { mode: "ts", include: "**/action.ts", - emitTemplate: "{{dir}}/{{name}}.types.mts" - } + emitTemplate: "{{dir}}/{{name}}.types.mts", + }, ], srcDir: "./src/bot/", failOnError: false, @@ -38,8 +38,8 @@ const config = { password: pgPass ? pgPass : "linkfix", host: "127.0.0.1", port: 15432, - ssl: false - } + ssl: false, + }, }; module.exports = config; From 4d4b41b8aa3910a185b46ba580e3f46ea575adfc Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Wed, 27 Dec 2023 15:11:57 -0500 Subject: [PATCH 06/29] ci (linting): split prettier and eslint jobs - move prettier and eslint to separate jobs within the same workflow - use actions/setup-node's built in caching instead of a manual step --- .github/workflows/_lint.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml index fb6f363..6842162 100644 --- a/.github/workflows/_lint.yml +++ b/.github/workflows/_lint.yml @@ -4,7 +4,7 @@ on: workflow_call: jobs: - npm_lint: + prettier: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -12,13 +12,24 @@ jobs: - uses: actions/setup-node@v4 with: node-version: "lts/*" + cache: "npm" - - uses: actions/cache@v3 + - run: npm ci + + - run: npm run prettier + + eslint: + runs-on: ubuntu-latest + env: + DEBUG: "eslint:cli-engine" + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 with: - path: ~/.npm - key: npm-${{ hashFiles('package-lock.json') }} - restore-keys: npm- + node-version: "lts/*" + cache: "npm" - - run: npm ci --ignore-scripts + - run: npm ci - - run: npm run lint + - run: npm run eslint From ca06e397e04a1fce2920c2dd43928350f17f36ce Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Wed, 27 Dec 2023 15:13:08 -0500 Subject: [PATCH 07/29] style: add comments to .env.example --- .env.example | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 8f8c14a..0d6812e 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,9 @@ -DISCORD_BOT_TOKEN_FILE=./secrets/discord-bot-token.txt DISCORD_APP_ID=385950397493280805 +# Note this should be set to /run/secrets/discord-bot-token for Docker instances +DISCORD_BOT_TOKEN_FILE=./secrets/discord-bot-token.txt POSTGRES_USER=linkfix +# Note this should be set to /run/secrets/postgres-password for Docker instances POSTGRES_PASSWORD_FILE=./secrets/postgres-password.txt LINKFIX_DEBUG=0 From 7d7a2cb2f2afa137fed4ae69b4602fd5a441a39e Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Wed, 27 Dec 2023 22:26:54 -0500 Subject: [PATCH 08/29] use regular .ts extensions for queries using .mts was a folley... instead, it appears we need to prepend import statements in *.queries.ts files with `// @ts-expect-error moduleResolution` --- .eslintrc.js | 2 +- .pre-commit-config.yaml | 3 ++- .prettierignore | 2 +- pgtyped.config.js | 4 ++-- .../{setup.queries.mts => configure.queries.ts} | 13 ++++++------- src/bot/commands/Configure/configure.sql | 2 ++ src/bot/commands/Configure/index.ts | 1 + src/bot/commands/Configure/setup.sql | 2 -- 8 files changed, 15 insertions(+), 14 deletions(-) rename src/bot/commands/Configure/{setup.queries.mts => configure.queries.ts} (69%) create mode 100644 src/bot/commands/Configure/configure.sql delete mode 100644 src/bot/commands/Configure/setup.sql diff --git a/.eslintrc.js b/.eslintrc.js index b3025d6..c5e67e2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { es6: true, }, root: true, - ignorePatterns: ["/dist"], + ignorePatterns: ["/dist", "*.queries.ts"], overrides: [ { files: ["*.ts"], diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a7e2ca3..4fcda99 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ repos: hooks: - id: eslint files: \.[jt]s$ + exclude: \.queries\.ts$ additional_dependencies: - "eslint@8.56.0" - "@typescript-eslint/eslint-plugin@6.15.0" @@ -13,7 +14,7 @@ repos: rev: v3.1.0 hooks: - id: prettier - exclude: \.(json|md|mts|sql)$ + exclude: \.(json|md|sql|\.queries\.ts)$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 diff --git a/.prettierignore b/.prettierignore index 9c3403d..decd7cf 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,5 +8,5 @@ secrets **/*.json **/*.md # .mts files are generated by PgTyped. Shouldn't be touched otherwise. -**/*.mts +**/*.queries.ts **/*.sql diff --git a/pgtyped.config.js b/pgtyped.config.js index ad763c9..c4cd67e 100644 --- a/pgtyped.config.js +++ b/pgtyped.config.js @@ -18,12 +18,12 @@ const config = { { mode: "sql", include: "**/*.sql", - emitTemplate: "{{dir}}/{{name}}.queries.mts", + emitTemplate: "{{dir}}/{{name}}.queries.ts", }, { mode: "ts", include: "**/action.ts", - emitTemplate: "{{dir}}/{{name}}.types.mts", + emitTemplate: "{{dir}}/{{name}}.types.ts", }, ], srcDir: "./src/bot/", diff --git a/src/bot/commands/Configure/setup.queries.mts b/src/bot/commands/Configure/configure.queries.ts similarity index 69% rename from src/bot/commands/Configure/setup.queries.mts rename to src/bot/commands/Configure/configure.queries.ts index 3600561..c4454b5 100644 --- a/src/bot/commands/Configure/setup.queries.mts +++ b/src/bot/commands/Configure/configure.queries.ts @@ -1,4 +1,5 @@ -/** Types generated for queries found in "src/bot/commands/Setup/setup.sql" */ +/** Types generated for queries found in "src/bot/commands/Configure/configure.sql" */ +// @ts-expect-error moduleResolution import { PreparedQuery } from '@pgtyped/runtime'; export type NumberOrString = number | string; @@ -10,9 +11,9 @@ export interface IGetServerByGuildIdParams { /** 'GetServerByGuildId' return type */ export interface IGetServerByGuildIdResult { + /** Discord Server ID */ + discord_native_id: string; id: number; - /** Discord Guild ID */ - native_id: string; } /** 'GetServerByGuildId' query type */ @@ -21,14 +22,12 @@ export interface IGetServerByGuildIdQuery { result: IGetServerByGuildIdResult; } -const getServerByGuildIdIR: any = {"usedParamSet":{"guildId":true},"params":[{"name":"guildId","required":false,"transform":{"type":"scalar"},"locs":[{"a":39,"b":46}]}],"statement":"SELECT * FROM guilds WHERE native_id = :guildId"}; +const getServerByGuildIdIR: any = {"usedParamSet":{"guildId":true},"params":[{"name":"guildId","required":false,"transform":{"type":"scalar"},"locs":[{"a":47,"b":54}]}],"statement":"SELECT * FROM guilds WHERE discord_native_id = :guildId"}; /** * Query generated from SQL: * ``` - * SELECT * FROM guilds WHERE native_id = :guildId + * SELECT * FROM guilds WHERE discord_native_id = :guildId * ``` */ export const getServerByGuildId = new PreparedQuery(getServerByGuildIdIR); - - diff --git a/src/bot/commands/Configure/configure.sql b/src/bot/commands/Configure/configure.sql new file mode 100644 index 0000000..4cb4d7e --- /dev/null +++ b/src/bot/commands/Configure/configure.sql @@ -0,0 +1,2 @@ +/* @name GetServerByGuildId */ +SELECT * FROM guilds WHERE discord_native_id = :guildId; diff --git a/src/bot/commands/Configure/index.ts b/src/bot/commands/Configure/index.ts index 19fce80..865b7e9 100644 --- a/src/bot/commands/Configure/index.ts +++ b/src/bot/commands/Configure/index.ts @@ -1,4 +1,5 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; +// import { getServerByGuildId } from "./configure.queries"; import { CustomCommand } from "../../@types/CustomCommand"; // TODO: Implement database logic and this command lol diff --git a/src/bot/commands/Configure/setup.sql b/src/bot/commands/Configure/setup.sql deleted file mode 100644 index 444eb09..0000000 --- a/src/bot/commands/Configure/setup.sql +++ /dev/null @@ -1,2 +0,0 @@ -/* @name GetServerByGuildId */ -SELECT * FROM guilds WHERE native_id = :guildId; From 1196aee66be4a1130d30957fda58b78aa6a3d575 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Wed, 27 Dec 2023 22:50:27 -0500 Subject: [PATCH 09/29] refactor: move getFromEnvOrFile to its own file we were using this functionality for getting the Discord token, we will also use this for getting the database password. --- src/bot/index.ts | 28 ++---------------------- src/cli/AppCommandsCLI.ts | 40 ++++++---------------------------- src/lib/GetFromEnvOrFile.ts | 43 +++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 59 deletions(-) create mode 100644 src/lib/GetFromEnvOrFile.ts diff --git a/src/bot/index.ts b/src/bot/index.ts index 0354e53..74a1f1c 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -3,34 +3,10 @@ import { Client, Collection, Events, GatewayIntentBits } from "discord.js"; import { Commands } from "./commands"; import { replacements } from "./replacements"; import { CustomCommand } from "./@types/CustomCommand"; -import fs from "node:fs"; +import getFromEnvOrFile from "../lib/GetFromEnvOrFile"; dotenv.config(); -const getDiscordToken: () => string = () => { - if (process.env.DISCORD_BOT_TOKEN) { - console.debug("[getDiscordToken]\tBot token found in environment."); - return process.env.DISCORD_BOT_TOKEN; - } - - if (process.env.DISCORD_BOT_TOKEN_FILE) { - console.debug("[getDiscordToken]\tReading bot token from disk."); - try { - const token = fs.readFileSync(process.env.DISCORD_BOT_TOKEN_FILE, { - encoding: "utf8", - }); - return token.replaceAll(/\n/g, ""); - } catch (err) { - throw Error( - `Could not read contents of ${process.env.DISCORD_BOT_TOKEN_FILE}\n` + - err, - ); - } - } - - throw Error("DISCORD_BOT_TOKEN and DISCORD_BOT_TOKEN_FILE are both undefined."); -}; - const replacementsEntries = Object.entries(replacements); const client = new Client({ @@ -130,4 +106,4 @@ client.on(Events.MessageCreate, (message) => { }); }); -void client.login(getDiscordToken()); +void client.login(getFromEnvOrFile("DISCORD_BOT_TOKEN")); diff --git a/src/cli/AppCommandsCLI.ts b/src/cli/AppCommandsCLI.ts index 9b8921d..e7aaae6 100644 --- a/src/cli/AppCommandsCLI.ts +++ b/src/cli/AppCommandsCLI.ts @@ -1,3 +1,7 @@ +/* eslint-disable-next-line import/no-extraneous-dependencies -- + * HACK: I should really break this CLI script out into its own project. + */ +import { Command, Option } from "commander"; import dotenv from "dotenv"; import { REST, @@ -5,37 +9,7 @@ import { Routes, } from "discord.js"; import { Commands } from "../bot/commands"; -/* eslint-disable-next-line import/no-extraneous-dependencies -- - * HACK: I should really break this CLI script out into its own project. - */ -import { Command, Option } from "commander"; -import fs from "node:fs"; - -// HACK: I just copy/pasted this from src/bot/index.ts. Should break it out into -// its own file for importing -const getDiscordToken: () => string = () => { - if (process.env.DISCORD_BOT_TOKEN) { - console.debug("[getDiscordToken]\tBot token found in environment."); - return process.env.DISCORD_BOT_TOKEN; - } - - if (process.env.DISCORD_BOT_TOKEN_FILE) { - console.debug("[getDiscordToken]\tReading bot token from disk."); - try { - const token = fs.readFileSync(process.env.DISCORD_BOT_TOKEN_FILE, { - encoding: "utf8", - }); - return token.replaceAll(/\n/g, ""); - } catch (err) { - throw Error( - `Could not read contents of ${process.env.DISCORD_BOT_TOKEN_FILE}\n` + - err, - ); - } - } - - throw Error("DISCORD_BOT_TOKEN and DISCORD_BOT_TOKEN_FILE are both undefined."); -}; +import getFromEnvOrFile from "../lib/GetFromEnvOrFile"; /** * Helper function for delaying async functions. @@ -100,7 +74,7 @@ const syncCommands: (args: { const restClient = new REST(); - restClient.setToken(getDiscordToken()); + restClient.setToken(getFromEnvOrFile("DISCORD_BOT_TOKEN")); const commandsJSON: Array = []; for (const cmd of Commands) { @@ -157,7 +131,7 @@ const deleteCommands: (args: { const restClient = new REST(); - restClient.setToken(getDiscordToken()); + restClient.setToken(getFromEnvOrFile("DISCORD_BOT_TOKEN")); if (args.deleteAll && args.global) { const timeout = 5; diff --git a/src/lib/GetFromEnvOrFile.ts b/src/lib/GetFromEnvOrFile.ts new file mode 100644 index 0000000..904c558 --- /dev/null +++ b/src/lib/GetFromEnvOrFile.ts @@ -0,0 +1,43 @@ +import fs from "node:fs"; + +/** + * Check for the presence of `varName` or `varName_FILE` keys in the environment. + * If `varName` exists, return its content. If `varName_FILE` exits, read the content + * of the file path. + * @param varName Name of the variable to check for defenition/file pointer in environment. + * @returns Contents of `varName` environment variable or `varName_FILE` file contents. + */ +const getFromEnvOrFile: (varName: string) => string = (varName) => { + const varEnv = process.env[varName]; + const varFile = process.env[varName + "_FILE"]; + + if (typeof varEnv === "undefined" && typeof varFile === "undefined") { + throw Error( + `[getFromEnvOrFile] ${varName} and ${varName}_FILE environment variables are both undefined!`, + ); + } + + if (varEnv) { + console.debug(`[getFromEnvOrFile] ${varName} found in environment.`); + return varEnv; + } + + console.debug(`[getFromEnvOrFile] Attempting to read contents of ${varFile}.`); + + let fileContents = ""; + + try { + // Still have to cast varFile even though it's 100% defined at this point + fileContents = fs.readFileSync(varFile, { encoding: "utf8" }); + } catch (err) { + throw Error( + `[getFromEnvOrFile] Error reading ${varFile}:\n` + (err).message, + ); + } + + console.debug(`[getFromEnvOrFile] ${varFile} contents read successfully.`); + // I don't think removing newlines will ever be an issue. If it is, you can get mad at me. + return fileContents.replaceAll(/\r?\n/g, ""); +}; + +export default getFromEnvOrFile; From 938b57c60782d75a61c8563ac6273866525c3486 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Wed, 27 Dec 2023 23:03:33 -0500 Subject: [PATCH 10/29] db: update sql schema - make `id` columns properly autoincrement - flesh out table relations - add "pg" and "@types/pg" dependencies --- package-lock.json | 269 ++++++++++++++++++++++++++++++++++++++------- package.json | 2 + src/sql/schema.sql | 22 ++-- 3 files changed, 245 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec78e88..f6009fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,21 @@ { "name": "linkfix-for-discord", - "version": "1.6.0", + "version": "1.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "linkfix-for-discord", - "version": "1.6.0", + "version": "1.6.1", "license": "AGPL-3.0-or-later", "dependencies": { "@pgtyped/runtime": "^2.3.0", "@tsconfig/node-lts": "^20.1.0", "@types/node": "^20.10.0", + "@types/pg": "^8.10.9", "discord.js": "^14.14.0", "dotenv": "16.3.1", + "pg": "^8.11.3", "typescript": "^5.3.3" }, "devDependencies": { @@ -612,6 +614,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/pg": { + "version": "8.10.9", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz", + "integrity": "sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", @@ -1156,6 +1168,14 @@ "node": ">=8" } }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -2061,42 +2081,6 @@ "node": ">=14.14" } }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fp-ts": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", - "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==", - "dev": true - }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3165,6 +3149,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3221,6 +3210,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -3317,6 +3311,149 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pg/node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg/node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg/node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3342,6 +3479,46 @@ "nice-napi": "^1.0.2" } }, + "node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3640,6 +3817,14 @@ "node": ">=8" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4166,6 +4351,14 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 4070056..5ce6e36 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,10 @@ "@pgtyped/runtime": "^2.3.0", "@tsconfig/node-lts": "^20.1.0", "@types/node": "^20.10.0", + "@types/pg": "^8.10.9", "discord.js": "^14.14.0", "dotenv": "16.3.1", + "pg": "^8.11.3", "typescript": "^5.3.3" }, "devDependencies": { diff --git a/src/sql/schema.sql b/src/sql/schema.sql index 9754cd4..a86eb47 100644 --- a/src/sql/schema.sql +++ b/src/sql/schema.sql @@ -1,13 +1,13 @@ CREATE TABLE "guilds" ( - "id" integer PRIMARY KEY, - "native_id" bigint UNIQUE NOT NULL + "id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "discord_native_id" bigint UNIQUE NOT NULL ); CREATE TABLE "settings" ( - "id" integer PRIMARY KEY, - "guild_id" integer UNIQUE NOT NULL, - "delete_original" boolean NOT NULL DEFAULT false, - "mention_user" boolean NOT NULL DEFAULT false, + "id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "guild" integer UNIQUE NOT NULL, + "delete_original_message" boolean NOT NULL DEFAULT false, + "mention_user_in_reply" boolean NOT NULL DEFAULT false, "fix_instagram" boolean NOT NULL DEFAULT true, "fix_reddit" boolean NOT NULL DEFAULT true, "fix_tiktok" boolean NOT NULL DEFAULT true, @@ -15,10 +15,12 @@ CREATE TABLE "settings" ( "fix_yt_shorts" boolean NOT NULL DEFAULT true ); -CREATE UNIQUE INDEX ON "guilds" ("native_id"); +CREATE UNIQUE INDEX ON "guilds" ("id"); -CREATE UNIQUE INDEX ON "settings" ("guild_id"); +CREATE UNIQUE INDEX ON "guilds" ("discord_native_id"); -COMMENT ON COLUMN "guilds"."native_id" IS 'Discord Guild ID'; +CREATE UNIQUE INDEX ON "settings" ("guild"); -ALTER TABLE "guilds" ADD FOREIGN KEY ("id") REFERENCES "settings" ("guild_id"); +COMMENT ON COLUMN "guilds"."discord_native_id" IS 'Discord Server ID'; + +ALTER TABLE "settings" ADD FOREIGN KEY ("guild") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE RESTRICT; From 38cc337dc6f2c28a673f3095d18ad634d11e91c6 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Wed, 27 Dec 2023 23:04:35 -0500 Subject: [PATCH 11/29] feat(docker-compose): add `links` to bot I *think* this will allow for easy access to the database with the IP `postgres.docker` --- docker-compose.example.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 36d3879..92645cb 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -36,6 +36,8 @@ services: depends_on: postgres: condition: service_healthy + links: + - "postgres:postgres.docker" postgres: image: postgres:16 From f395c318c3a5dda3b9e33d7973de1efb4fb6051e Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Thu, 28 Dec 2023 01:37:59 -0500 Subject: [PATCH 12/29] refactor: drop PgTyped for raw Postgres queries - dropped PgTyped and related files - set up pg for raw Postgres queries - catch Ctrl+C SIGTERM so we can cleanly shut down database connections - add pgPool member to Discord Client instance - update linter configs to reflect dropping PgTyped --- .eslintrc.js | 2 +- .pre-commit-config.yaml | 3 +- .prettierignore | 4 - Dockerfile | 5 +- docker-compose.example.yml | 6 +- package-lock.json | 903 +----------------- package.json | 2 - pgtyped.config.js | 45 - src/bot/@types/Discord.d.ts | 2 + .../commands/Configure/configure.queries.ts | 33 - src/bot/database/index.ts | 37 + src/bot/index.ts | 45 +- 12 files changed, 98 insertions(+), 989 deletions(-) delete mode 100644 pgtyped.config.js delete mode 100644 src/bot/commands/Configure/configure.queries.ts create mode 100644 src/bot/database/index.ts diff --git a/.eslintrc.js b/.eslintrc.js index c5e67e2..b3025d6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { es6: true, }, root: true, - ignorePatterns: ["/dist", "*.queries.ts"], + ignorePatterns: ["/dist"], overrides: [ { files: ["*.ts"], diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4fcda99..ec635fd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,6 @@ repos: hooks: - id: eslint files: \.[jt]s$ - exclude: \.queries\.ts$ additional_dependencies: - "eslint@8.56.0" - "@typescript-eslint/eslint-plugin@6.15.0" @@ -14,7 +13,7 @@ repos: rev: v3.1.0 hooks: - id: prettier - exclude: \.(json|md|sql|\.queries\.ts)$ + exclude: \.(json|md|sql)$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 diff --git a/.prettierignore b/.prettierignore index decd7cf..581242d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,12 +1,8 @@ -# ignore folders .vscode dist media secrets -# ignore files **/*.json **/*.md -# .mts files are generated by PgTyped. Shouldn't be touched otherwise. -**/*.queries.ts **/*.sql diff --git a/Dockerfile b/Dockerfile index bb38a52..730793d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,5 +18,6 @@ RUN [ "npm", "ci", "--omit=dev" ] COPY . . RUN [ "npm", "run", "build-prod" ] -# Fire 'er up! -CMD [ "node", "dist/index.js" ] +ENV RUNNING_IN_DOCKER "true" + +CMD [ "/usr/local/bin/node", "./dist/bot/index.js" ] diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 92645cb..a2ed271 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -17,14 +17,14 @@ secrets: services: linkfix_bot: - # replace 'image: ' with 'build: .' for local dev environments + # replace `image: ` with `build: .` for local dev environments image: ghcr.io/podaboutlist/linkfix-for-discord:latest - # Use Ctrl+C to kill LinkFix (discord.js doesn't catch SIGTERM) - stop_signal: SIGKILL secrets: - discord-bot-token - postgres-password environment: + # You can specify your token here or add it to `secrets/discord-bot-token.txt` + # - DISCORD_BOT_TOKEN=your.token.here - DISCORD_BOT_TOKEN_FILE=/run/secrets/discord-bot-token - POSTGRES_PASSWORD_FILE=/run/secrets/postgres-password - LINKFIX_DEBUG=0 diff --git a/package-lock.json b/package-lock.json index f6009fb..835aeef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.6.1", "license": "AGPL-3.0-or-later", "dependencies": { - "@pgtyped/runtime": "^2.3.0", "@tsconfig/node-lts": "^20.1.0", "@types/node": "^20.10.0", "@types/pg": "^8.10.9", @@ -19,7 +18,6 @@ "typescript": "^5.3.3" }, "devDependencies": { - "@pgtyped/cli": "^2.3.0", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "commander": "^11.1.0", @@ -40,12 +38,6 @@ "node": ">=0.10.0" } }, - "node_modules/@assemblyscript/loader": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", - "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", - "dev": true - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -230,102 +222,6 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -386,147 +282,6 @@ "node": ">= 8" } }, - "node_modules/@pgtyped/cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@pgtyped/cli/-/cli-2.3.0.tgz", - "integrity": "sha512-mSCx3BQW4IkfMyAOdCJZSYo2+G6rRaP05TkIMCLxTl+qiAyDyPiTwnYHCfcLV9ZHvloZ03UEUMLyOySapq5eyw==", - "dev": true, - "dependencies": { - "@pgtyped/parser": "^2.3.0", - "@pgtyped/query": "^2.3.0", - "@pgtyped/wire": "^2.3.0", - "camel-case": "^4.1.1", - "chalk": "^4.0.0", - "chokidar": "^3.3.1", - "debug": "^4.1.1", - "fp-ts": "^2.5.3", - "fs-extra": "^11.0.0", - "glob": "^10.3.7", - "io-ts": "^2.2.20", - "io-ts-reporters": "^2.0.1", - "nunjucks": "3.2.4", - "pascal-case": "^3.1.1", - "piscina": "^4.0.0", - "tinypool": "^0.8.0", - "ts-parse-database-url": "^1.0.3", - "yargs": "^17.0.1" - }, - "bin": { - "pgtyped": "lib/index.js" - }, - "engines": { - "node": ">=14.16" - }, - "peerDependencies": { - "typescript": "3.1 - 5" - } - }, - "node_modules/@pgtyped/cli/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@pgtyped/cli/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@pgtyped/cli/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@pgtyped/parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@pgtyped/parser/-/parser-2.3.0.tgz", - "integrity": "sha512-KJ/rXbHnq0aRdyVo4RjIivXJMnOJjaxmk060uAqLVzgOVy2xF2lZ2GAykH1JSCRpRbpikUOtb4QE7dkbownB6g==", - "dependencies": { - "antlr4ts": "0.5.0-alpha.4", - "chalk": "^4.1.0", - "debug": "^4.1.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@pgtyped/query": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@pgtyped/query/-/query-2.3.0.tgz", - "integrity": "sha512-Ko+JqkeU+bNlQK1PiL/+KitNj4fBNIvgJPBeRJGbNyRidTNuMSBFnd+DLjnhWP8bEu8e0b+mm9nECcmbbBNrpA==", - "dev": true, - "dependencies": { - "@pgtyped/runtime": "^2.3.0", - "@pgtyped/wire": "^2.3.0", - "chalk": "^4.1.0", - "debug": "^4.1.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@pgtyped/runtime": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@pgtyped/runtime/-/runtime-2.3.0.tgz", - "integrity": "sha512-B8RMUeX+zsaXfKOuR3w3Vku5YLSQ8rw+YUYc2IyAFzlQJZpAQDkkAVM0abgGNQE8bOK1wuE+nHJawWuVy+I8bA==", - "dependencies": { - "@pgtyped/parser": "^2.3.0", - "chalk": "^4.1.0", - "debug": "^4.1.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@pgtyped/wire": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@pgtyped/wire/-/wire-2.3.0.tgz", - "integrity": "sha512-bUJXVeSphcZvk8nSyIz42oZhthQK/zvDWC6JPgLZ3zjTknOGTFLIyLJomTpbfy0CHdEpNMEeF64hh4rU1uQyiA==", - "dev": true, - "dependencies": { - "debug": "^4.1.1" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@sapphire/async-queue": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.1.tgz", @@ -557,13 +312,6 @@ "npm": ">=7.0.0" } }, - "node_modules/@scarf/scarf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.3.0.tgz", - "integrity": "sha512-lHKK8M5CTcpFj2hZDB3wIjb0KAbEOgDmiJGDv1WBRfQgRm/a8/XMEkG/N1iM01xgbUDsPQwi42D+dFo1XPAKew==", - "dev": true, - "hasInstallScript": true - }, "node_modules/@tsconfig/node-lts": { "version": "20.1.0", "resolved": "https://registry.npmjs.org/@tsconfig/node-lts/-/node-lts-20.1.0.tgz", @@ -867,12 +615,6 @@ "npm": ">=7.0.0" } }, - "node_modules/a-sync-waterfall": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", - "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", - "dev": true - }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", @@ -932,6 +674,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -942,24 +685,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/antlr4ts": { - "version": "0.5.0-alpha.4", - "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1093,12 +818,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true - }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -1117,35 +836,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1199,20 +889,11 @@ "node": ">=6" } }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1224,63 +905,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1291,7 +920,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/commander": { "version": "11.1.0", @@ -1338,6 +968,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1460,18 +1091,6 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, "node_modules/es-abstract": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", @@ -1566,15 +1185,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2045,62 +1655,12 @@ "is-callable": "^1.1.3" } }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fp-ts": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz", - "integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==", - "dev": true - }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2137,15 +1697,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -2271,12 +1822,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2296,6 +1841,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -2363,23 +1909,6 @@ "node": ">= 0.4" } }, - "node_modules/hdr-histogram-js": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", - "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", - "dev": true, - "dependencies": { - "@assemblyscript/loader": "^0.10.1", - "base64-js": "^1.2.0", - "pako": "^1.0.3" - } - }, - "node_modules/hdr-histogram-percentiles-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", - "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", - "dev": true - }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -2444,28 +1973,6 @@ "node": ">= 0.4" } }, - "node_modules/io-ts": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz", - "integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==", - "dev": true, - "peerDependencies": { - "fp-ts": "^2.5.0" - } - }, - "node_modules/io-ts-reporters": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/io-ts-reporters/-/io-ts-reporters-2.0.1.tgz", - "integrity": "sha512-RVpLstYBsmTGgCW9wJ5KVyN/eRnRUDp87Flt4D1O3aJ7oAnd8csq8aXuu7ZeNK8qEDKmjUl9oUuzfwikaNAMKQ==", - "dev": true, - "dependencies": { - "@scarf/scarf": "^1.1.1" - }, - "peerDependencies": { - "fp-ts": "^2.10.5", - "io-ts": "^2.2.16" - } - }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -2492,18 +1999,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -2569,15 +2064,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2732,24 +2218,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2793,18 +2261,6 @@ "json5": "lib/cli.js" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2858,15 +2314,6 @@ "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2934,28 +2381,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mongodb-uri": { - "version": "0.9.7", - "resolved": "https://registry.npmjs.org/mongodb-uri/-/mongodb-uri-0.9.7.tgz", - "integrity": "sha512-s6BdnqNoEYfViPJgkH85X5Nw5NpzxN8hoflKLweNa7vBxt2V7kaS06d74pAtqDxde8fn4r9h4dNdLiFGoNV0KA==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2963,93 +2393,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/nice-napi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", - "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "!win32" - ], - "dependencies": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2" - } - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true, - "optional": true - }, - "node_modules/node-gyp-build": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.1.tgz", - "integrity": "sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg==", - "dev": true, - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nunjucks": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", - "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", - "dev": true, - "dependencies": { - "a-sync-waterfall": "^1.0.0", - "asap": "^2.0.3", - "commander": "^5.1.0" - }, - "bin": { - "nunjucks-precompile": "bin/precompile" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "chokidar": "^3.3.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/nunjucks/node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -3215,12 +2558,6 @@ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3233,16 +2570,6 @@ "node": ">=6" } }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3277,31 +2604,6 @@ "dev": true, "peer": true }, - "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, - "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3466,19 +2768,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/piscina": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.2.1.tgz", - "integrity": "sha512-LShp0+lrO+WIzB9LXO+ZmO4zGHxtTJNZhEO56H9SSu+JPaUQb6oLcTCzWi5IL2DS8/vIkCE88ElahuSSw4TAkA==", - "dev": true, - "dependencies": { - "hdr-histogram-js": "^2.0.1", - "hdr-histogram-percentiles-obj": "^3.0.0" - }, - "optionalDependencies": { - "nice-napi": "^1.0.2" - } - }, "node_modules/postgres-array": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", @@ -3572,18 +2861,6 @@ } ] }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -3601,15 +2878,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3796,18 +3064,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3825,35 +3081,6 @@ "node": ">= 10.x" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", @@ -3911,19 +3138,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -3950,6 +3164,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3976,15 +3191,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/tinypool": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz", - "integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4057,15 +3263,6 @@ } } }, - "node_modules/ts-parse-database-url": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-parse-database-url/-/ts-parse-database-url-1.0.3.tgz", - "integrity": "sha512-7AIP9EZyKsgaeGpu+Yhu6xDQtwbKDfkw5zBUsuYXju79tFRj6u8w2W+5Ag5wtCS6LM1jOB4iIqDNyFYX758wVQ==", - "dev": true, - "dependencies": { - "mongodb-uri": "^0.9.7" - } - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -4216,15 +3413,6 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4290,41 +3478,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4359,48 +3512,12 @@ "node": ">=0.4" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 5ce6e36..5298f1d 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "appcmd-cli": "ts-node src/cli/AppCommandsCLI.ts" }, "dependencies": { - "@pgtyped/runtime": "^2.3.0", "@tsconfig/node-lts": "^20.1.0", "@types/node": "^20.10.0", "@types/pg": "^8.10.9", @@ -59,7 +58,6 @@ "typescript": "^5.3.3" }, "devDependencies": { - "@pgtyped/cli": "^2.3.0", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "commander": "^11.1.0", diff --git a/pgtyped.config.js b/pgtyped.config.js deleted file mode 100644 index c4cd67e..0000000 --- a/pgtyped.config.js +++ /dev/null @@ -1,45 +0,0 @@ -const dotenv = require("dotenv"); -const fs = require("node:fs"); - -dotenv.config(); - -let pgPass; - -try { - pgPass = fs.readFileSync("./secrets/postgres-password.txt", { encoding: "utf8" }); -} catch (err) { - console.error("Could not read Postgres password from file."); -} - -pgPass = pgPass.replace(/\n/g, ""); - -const config = { - transforms: [ - { - mode: "sql", - include: "**/*.sql", - emitTemplate: "{{dir}}/{{name}}.queries.ts", - }, - { - mode: "ts", - include: "**/action.ts", - emitTemplate: "{{dir}}/{{name}}.types.ts", - }, - ], - srcDir: "./src/bot/", - failOnError: false, - camelCaseColumnNames: false, - // PostgreSQL environment variables will override these settings - // https://pgtyped.dev/docs/cli#environment-variables - // Use `docker compose up -d postgres` to run the database in the background - db: { - dbName: "linkfix", - user: "linkfix", - password: pgPass ? pgPass : "linkfix", - host: "127.0.0.1", - port: 15432, - ssl: false, - }, -}; - -module.exports = config; diff --git a/src/bot/@types/Discord.d.ts b/src/bot/@types/Discord.d.ts index 6404393..7975709 100644 --- a/src/bot/@types/Discord.d.ts +++ b/src/bot/@types/Discord.d.ts @@ -1,9 +1,11 @@ import { Collection } from "discord.js"; +import { Pool } from "pg"; import { CustomCommand } from "./CustomCommand"; // extend discord.js Client type to allow commands collection declare module "discord.js" { export interface Client { commands: Collection; + pgPool: Pool; } } diff --git a/src/bot/commands/Configure/configure.queries.ts b/src/bot/commands/Configure/configure.queries.ts deleted file mode 100644 index c4454b5..0000000 --- a/src/bot/commands/Configure/configure.queries.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** Types generated for queries found in "src/bot/commands/Configure/configure.sql" */ -// @ts-expect-error moduleResolution -import { PreparedQuery } from '@pgtyped/runtime'; - -export type NumberOrString = number | string; - -/** 'GetServerByGuildId' parameters type */ -export interface IGetServerByGuildIdParams { - guildId?: NumberOrString | null | void; -} - -/** 'GetServerByGuildId' return type */ -export interface IGetServerByGuildIdResult { - /** Discord Server ID */ - discord_native_id: string; - id: number; -} - -/** 'GetServerByGuildId' query type */ -export interface IGetServerByGuildIdQuery { - params: IGetServerByGuildIdParams; - result: IGetServerByGuildIdResult; -} - -const getServerByGuildIdIR: any = {"usedParamSet":{"guildId":true},"params":[{"name":"guildId","required":false,"transform":{"type":"scalar"},"locs":[{"a":47,"b":54}]}],"statement":"SELECT * FROM guilds WHERE discord_native_id = :guildId"}; - -/** - * Query generated from SQL: - * ``` - * SELECT * FROM guilds WHERE discord_native_id = :guildId - * ``` - */ -export const getServerByGuildId = new PreparedQuery(getServerByGuildIdIR); diff --git a/src/bot/database/index.ts b/src/bot/database/index.ts new file mode 100644 index 0000000..77597b6 --- /dev/null +++ b/src/bot/database/index.ts @@ -0,0 +1,37 @@ +import { PoolConfig } from "pg"; +import getFromEnvOrFile from "../../lib/GetFromEnvOrFile"; + +// TODO: See if we need to tweak this timeout value. +const timeoutMs = 2000; + +const inDocker = process.env.RUNNING_IN_DOCKER; + +// TODO: Fix this so it uses Postgres environment variables like PGHOST if they +// are present. https://www.postgresql.org/docs/9.1/libpq-envars.html +export const pgPoolConfig: () => PoolConfig = () => { + let pgPass: string | undefined; + + try { + pgPass = getFromEnvOrFile("POSTGRES_PASSWORD"); + } catch (err) { + pgPass = undefined; + } + + return { + host: inDocker ? "postgres.docker" : "127.0.0.1", + port: inDocker ? 5432 : 15432, + application_name: "linkfix", + database: "linkfix", + user: process.env.POSTGRES_USER ?? "linkfix", + password: pgPass ?? "linkfix", + statement_timeout: timeoutMs, + query_timeout: timeoutMs, + connectionTimeoutMillis: timeoutMs, + // might wanna change this one in the future... + idle_in_transaction_session_timeout: timeoutMs, + + // pool-specific settings + max: 16, + idleTimeoutMillis: 30 * 1000, + }; +}; diff --git a/src/bot/index.ts b/src/bot/index.ts index 74a1f1c..7ad8df4 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -1,5 +1,13 @@ import dotenv from "dotenv"; -import { Client, Collection, Events, GatewayIntentBits } from "discord.js"; +// import pg from "pg"; +import { Pool as PgPool } from "pg"; +import { + Client as DiscordClient, + Collection, + Events, + GatewayIntentBits, +} from "discord.js"; +import { pgPoolConfig } from "./database"; import { Commands } from "./commands"; import { replacements } from "./replacements"; import { CustomCommand } from "./@types/CustomCommand"; @@ -9,7 +17,7 @@ dotenv.config(); const replacementsEntries = Object.entries(replacements); -const client = new Client({ +const client = new DiscordClient({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, @@ -24,8 +32,6 @@ for (const cmd of Commands) { } client.once(Events.ClientReady, (eventClient) => { - client.user?.setActivity("/help"); - console.log(`[Events.ClientReady]\tLogged in as ${eventClient.user.tag}.`); const guildCount = eventClient.guilds.cache.size; @@ -34,6 +40,37 @@ client.once(Events.ClientReady, (eventClient) => { guildCount === 1 ? "guild" : "guilds" }.`, ); + + console.debug("[Events.ClientReady] Initializing Postgres connection pool..."); + + client.pgPool = new PgPool(pgPoolConfig()); + + console.debug("[Events.ClientReady] Postgres connection pool established."); + + client.user?.setActivity("/help"); +}); + +/* + Trap Ctrl+C and perform a graceful shutdown. + TODO: Break this out into its own file and handle other process term codes. + Right now this only supports SIGTERM from *nix Ctrl+C and Docker. +*/ +process.once("SIGTERM", () => { + console.log("[process]\tSIGTERM\tShutting down..."); + void client.pgPool + .end() + .then( + () => { + console.log("[process]\tSIGTERM\tDatabase connection closed."); + }, + (rej) => { + console.log("[process]\tSIGTERM\tIt- it's not shutting down!\n" + rej); + }, + ) + .finally(() => { + console.log("[process]\tSIGTERM\tAll done. Goodbye!"); + process.exit(0); + }); }); client.on(Events.InteractionCreate, async (interaction) => { From 2ff74100b1b6127207a9f79bead5d0de557e21d8 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Thu, 28 Dec 2023 02:19:59 -0500 Subject: [PATCH 13/29] refactor: docker secrets suck! + pgPool test - docker-compose: Use a shared array of environment variables in docker compose - discord-bot: Run a test command on startup to check our database --- docker-compose.example.yml | 34 +++++++++--------------- src/bot/database/index.ts | 53 +++++++++++++++++++++++++++++--------- src/bot/index.ts | 10 ++++++- 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index a2ed271..8498534 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -9,30 +9,23 @@ version: "3.8" -secrets: - discord-bot-token: - file: secrets/discord-bot-token.txt - postgres-password: - file: secrets/postgres-password.txt +x-db-config: &db-config + POSTGRES_USER: linkfix + POSTGRES_PASSWORD: change.me.please services: linkfix_bot: # replace `image: ` with `build: .` for local dev environments image: ghcr.io/podaboutlist/linkfix-for-discord:latest - secrets: - - discord-bot-token - - postgres-password environment: - # You can specify your token here or add it to `secrets/discord-bot-token.txt` - # - DISCORD_BOT_TOKEN=your.token.here - - DISCORD_BOT_TOKEN_FILE=/run/secrets/discord-bot-token - - POSTGRES_PASSWORD_FILE=/run/secrets/postgres-password - - LINKFIX_DEBUG=0 - - TWITTER_FIX_URL=fxtwitter.com - - YOUTUBE_FIX_URL=youtu.be - - INSTAGRAM_FIX_URL=ddinstagram.com - - TIKTOK_FIX_URL=vxtiktok.com - - REDDIT_FIX_URL=vxreddit.com + <<: *db-config + DISCORD_BOT_TOKEN: your.token.here + LINKFIX_DEBUG: 0 + TWITTER_FIX_URL: fxtwitter.com + YOUTUBE_FIX_URL: youtu.be + INSTAGRAM_FIX_URL: ddinstagram.com + TIKTOK_FIX_URL: vxtiktok.com + REDDIT_FIX_URL: vxreddit.com depends_on: postgres: condition: service_healthy @@ -42,11 +35,8 @@ services: postgres: image: postgres:16 restart: always - secrets: - - postgres-password environment: - - POSTGRES_USER=linkfix - - POSTGRES_PASSWORD_FILE=/run/secrets/postgres-password + <<: *db-config volumes: - pgdata:/var/lib/postgresql/data - ./src/sql/:/docker-entrypoint-initdb.d diff --git a/src/bot/database/index.ts b/src/bot/database/index.ts index 77597b6..07c0c67 100644 --- a/src/bot/database/index.ts +++ b/src/bot/database/index.ts @@ -1,14 +1,37 @@ +import dotenv from "dotenv"; import { PoolConfig } from "pg"; import getFromEnvOrFile from "../../lib/GetFromEnvOrFile"; +dotenv.config(); + // TODO: See if we need to tweak this timeout value. -const timeoutMs = 2000; +const shortTimeout = 2 * 1000; +const longTimeout = 30 * 1000; const inDocker = process.env.RUNNING_IN_DOCKER; +const dbName: () => string = () => { + if (typeof process.env.PGDATABASE === "string") { + return process.env.PGDATABASE; + } + + if (typeof inDocker === "string") { + if (typeof process.env.POSTGRES_DB === "string") { + return process.env.POSTGRES_DB; + } + + if (typeof process.env.POSTGRES_USER === "string") { + return process.env.POSTGRES_USER; + } + } + + throw Error("Could not find PGDATABASE|POSTGRES_DB|POSTGRES_USER"); +}; + // TODO: Fix this so it uses Postgres environment variables like PGHOST if they // are present. https://www.postgresql.org/docs/9.1/libpq-envars.html export const pgPoolConfig: () => PoolConfig = () => { + const pgDb = dbName(); let pgPass: string | undefined; try { @@ -18,20 +41,26 @@ export const pgPoolConfig: () => PoolConfig = () => { } return { - host: inDocker ? "postgres.docker" : "127.0.0.1", - port: inDocker ? 5432 : 15432, application_name: "linkfix", - database: "linkfix", - user: process.env.POSTGRES_USER ?? "linkfix", - password: pgPass ?? "linkfix", - statement_timeout: timeoutMs, - query_timeout: timeoutMs, - connectionTimeoutMillis: timeoutMs, - // might wanna change this one in the future... - idle_in_transaction_session_timeout: timeoutMs, + + host: inDocker ? "postgres.docker" : process.env.PGHOST, + port: inDocker ? 5432 : Number(process.env.PGPORT), + + database: pgDb, + + user: process.env.POSTGRES_USER ?? process.env.PGUSER, + password: pgPass ?? process.env.PGPASSWORD, + + statement_timeout: shortTimeout, + query_timeout: shortTimeout, + connectionTimeoutMillis: shortTimeout, + idle_in_transaction_session_timeout: longTimeout, // pool-specific settings max: 16, - idleTimeoutMillis: 30 * 1000, + idleTimeoutMillis: longTimeout, + log: (msg) => { + console.debug(`[pgPool]\t${msg}`); + }, }; }; diff --git a/src/bot/index.ts b/src/bot/index.ts index 7ad8df4..70747c9 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -31,7 +31,7 @@ for (const cmd of Commands) { client.commands.set(cmd.data.name, cmd); } -client.once(Events.ClientReady, (eventClient) => { +client.once(Events.ClientReady, async (eventClient) => { console.log(`[Events.ClientReady]\tLogged in as ${eventClient.user.tag}.`); const guildCount = eventClient.guilds.cache.size; @@ -47,6 +47,14 @@ client.once(Events.ClientReady, (eventClient) => { console.debug("[Events.ClientReady] Postgres connection pool established."); + // TODO: Remove this query. Just a sanity check for now :) + await client.pgPool.query("SELECT * FROM guilds LIMIT 1").then((res) => { + console.debug( + // eslint-disable-next-line + `[Events.ClientReady] SELECT * FROM guilds LIMIT 1: { id: ${res.rows[0].id}, discord_native_id: ${res.rows[0].discord_native_id} }`, + ); + }); + client.user?.setActivity("/help"); }); From 1c6fa9bf7061b8be4a2d643836a23ecdc49976b3 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Thu, 28 Dec 2023 15:16:30 -0500 Subject: [PATCH 14/29] refactor: move CustomPool to submodule we're going to store all the database logic in src/bot/database. also flesh out the CustomPool object to add event handlers. --- src/bot/database/CustomPool.ts | 105 ++++++++++++++++++++++ src/bot/database/index.ts | 67 +------------- src/bot/database/seed/index.ts | 0 src/{sql => bot/database/seed}/schema.sql | 0 src/bot/index.ts | 12 ++- 5 files changed, 112 insertions(+), 72 deletions(-) create mode 100644 src/bot/database/CustomPool.ts create mode 100644 src/bot/database/seed/index.ts rename src/{sql => bot/database/seed}/schema.sql (100%) diff --git a/src/bot/database/CustomPool.ts b/src/bot/database/CustomPool.ts new file mode 100644 index 0000000..50616d1 --- /dev/null +++ b/src/bot/database/CustomPool.ts @@ -0,0 +1,105 @@ +import { Pool, PoolConfig } from "pg"; +import dotenv from "dotenv"; +import getFromEnvOrFile from "../../lib/GetFromEnvOrFile"; + +dotenv.config(); + +// TODO: See if we need to tweak this timeout value. +const shortTimeout = 2 * 1000; +const longTimeout = 30 * 1000; + +const inDocker = process.env.RUNNING_IN_DOCKER; + +/** + * Try to figure out what the database name is given all the possible environment + * variables that could store it. + * @returns Calculated database name + */ +const dbName: () => string = () => { + if (typeof process.env.PGDATABASE === "string") { + return process.env.PGDATABASE; + } + + if (typeof inDocker === "string") { + if (typeof process.env.POSTGRES_DB === "string") { + return process.env.POSTGRES_DB; + } + + if (typeof process.env.POSTGRES_USER === "string") { + return process.env.POSTGRES_USER; + } + } + + throw Error("Could not find PGDATABASE|POSTGRES_DB|POSTGRES_USER"); +}; + +/** + * Create an application-specific configuration object for LinkFix + * @returns Instantiated configuration object. + */ +const customPoolConfig: () => PoolConfig = () => { + const pgDb = dbName(); + let pgPass: string | undefined; + + try { + pgPass = getFromEnvOrFile("POSTGRES_PASSWORD"); + } catch (err) { + pgPass = undefined; + } + + return { + application_name: "linkfix", + + host: inDocker ? "postgres.docker" : process.env.PGHOST, + port: inDocker ? 5432 : Number(process.env.PGPORT), + + database: pgDb, + + user: process.env.POSTGRES_USER ?? process.env.PGUSER, + password: pgPass ?? process.env.PGPASSWORD, + + statement_timeout: shortTimeout, + query_timeout: shortTimeout, + connectionTimeoutMillis: shortTimeout, + idle_in_transaction_session_timeout: longTimeout, + + // pool-specific settings + max: 16, + idleTimeoutMillis: longTimeout, + log: (msg) => { + console.debug(`[pgPool]\t${msg}`); + }, + }; +}; + +/** + * Create a Pool object with the application configuration and attach event handlers. + * @returns Configured Pool object with attached event handlers. + */ +const CustomPool: () => Pool = () => { + const pool = new Pool(customPoolConfig()); + + pool.on("connect", () => { + console.debug("[CustomPool]\tNew connection established"); + }); + + pool.on("acquire", () => { + console.debug("[CustomPool]\tClient checked out from pool"); + }); + + pool.on("error", (err) => { + console.error(`[CustomPool]\tEncountered an error:\t${err.message}`); + }); + + pool.on("release", (err) => { + console.error(`[CustomPool]\tClient released back to pool:\t${err.message}`); + }); + + pool.on("remove", () => { + console.debug("[CustomPool]\tClient closed and removed from pool"); + }); + + return pool; +}; + +export default CustomPool; diff --git a/src/bot/database/index.ts b/src/bot/database/index.ts index 07c0c67..1fe8043 100644 --- a/src/bot/database/index.ts +++ b/src/bot/database/index.ts @@ -1,66 +1,3 @@ -import dotenv from "dotenv"; -import { PoolConfig } from "pg"; -import getFromEnvOrFile from "../../lib/GetFromEnvOrFile"; +import CustomPool from "./CustomPool"; -dotenv.config(); - -// TODO: See if we need to tweak this timeout value. -const shortTimeout = 2 * 1000; -const longTimeout = 30 * 1000; - -const inDocker = process.env.RUNNING_IN_DOCKER; - -const dbName: () => string = () => { - if (typeof process.env.PGDATABASE === "string") { - return process.env.PGDATABASE; - } - - if (typeof inDocker === "string") { - if (typeof process.env.POSTGRES_DB === "string") { - return process.env.POSTGRES_DB; - } - - if (typeof process.env.POSTGRES_USER === "string") { - return process.env.POSTGRES_USER; - } - } - - throw Error("Could not find PGDATABASE|POSTGRES_DB|POSTGRES_USER"); -}; - -// TODO: Fix this so it uses Postgres environment variables like PGHOST if they -// are present. https://www.postgresql.org/docs/9.1/libpq-envars.html -export const pgPoolConfig: () => PoolConfig = () => { - const pgDb = dbName(); - let pgPass: string | undefined; - - try { - pgPass = getFromEnvOrFile("POSTGRES_PASSWORD"); - } catch (err) { - pgPass = undefined; - } - - return { - application_name: "linkfix", - - host: inDocker ? "postgres.docker" : process.env.PGHOST, - port: inDocker ? 5432 : Number(process.env.PGPORT), - - database: pgDb, - - user: process.env.POSTGRES_USER ?? process.env.PGUSER, - password: pgPass ?? process.env.PGPASSWORD, - - statement_timeout: shortTimeout, - query_timeout: shortTimeout, - connectionTimeoutMillis: shortTimeout, - idle_in_transaction_session_timeout: longTimeout, - - // pool-specific settings - max: 16, - idleTimeoutMillis: longTimeout, - log: (msg) => { - console.debug(`[pgPool]\t${msg}`); - }, - }; -}; +export { CustomPool }; diff --git a/src/bot/database/seed/index.ts b/src/bot/database/seed/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/sql/schema.sql b/src/bot/database/seed/schema.sql similarity index 100% rename from src/sql/schema.sql rename to src/bot/database/seed/schema.sql diff --git a/src/bot/index.ts b/src/bot/index.ts index 70747c9..23d893a 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -1,17 +1,15 @@ -import dotenv from "dotenv"; -// import pg from "pg"; -import { Pool as PgPool } from "pg"; import { - Client as DiscordClient, Collection, + Client as DiscordClient, Events, GatewayIntentBits, } from "discord.js"; -import { pgPoolConfig } from "./database"; import { Commands } from "./commands"; -import { replacements } from "./replacements"; import { CustomCommand } from "./@types/CustomCommand"; +import { CustomPool } from "./database"; +import dotenv from "dotenv"; import getFromEnvOrFile from "../lib/GetFromEnvOrFile"; +import { replacements } from "./replacements"; dotenv.config(); @@ -43,7 +41,7 @@ client.once(Events.ClientReady, async (eventClient) => { console.debug("[Events.ClientReady] Initializing Postgres connection pool..."); - client.pgPool = new PgPool(pgPoolConfig()); + client.pgPool = CustomPool(); console.debug("[Events.ClientReady] Postgres connection pool established."); From 4406bd9cb8cd7bdac14d32306116e7e101fd24d4 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Thu, 28 Dec 2023 15:18:49 -0500 Subject: [PATCH 15/29] style (eslint): enable eslint/sort-imports rule --- .eslintrc.js | 3 +++ src/bot/@types/Discord.d.ts | 2 +- src/bot/commands/Configure/index.ts | 1 + src/bot/commands/Help.ts | 1 + src/bot/commands/Invite.ts | 1 + src/bot/commands/Vote.ts | 1 + src/bot/commands/index.ts | 3 +-- src/cli/AppCommandsCLI.ts | 2 +- 8 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b3025d6..4766746 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,9 @@ module.exports = { }, root: true, ignorePatterns: ["/dist"], + rules: { + "sort-imports": "error", + }, overrides: [ { files: ["*.ts"], diff --git a/src/bot/@types/Discord.d.ts b/src/bot/@types/Discord.d.ts index 7975709..c6f4037 100644 --- a/src/bot/@types/Discord.d.ts +++ b/src/bot/@types/Discord.d.ts @@ -1,6 +1,6 @@ import { Collection } from "discord.js"; -import { Pool } from "pg"; import { CustomCommand } from "./CustomCommand"; +import { Pool } from "pg"; // extend discord.js Client type to allow commands collection declare module "discord.js" { diff --git a/src/bot/commands/Configure/index.ts b/src/bot/commands/Configure/index.ts index 865b7e9..9bd46d3 100644 --- a/src/bot/commands/Configure/index.ts +++ b/src/bot/commands/Configure/index.ts @@ -1,4 +1,5 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; + // import { getServerByGuildId } from "./configure.queries"; import { CustomCommand } from "../../@types/CustomCommand"; diff --git a/src/bot/commands/Help.ts b/src/bot/commands/Help.ts index 58ecb05..d8cf08e 100644 --- a/src/bot/commands/Help.ts +++ b/src/bot/commands/Help.ts @@ -1,4 +1,5 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; + import { CustomCommand } from "../@types/CustomCommand"; export const HelpCommand: CustomCommand = { diff --git a/src/bot/commands/Invite.ts b/src/bot/commands/Invite.ts index ad6dfcd..33ea350 100644 --- a/src/bot/commands/Invite.ts +++ b/src/bot/commands/Invite.ts @@ -1,4 +1,5 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; + import { CustomCommand } from "../@types/CustomCommand"; export const InviteCommand: CustomCommand = { diff --git a/src/bot/commands/Vote.ts b/src/bot/commands/Vote.ts index 568e09d..1e21cb0 100644 --- a/src/bot/commands/Vote.ts +++ b/src/bot/commands/Vote.ts @@ -1,4 +1,5 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; + import { CustomCommand } from "../@types/CustomCommand"; export const VoteCommand: CustomCommand = { diff --git a/src/bot/commands/index.ts b/src/bot/commands/index.ts index 54c0b47..38e6de7 100644 --- a/src/bot/commands/index.ts +++ b/src/bot/commands/index.ts @@ -1,6 +1,5 @@ -import { CustomCommand } from "../@types/CustomCommand"; - import { ConfigureCommand } from "./Configure"; +import { CustomCommand } from "../@types/CustomCommand"; import { HelpCommand } from "./Help"; import { InviteCommand } from "./Invite"; import { VoteCommand } from "./Vote"; diff --git a/src/cli/AppCommandsCLI.ts b/src/cli/AppCommandsCLI.ts index e7aaae6..90e483e 100644 --- a/src/cli/AppCommandsCLI.ts +++ b/src/cli/AppCommandsCLI.ts @@ -2,13 +2,13 @@ * HACK: I should really break this CLI script out into its own project. */ import { Command, Option } from "commander"; -import dotenv from "dotenv"; import { REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes, } from "discord.js"; import { Commands } from "../bot/commands"; +import dotenv from "dotenv"; import getFromEnvOrFile from "../lib/GetFromEnvOrFile"; /** From e11a627fe0589c90b35ef880e9b397adcbf72edd Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Thu, 28 Dec 2023 15:29:20 -0500 Subject: [PATCH 16/29] refactor: oops I forgot we're using initdb - delete seed/index.ts (don't need to initialize db in software) - rename seed/ to initdb/ - change initdb path in docker-compose - use smaller postgres:16-alpine image in docker-compose --- docker-compose.example.yml | 4 ++-- src/bot/database/{seed => initdb}/schema.sql | 19 +++++++++++-------- src/bot/database/seed/index.ts | 0 3 files changed, 13 insertions(+), 10 deletions(-) rename src/bot/database/{seed => initdb}/schema.sql (58%) delete mode 100644 src/bot/database/seed/index.ts diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 8498534..a6a1dc4 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -33,13 +33,13 @@ services: - "postgres:postgres.docker" postgres: - image: postgres:16 + image: postgres:16-alpine restart: always environment: <<: *db-config volumes: - pgdata:/var/lib/postgresql/data - - ./src/sql/:/docker-entrypoint-initdb.d + - ./src/bot/database/initdb/:/docker-entrypoint-initdb.d ports: - 15432:5432 healthcheck: diff --git a/src/bot/database/seed/schema.sql b/src/bot/database/initdb/schema.sql similarity index 58% rename from src/bot/database/seed/schema.sql rename to src/bot/database/initdb/schema.sql index a86eb47..b795853 100644 --- a/src/bot/database/seed/schema.sql +++ b/src/bot/database/initdb/schema.sql @@ -1,13 +1,18 @@ -CREATE TABLE "guilds" ( +CREATE TABLE IF NOT EXISTS "guilds" ( "id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "discord_native_id" bigint UNIQUE NOT NULL ); -CREATE TABLE "settings" ( +CREATE TABLE IF NOT EXISTS "settings" ( "id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "guild" integer UNIQUE NOT NULL, + "guild" integer UNIQUE NOT NULL + REFERENCES guilds(id) + ON DELETE CASCADE + ON UPDATE RESTRICT, + "delete_original_message" boolean NOT NULL DEFAULT false, "mention_user_in_reply" boolean NOT NULL DEFAULT false, + "fix_instagram" boolean NOT NULL DEFAULT true, "fix_reddit" boolean NOT NULL DEFAULT true, "fix_tiktok" boolean NOT NULL DEFAULT true, @@ -15,12 +20,10 @@ CREATE TABLE "settings" ( "fix_yt_shorts" boolean NOT NULL DEFAULT true ); -CREATE UNIQUE INDEX ON "guilds" ("id"); +CREATE UNIQUE INDEX IF NOT EXISTS ux_guilds_id ON "guilds" ("id"); -CREATE UNIQUE INDEX ON "guilds" ("discord_native_id"); +CREATE UNIQUE INDEX IF NOT EXISTS ux_guilds_discord_native_id ON "guilds" ("discord_native_id"); -CREATE UNIQUE INDEX ON "settings" ("guild"); +CREATE UNIQUE INDEX IF NOT EXISTS ux_settings_guild ON "settings" ("guild"); COMMENT ON COLUMN "guilds"."discord_native_id" IS 'Discord Server ID'; - -ALTER TABLE "settings" ADD FOREIGN KEY ("guild") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE RESTRICT; diff --git a/src/bot/database/seed/index.ts b/src/bot/database/seed/index.ts deleted file mode 100644 index e69de29..0000000 From 58396d51cbff8ed3357585ada879a478a6e6afbc Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Thu, 28 Dec 2023 18:32:43 -0500 Subject: [PATCH 17/29] npm run format: use cache for eslint and prettier --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5298f1d..c4f75ea 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "build-prod": "rm -rf dist && tsc --project ./tsconfig.prod.json --listEmittedFiles", "prettier": "prettier --check .", "eslint": "eslint .", - "format": "eslint . --fix && prettier . --write", + "format": "eslint --cache --fix . && prettier --cache --write .", "appcmd-cli": "ts-node src/cli/AppCommandsCLI.ts" }, "dependencies": { From 92617e2a31f8ba4fe4198b2b0c8cc29a806fff84 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Thu, 28 Dec 2023 18:34:48 -0500 Subject: [PATCH 18/29] feat: initial work on `/config` command man oh man it's ugly as sin but I got some database crap working. next step is to abstract db logic out into its own files to simplify application command code. --- src/bot/@types/CustomCommand.d.ts | 3 +- src/bot/commands/Configure/configure.sql | 2 +- src/bot/commands/Configure/index.ts | 114 ++++++++++++++++++++--- src/bot/database/CustomPool.ts | 5 +- src/bot/database/initdb/schema.sql | 6 +- src/bot/index.ts | 34 ++++++- 6 files changed, 141 insertions(+), 23 deletions(-) diff --git a/src/bot/@types/CustomCommand.d.ts b/src/bot/@types/CustomCommand.d.ts index e5ad5df..d2b47e6 100644 --- a/src/bot/@types/CustomCommand.d.ts +++ b/src/bot/@types/CustomCommand.d.ts @@ -1,6 +1,7 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; +import { Pool } from "pg"; type CustomCommand = { data: SlashCommandBuilder; - execute: (interaction: CommandInteraction) => Promise; + execute: (interaction: CommandInteraction, pool?: Pool) => Promise; }; diff --git a/src/bot/commands/Configure/configure.sql b/src/bot/commands/Configure/configure.sql index 4cb4d7e..7ac0e51 100644 --- a/src/bot/commands/Configure/configure.sql +++ b/src/bot/commands/Configure/configure.sql @@ -1,2 +1,2 @@ /* @name GetServerByGuildId */ -SELECT * FROM guilds WHERE discord_native_id = :guildId; +SELECT * FROM guilds WHERE native_guild_id = :guildId; diff --git a/src/bot/commands/Configure/index.ts b/src/bot/commands/Configure/index.ts index 9bd46d3..4386c85 100644 --- a/src/bot/commands/Configure/index.ts +++ b/src/bot/commands/Configure/index.ts @@ -1,27 +1,119 @@ -import { CommandInteraction, SlashCommandBuilder } from "discord.js"; - -// import { getServerByGuildId } from "./configure.queries"; import { CustomCommand } from "../../@types/CustomCommand"; - -// TODO: Implement database logic and this command lol +import { SlashCommandBuilder } from "discord.js"; const commandData = new SlashCommandBuilder() .setName("configure") .addBooleanOption((option) => option - .setName("delete-on-reply") + .setName("delete_on_reply") .setDescription("Should LinkFix delete messages when it replies to them?"), ) - // run SetDescription last becasue add<>Option functions don't return a + .addBooleanOption((option) => + option + .setName("mention_user") + .setDescription( + "Mention the user LinkFix is replying to (when deleting a message).", + ), + ) + .addBooleanOption((option) => + option + .setName("fix_instagram") + .setDescription("Enable/disable fixing of Instagram URLs."), + ) + .addBooleanOption((option) => + option + .setName("fix_reddit") + .setDescription("Enable/disable fixing of Reddit URLs."), + ) + .addBooleanOption((option) => + option + .setName("fix_tiktok") + .setDescription("Enable/disable fixing of TikTok URLs."), + ) + .addBooleanOption((option) => + option + .setName("fix_twitter") + .setDescription("Enable/disable fixing of Twitter/X URLs."), + ) + .addBooleanOption((option) => + option + .setName("fix_youtube") + .setDescription("Enable/disable fixing of YouTube Shorts URLs."), + ) + // run SetDescription last becasue add...Option functions don't return a // SlashCommandBuilder .setDescription("Configure how LinkFix behaves in your server."); export const ConfigureCommand: CustomCommand = { data: commandData, - execute: async (interaction: CommandInteraction) => { - await interaction.reply({ - content: "i am going to write a message here", - ephemeral: true, + execute: async (i, pool) => { + // FIXME: "Property 'getBoolean' does not exist on type 'OmitqueryData.rows[0]; + + // man oh man did prettier make this ugly lol + await i.reply({ + content: `Your guild was ${ + inserting ? "inserted into" : "found in" + } the database: \n\`\`\`\n{ id: ${row.id}, native_guild_id: ${ + row.native_guild_id + } }\n\`\`\``, + ephemeral: false, }); + + poolClient.release(); + + return; }, }; diff --git a/src/bot/database/CustomPool.ts b/src/bot/database/CustomPool.ts index 50616d1..5fd6233 100644 --- a/src/bot/database/CustomPool.ts +++ b/src/bot/database/CustomPool.ts @@ -88,11 +88,12 @@ const CustomPool: () => Pool = () => { }); pool.on("error", (err) => { - console.error(`[CustomPool]\tEncountered an error:\t${err.message}`); + console.error(`[CustomPool]\tEncountered an error:\t${String(err)}`); }); pool.on("release", (err) => { - console.error(`[CustomPool]\tClient released back to pool:\t${err.message}`); + // Workaround: @types/pg doesn't specify err can be Error | null | undefined + console.debug(`[CustomPool]\tClient released back to pool:\t${String(err)}`); }); pool.on("remove", () => { diff --git a/src/bot/database/initdb/schema.sql b/src/bot/database/initdb/schema.sql index b795853..1f4f048 100644 --- a/src/bot/database/initdb/schema.sql +++ b/src/bot/database/initdb/schema.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS "guilds" ( "id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "discord_native_id" bigint UNIQUE NOT NULL + "native_guild_id" bigint UNIQUE NOT NULL ); CREATE TABLE IF NOT EXISTS "settings" ( @@ -22,8 +22,8 @@ CREATE TABLE IF NOT EXISTS "settings" ( CREATE UNIQUE INDEX IF NOT EXISTS ux_guilds_id ON "guilds" ("id"); -CREATE UNIQUE INDEX IF NOT EXISTS ux_guilds_discord_native_id ON "guilds" ("discord_native_id"); +CREATE UNIQUE INDEX IF NOT EXISTS ux_guilds_native_guild_id ON "guilds" ("native_guild_id"); CREATE UNIQUE INDEX IF NOT EXISTS ux_settings_guild ON "settings" ("guild"); -COMMENT ON COLUMN "guilds"."discord_native_id" IS 'Discord Server ID'; +COMMENT ON COLUMN "guilds"."native_guild_id" IS 'Discord Server ID'; diff --git a/src/bot/index.ts b/src/bot/index.ts index 23d893a..edd44dd 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -29,6 +29,11 @@ for (const cmd of Commands) { client.commands.set(cmd.data.name, cmd); } +/* + Events.ClientReady gets fired when the bot fully authenticates after it first + starts up. We perform our initial logic (database pool initialization, etc.) + when this event is fired. +*/ client.once(Events.ClientReady, async (eventClient) => { console.log(`[Events.ClientReady]\tLogged in as ${eventClient.user.tag}.`); @@ -46,12 +51,17 @@ client.once(Events.ClientReady, async (eventClient) => { console.debug("[Events.ClientReady] Postgres connection pool established."); // TODO: Remove this query. Just a sanity check for now :) - await client.pgPool.query("SELECT * FROM guilds LIMIT 1").then((res) => { + const res = await client.pgPool.query("SELECT * FROM guilds LIMIT 1"); + + if (res.rowCount === null || res.rowCount < 1) { + console.debug("[Events.ClientReady] Database appears to be empty."); + } else { + const row = <{ id: number; native_guild_id: string }>res.rows[0]; + console.debug( - // eslint-disable-next-line - `[Events.ClientReady] SELECT * FROM guilds LIMIT 1: { id: ${res.rows[0].id}, discord_native_id: ${res.rows[0].discord_native_id} }`, + `[Events.ClientReady] SELECT * FROM guilds LIMIT 1: { id: ${row.id}, native_guild_id: ${row.native_guild_id} }`, ); - }); + } client.user?.setActivity("/help"); }); @@ -79,15 +89,29 @@ process.once("SIGTERM", () => { }); }); +/* + Events.InteractionCreate fires whenever someone uses one of our slash, a.k.a. + application commands. + + I initially thought we could persist the pgPool object on the client but it + appears that is not possible. Instead, we pass a reference to the pool as an + optional argument to the callback function. +*/ client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; const command = ( interaction.client.commands.get(interaction.commandName) ); - await command.execute(interaction); + + await command.execute(interaction, client.pgPool); }); +/* + Events.MessageCreate is fired whenever someone posts a message to any channel + we have permission to view. This is where the actual "link fixing" happens + since we want the functionality to work passively instead of with a command. +*/ client.on(Events.MessageCreate, (message) => { // Avoid infinite loops of bots replying to each other if (message.author.bot) { From 20cee19bb4a7f388fdf3da9c596daf26d26b3326 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Fri, 29 Dec 2023 01:31:18 -0500 Subject: [PATCH 19/29] style: update prettierrc on our side makes merging in from main easier --- .eslintrc.js | 5 +- .prettierrc.js | 2 +- src/bot/commands/Configure/index.ts | 27 +++------- src/bot/commands/Vote.ts | 4 +- src/bot/index.ts | 20 ++------ src/bot/replacements/BaseReplacement.ts | 50 +++++++++---------- .../replacements/RedditMediaReplacement.ts | 4 +- src/bot/replacements/TikTokReplacement.ts | 6 +-- src/bot/replacements/index.ts | 8 +-- src/cli/AppCommandsCLI.ts | 42 ++++------------ src/lib/GetFromEnvOrFile.ts | 4 +- 11 files changed, 55 insertions(+), 117 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 4766746..0dffbc0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,10 +13,7 @@ module.exports = { overrides: [ { files: ["*.ts"], - extends: [ - "airbnb-typescript/base", - "plugin:@typescript-eslint/strict-type-checked", - ], + extends: ["airbnb-typescript/base", "plugin:@typescript-eslint/strict-type-checked"], plugins: ["@typescript-eslint"], parser: "@typescript-eslint/parser", parserOptions: { diff --git a/.prettierrc.js b/.prettierrc.js index 312b26d..5f896b7 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,6 +1,6 @@ module.exports = { // Slightly longer line length. I like Black's choice of 88 (10% extra) - printWidth: 88, + printWidth: 96, // TypeScript parsin' overrides: [ diff --git a/src/bot/commands/Configure/index.ts b/src/bot/commands/Configure/index.ts index 4386c85..3c2ec10 100644 --- a/src/bot/commands/Configure/index.ts +++ b/src/bot/commands/Configure/index.ts @@ -11,29 +11,19 @@ const commandData = new SlashCommandBuilder() .addBooleanOption((option) => option .setName("mention_user") - .setDescription( - "Mention the user LinkFix is replying to (when deleting a message).", - ), + .setDescription("Mention the user LinkFix is replying to (when deleting a message)."), ) .addBooleanOption((option) => - option - .setName("fix_instagram") - .setDescription("Enable/disable fixing of Instagram URLs."), + option.setName("fix_instagram").setDescription("Enable/disable fixing of Instagram URLs."), ) .addBooleanOption((option) => - option - .setName("fix_reddit") - .setDescription("Enable/disable fixing of Reddit URLs."), + option.setName("fix_reddit").setDescription("Enable/disable fixing of Reddit URLs."), ) .addBooleanOption((option) => - option - .setName("fix_tiktok") - .setDescription("Enable/disable fixing of TikTok URLs."), + option.setName("fix_tiktok").setDescription("Enable/disable fixing of TikTok URLs."), ) .addBooleanOption((option) => - option - .setName("fix_twitter") - .setDescription("Enable/disable fixing of Twitter/X URLs."), + option.setName("fix_twitter").setDescription("Enable/disable fixing of Twitter/X URLs."), ) .addBooleanOption((option) => option @@ -72,10 +62,9 @@ export const ConfigureCommand: CustomCommand = { const poolClient = await pool.connect(); - let queryData = await poolClient.query( - "SELECT * FROM guilds WHERE native_guild_id = $1", - [i.guildId], - ); + let queryData = await poolClient.query("SELECT * FROM guilds WHERE native_guild_id = $1", [ + i.guildId, + ]); let inserting = false; // Add server to database if it does not already exist diff --git a/src/bot/commands/Vote.ts b/src/bot/commands/Vote.ts index 1e21cb0..7c29681 100644 --- a/src/bot/commands/Vote.ts +++ b/src/bot/commands/Vote.ts @@ -3,9 +3,7 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; import { CustomCommand } from "../@types/CustomCommand"; export const VoteCommand: CustomCommand = { - data: new SlashCommandBuilder() - .setName("vote") - .setDescription("Vote for LinkFix on Top.gg!"), + data: new SlashCommandBuilder().setName("vote").setDescription("Vote for LinkFix on Top.gg!"), execute: async (interaction: CommandInteraction) => { await interaction.reply({ content: diff --git a/src/bot/index.ts b/src/bot/index.ts index b808917..b2c469d 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -1,9 +1,4 @@ -import { - Collection, - Client as DiscordClient, - Events, - GatewayIntentBits, -} from "discord.js"; +import { Collection, Client as DiscordClient, Events, GatewayIntentBits } from "discord.js"; import { Commands } from "./commands"; import { CustomCommand } from "./@types/CustomCommand"; import { CustomPool } from "./database"; @@ -39,9 +34,7 @@ client.once(Events.ClientReady, async (eventClient) => { const guildCount = eventClient.guilds.cache.size; console.log( - `[Events.ClientReady]\tPresent in ${guildCount} ${ - guildCount === 1 ? "guild" : "guilds" - }.`, + `[Events.ClientReady]\tPresent in ${guildCount} ${guildCount === 1 ? "guild" : "guilds"}.`, ); console.debug("[Events.ClientReady] Initializing Postgres connection pool..."); @@ -100,9 +93,7 @@ process.once("SIGTERM", () => { client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; - const command = ( - interaction.client.commands.get(interaction.commandName) - ); + const command = interaction.client.commands.get(interaction.commandName); await command.execute(interaction, client.pgPool); }); @@ -169,10 +160,7 @@ client.on(Events.MessageCreate, (message) => { return; } - console.error( - "[Events.MessageCreate]\tFailed to reply\t", - (err as Error).message, - ); + console.error("[Events.MessageCreate]\tFailed to reply\t", (err as Error).message); }); }); diff --git a/src/bot/replacements/BaseReplacement.ts b/src/bot/replacements/BaseReplacement.ts index 6155e83..7b3db45 100644 --- a/src/bot/replacements/BaseReplacement.ts +++ b/src/bot/replacements/BaseReplacement.ts @@ -31,9 +31,7 @@ export default class BaseReplacement { * @param messageContent - Original text content of a message from Discord. * @returns An array of URLs to process or null if no matches were found. */ - protected getURLs: (messageContent: string) => RegExpMatchArray | null = ( - messageContent, - ) => { + protected getURLs: (messageContent: string) => RegExpMatchArray | null = (messageContent) => { return messageContent.match(this.matchRegex); }; @@ -43,31 +41,33 @@ export default class BaseReplacement { * @param domainFilter Used when one instance of a Replacement handles multiple domains. * @returns A message to post as a response in discord or null if we made no replacements. */ - public replaceURLs: (messageContent: string, domainFilter?: string) => string | null = - (messageContent, domainFilter?) => { - const urls = this.getURLs(messageContent)?.filter((url) => { - return domainFilter ? url.includes(domainFilter) : url; - }); + public replaceURLs: (messageContent: string, domainFilter?: string) => string | null = ( + messageContent, + domainFilter?, + ) => { + const urls = this.getURLs(messageContent)?.filter((url) => { + return domainFilter ? url.includes(domainFilter) : url; + }); - // idk if we'll ever hit this second case but better safe than sorry - if (urls === undefined || urls.length < 1) { - return null; - } + // idk if we'll ever hit this second case but better safe than sorry + if (urls === undefined || urls.length < 1) { + return null; + } - return urls - .map((url) => { - let c = url.replace(this.replaceRegex, `${this.newDomain}/`); + return urls + .map((url) => { + let c = url.replace(this.replaceRegex, `${this.newDomain}/`); - if (this.stripQueryString) { - c = c.replace(/\?\w+=.*$/gm, ""); - } + if (this.stripQueryString) { + c = c.replace(/\?\w+=.*$/gm, ""); + } - if (process.env.LINKFIX_DEBUG) { - console.debug(`[${this.constructor.name}]\treplaceURLs()\t${url}\t${c}`); - } + if (process.env.LINKFIX_DEBUG) { + console.debug(`[${this.constructor.name}]\treplaceURLs()\t${url}\t${c}`); + } - return c; - }) - .join("\n"); - }; + return c; + }) + .join("\n"); + }; } diff --git a/src/bot/replacements/RedditMediaReplacement.ts b/src/bot/replacements/RedditMediaReplacement.ts index ddb3193..dfe0e6e 100644 --- a/src/bot/replacements/RedditMediaReplacement.ts +++ b/src/bot/replacements/RedditMediaReplacement.ts @@ -20,9 +20,7 @@ export default class RedditMediaReplacement { const decoded = decodeURIComponent(uri); if (process.env.LINKFIX_DEBUG) { - console.debug( - `[${this.constructor.name}]\treplaceURLs()\t${url}\t${decoded}`, - ); + console.debug(`[${this.constructor.name}]\treplaceURLs()\t${url}\t${decoded}`); } decodedURIs.push(decoded); diff --git a/src/bot/replacements/TikTokReplacement.ts b/src/bot/replacements/TikTokReplacement.ts index 8019170..2467940 100644 --- a/src/bot/replacements/TikTokReplacement.ts +++ b/src/bot/replacements/TikTokReplacement.ts @@ -2,10 +2,6 @@ import BaseReplacement from "./BaseReplacement"; export default class TikTokReplacement extends BaseReplacement { constructor(newDomain: string) { - super( - newDomain, - /https?:\/\/((www|vm)\.)?tiktok\.com\/[^\s]+/g, - /(www\.)?tiktok\.com\//, - ); + super(newDomain, /https?:\/\/((www|vm)\.)?tiktok\.com\/[^\s]+/g, /(www\.)?tiktok\.com\//); } } diff --git a/src/bot/replacements/index.ts b/src/bot/replacements/index.ts index 41c1a46..29b19cc 100644 --- a/src/bot/replacements/index.ts +++ b/src/bot/replacements/index.ts @@ -40,14 +40,10 @@ export const replacements: { return tiktokReplacer ? tiktokReplacer.replaceURLs(messageContent) : null; }, "(\\/\\/|\\.)reddit\\.com/(?!media)": (messageContent) => { - return redditReplacer - ? redditReplacer.replaceURLs(messageContent, "reddit.com/") - : null; + return redditReplacer ? redditReplacer.replaceURLs(messageContent, "reddit.com/") : null; }, "\\/\\/redd\\.it/": (messageContent) => { - return redditReplacer - ? redditReplacer.replaceURLs(messageContent, "redd.it/") - : null; + return redditReplacer ? redditReplacer.replaceURLs(messageContent, "redd.it/") : null; }, "(\\/\\/|\\.)reddit\\.com/media": (messageContent) => { return redditMediaReplacer ? redditMediaReplacer.replaceURLs(messageContent) : null; diff --git a/src/cli/AppCommandsCLI.ts b/src/cli/AppCommandsCLI.ts index b904426..68f54c6 100644 --- a/src/cli/AppCommandsCLI.ts +++ b/src/cli/AppCommandsCLI.ts @@ -2,11 +2,7 @@ * HACK: I should really break this CLI script out into its own project. */ import { Command, Option } from "commander"; -import { - REST, - RESTPostAPIChatInputApplicationCommandsJSONBody, - Routes, -} from "discord.js"; +import { REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes } from "discord.js"; import { Commands } from "../bot/commands"; import dotenv from "dotenv"; import getFromEnvOrFile from "../lib/GetFromEnvOrFile"; @@ -28,14 +24,11 @@ const sleep: (delay: number) => Promise = async (delay) => { * @param {{global: boolean, guildId: string | undefined}} args - CLI args passed to the program * @returns true (validated) | false (error validating) */ -const validateScope: (args: { - global: boolean; - guildId: string | undefined; -}) => boolean = (args) => { +const validateScope: (args: { global: boolean; guildId: string | undefined }) => boolean = ( + args, +) => { if (!args.global && !args.guildId) { - console.error( - "error: please specify option '--global' or '--guild-id '.", - ); + console.error("error: please specify option '--global' or '--guild-id '."); return false; } return true; @@ -104,9 +97,7 @@ const syncCommands: (args: { } // restClient.put returns an array of objects for application/json requests - console.log( - `Successfully synced ${(>data).length} application commands.`, - ); + console.log(`Successfully synced ${(>data).length} application commands.`); } catch (error) { console.error(error); } finally { @@ -136,9 +127,7 @@ const deleteCommands: (args: { if (args.deleteAll && args.global) { const timeout = 5; - console.warn( - "WARNING: YOU ARE ABOUT TO DELETE **ALL** APPLICATION COMMANDS **GLOBALLY**!", - ); + console.warn("WARNING: YOU ARE ABOUT TO DELETE **ALL** APPLICATION COMMANDS **GLOBALLY**!"); console.log(`Waiting ${timeout} seconds to give you a chance to bail...`); @@ -169,11 +158,7 @@ const deleteCommands: (args: { } else { restClient .delete( - Routes.applicationGuildCommand( - args.clientId, - args.guildId, - args.commandId, - ), + Routes.applicationGuildCommand(args.clientId, args.guildId, args.commandId), ) .then(() => { console.log( @@ -230,11 +215,7 @@ const deleteCommands: (args: { ) .option("--guild-id ", "Update application commands for a specific guild") .action( - async (args: { - clientId: string; - global: boolean; - guildId: string | undefined; - }) => { + async (args: { clientId: string; global: boolean; guildId: string | undefined }) => { await syncCommands(args); }, ); @@ -254,10 +235,7 @@ const deleteCommands: (args: { .default(false) .conflicts("commandId"), ) - .option( - "--command-id ", - "ID of the specific application command to delete", - ) + .option("--command-id ", "ID of the specific application command to delete") .action( async (args: { clientId: string; diff --git a/src/lib/GetFromEnvOrFile.ts b/src/lib/GetFromEnvOrFile.ts index 904c558..519fbd4 100644 --- a/src/lib/GetFromEnvOrFile.ts +++ b/src/lib/GetFromEnvOrFile.ts @@ -30,9 +30,7 @@ const getFromEnvOrFile: (varName: string) => string = (varName) => { // Still have to cast varFile even though it's 100% defined at this point fileContents = fs.readFileSync(varFile, { encoding: "utf8" }); } catch (err) { - throw Error( - `[getFromEnvOrFile] Error reading ${varFile}:\n` + (err).message, - ); + throw Error(`[getFromEnvOrFile] Error reading ${varFile}:\n` + (err).message); } console.debug(`[getFromEnvOrFile] ${varFile} contents read successfully.`); From 3b93258440b750b6eceb114f57ccb9bfaa269c59 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Fri, 29 Dec 2023 18:09:44 -0500 Subject: [PATCH 20/29] fix: run prettier first in npm format script --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2d35d07..c9aceb6 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,9 @@ "start": "node dist/bot/index.js", "build": "rm -rf dist && tsc --project ./tsconfig.json", "build-prod": "rm -rf dist && tsc --project ./tsconfig.prod.json --listEmittedFiles", - "prettier": "prettier --check .", + "prettier-check": "prettier --check .", "eslint": "eslint .", - "format": "eslint --cache --fix . && prettier --cache --write .", + "format": "prettier --cache --write . && eslint --cache --fix .", "appcmd-cli": "ts-node src/cli/AppCommandsCLI.ts" }, "dependencies": { From 6384dc26a2f1008d092dc1c92d2c09944033d1e4 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Fri, 29 Dec 2023 22:02:21 -0500 Subject: [PATCH 21/29] feat: now we have the settings... problems to solve: - abstract database logic out to src/bot/database - build paramaterized query strings with DEFAULT (cannot pass DEFAULT in a parameter... just pass same value we got back from the db?) - finally get around to parsing all those boolean options from the command --- .dockerignore | 35 +++-- src/bot/@types/DatabaseRecords.d.ts | 14 ++ src/bot/commands/Configure/configure.sql | 25 ++- src/bot/commands/Configure/index.ts | 164 ++++++++++++++++---- src/bot/database/initdb/schema.sql | 5 +- src/bot/database/util/ExtractFirstRecord.ts | 17 ++ 6 files changed, 212 insertions(+), 48 deletions(-) create mode 100644 src/bot/@types/DatabaseRecords.d.ts create mode 100644 src/bot/database/util/ExtractFirstRecord.ts diff --git a/.dockerignore b/.dockerignore index 0bfaa12..6304a21 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,22 +1,23 @@ -**/node_modules/* +**/.git/* +**/.github/* +**/.vscode/* **/dist/* +**/media/* +**/node_modules/* +**/secrets/* -.git/ -.github/ -.vscode/ -media/ -secrets/ -src/util +**/.env* +**/.eslint* +**/.gitattributes +**/.gitignore +**/.pre-commit-config.yaml +**/.prettier* +**/docker-compose*.y*ml +docs/* +src/cli/* + +*.gz *.log +*.sql *.tar -*.gz - -.env* -docker-compose*.y*ml - -.gitattributes -.gitignore - -.pre-commit-config.yaml -.prettierrc diff --git a/src/bot/@types/DatabaseRecords.d.ts b/src/bot/@types/DatabaseRecords.d.ts new file mode 100644 index 0000000..c03f3d0 --- /dev/null +++ b/src/bot/@types/DatabaseRecords.d.ts @@ -0,0 +1,14 @@ +export type GuildRecord = { id: number; native_guild_id: bigint | string }; + +export type SettingsRecord = { + id: number; + guild: number; + suppress_embeds: boolean; + delete_original_message: boolean; + mention_user_in_reply: boolean; + fix_instagram: boolean; + fix_reddit: boolean; + fix_tiktok: boolean; + fix_twitter: boolean; + fix_yt_shorts: boolean; +}; diff --git a/src/bot/commands/Configure/configure.sql b/src/bot/commands/Configure/configure.sql index 7ac0e51..3fb9537 100644 --- a/src/bot/commands/Configure/configure.sql +++ b/src/bot/commands/Configure/configure.sql @@ -1,2 +1,25 @@ -/* @name GetServerByGuildId */ +-- SQL Queries in this file are unused, it's left over from using PgTyped. +-- I find it helpful to have these here regardless so I haven't deleted it. + +/* @name findGuildByDiscordId */ SELECT * FROM guilds WHERE native_guild_id = :guildId; + +/* @name insertNewServer */ +INSERT INTO guilds(id, native_guild_id) VALUES (default, :guildId) RETURNING *; + +/* @name getServerSettings */ +SELECT * FROM guild_settings WHERE guild = :guildIndex; + +/* @name createServerSettings */ +INSERT INTO guild_settings( + id, guild, + suppress_embeds, delete_original_message, mention_user_in_reply, + fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts +) VALUES ( + default, :guildIndex, + :suppressEmbeds, :deleteOriginal, :mentionUser, + :fixInstagram, :fixReddit, :fixTiktok, :fixTwitter, :fixYtShorts +) RETURNING *; + +/* @name updateSetting */ +-- in code, we will replace these values with `default` diff --git a/src/bot/commands/Configure/index.ts b/src/bot/commands/Configure/index.ts index 3c2ec10..874e57b 100644 --- a/src/bot/commands/Configure/index.ts +++ b/src/bot/commands/Configure/index.ts @@ -1,5 +1,114 @@ +import { GuildRecord, SettingsRecord } from "../../@types/DatabaseRecords"; import { CustomCommand } from "../../@types/CustomCommand"; +import { PoolClient } from "pg"; import { SlashCommandBuilder } from "discord.js"; +import extractFirstRecord from "../../database/util/ExtractFirstRecord"; + +const findGuildRecord = "SELECT * FROM guilds WHERE native_guild_id = $1"; +const insertGuildRecord = + "INSERT INTO guilds(id, native_guild_id) VALUES(default,$1) RETURNING *"; +const findSettingsRecord = "SELECT * FROM guild_settings WHERE guild = $1"; +// after some googling it appears impossible to insert DEFAULT for a parameter +// in a query. brianc/node-postgres#1219 +const insertDefaultSettings = + "INSERT INTO guild_settings(" + + "id, guild," + + "suppress_embeds, delete_original_message, mention_user_in_reply," + + "fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts" + + ") VALUES (" + + "DEFAULT, $1," + // id, guild + "DEFAULT, DEFAULT, DEFAULT," + // suppress_embeds, delete_original_message, mention_user_in_reply + "DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT" + // fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts + ") RETURNING *"; +// const insertSettingsRecord = +// "INSERT INTO guild_settings(" + +// "id, guild," + +// "suppress_embeds, delete_original_message, mention_user_in_reply," + +// "fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts" + +// ") VALUES (" + +// "default, $1," + // id, guild +// "$2, $3, $4," + // suppress_embeds, delete_original_message, mention_user_in_reply +// "$5, $6, $7, $8, $9" + // fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts +// ") RETURNING *"; + +// todo: move this into src/bot/database +/** + * Look up a guild's record in the database. Add it to the database if one doesn't exist + * @param poolClient Checked out client from a pg pool + * @param guildId The native discord ID of a guild (i.e. 643644919751376899) + * @returns An opject representing the guild in the database. + */ +const getOrInsertGuildRecord: ( + poolClient: PoolClient, + guildId: bigint, +) => Promise = async (poolClient, guildId) => { + console.debug(`[getOrInsertGuildRecord] querying ${guildId} from database.`); + + let res = await poolClient.query(findGuildRecord, [guildId]); + + const record: object | null | GuildRecord = extractFirstRecord(res); + + if (record === null) { + console.debug( + `[getOrInsertGuildRecord] guild ${guildId} not found in database. Inserting...`, + ); + res = await poolClient.query(insertGuildRecord, [guildId]); + + return res.rows[0]; + } + + // guild exists in database + if (typeof record === "object") { + console.debug(`[getOrInsertGuildRecord] guild ${guildId} found in database.`); + return record; + } + + // idk if we should actually do this but it seems logical + poolClient.release(); + throw Error(`Unable to insert guild ${guildId} into database!`); +}; + +/** + * Look up a guild's settings in the database. Insert them if the record doesn't exist. + * @param poolClient Checked out client from a pg pool. + * @param id The index of the record for the guild we're dealing with. + * @returns An object represnting the settings for this guild in the database. + */ +const getOrInsertSettingsRecord: ( + poolClient: PoolClient, + id: number, +) => Promise = async (poolClient, id) => { + let res = await poolClient.query(findSettingsRecord, [id]); + + const record: object | null | SettingsRecord = extractFirstRecord(res); + + console.debug(`[getOrInsertSettingsRecord] record is of type ${typeof record}`); + + if (record === null) { + console.debug( + "[getOrInsertSettingsRecord] " + + `settings record related to guild with id ${id} not found in database. ` + + "Inserting...", + ); + // lol... ill fix this later... + res = await poolClient.query(insertDefaultSettings, [id]); + + return res.rows[0]; + } + + // guild exists in database + if (typeof record === "object") { + console.debug( + "[getOrInsertSettingsRecord] " + + `settings record related to guild with id ${id} found in database.`, + ); + return record; + } + + // idk if we should actually do this but it seems logical + poolClient.release(); + throw Error(`Unable to insert settings record for guild with id ${id} into database!`); +}; const commandData = new SlashCommandBuilder() .setName("configure") @@ -43,7 +152,7 @@ export const ConfigureCommand: CustomCommand = { if (typeof pool === "undefined") { await i.reply({ content: "Error: database pool is undefined.", - ephemeral: false, + ephemeral: true, }); return; } @@ -53,51 +162,48 @@ export const ConfigureCommand: CustomCommand = { if (guildId === null) { await i.reply({ content: "Error: `interaction.guildId` is `null`.", - ephemeral: false, + ephemeral: true, }); return; } + console.debug("[ConfigureCommand] guildId is valid. checking database..."); + guildId = BigInt(guildId); const poolClient = await pool.connect(); - let queryData = await poolClient.query("SELECT * FROM guilds WHERE native_guild_id = $1", [ - i.guildId, - ]); - let inserting = false; + const guildRecord: GuildRecord = await getOrInsertGuildRecord(poolClient, guildId); - // Add server to database if it does not already exist - if (queryData.rowCount === null || queryData.rowCount < 1) { - inserting = true; + console.debug( + `[ConfigureCommand] looking up settings for guild with index ${guildRecord.id}`, + ); - queryData = await poolClient.query( - "INSERT INTO guilds(id, native_guild_id) VALUES(default,$1) RETURNING *", - [guildId], - ); + const settingsRecord: SettingsRecord = await getOrInsertSettingsRecord( + poolClient, + guildRecord.id, + ); - // Error adding server to the database - if (queryData.rowCount === null || queryData.rowCount < 1) { - await i.reply({ - content: "Error inserting guild into database!", - ephemeral: false, - }); + console.debug( + `[ConfigureCommand] Settings record found or inserted into the database: ${settingsRecord.id}`, + ); - poolClient.release(); + let responseMessage = + "Your guild was found in the database:\n" + + "```d\n" + + `{ id: ${guildRecord.id}, native_guild_id: ${guildRecord.native_guild_id}}\n` + + "```\n" + + "Your guild's settings from the database:\n" + + "```d\n{\n"; - return; - } + for (const [k, v] of Object.entries(settingsRecord)) { + responseMessage += ` ${k}: ${v}\n`; } - const row = <{ id: number; native_guild_id: string }>queryData.rows[0]; + responseMessage += "}\n```"; - // man oh man did prettier make this ugly lol await i.reply({ - content: `Your guild was ${ - inserting ? "inserted into" : "found in" - } the database: \n\`\`\`\n{ id: ${row.id}, native_guild_id: ${ - row.native_guild_id - } }\n\`\`\``, + content: responseMessage, ephemeral: false, }); diff --git a/src/bot/database/initdb/schema.sql b/src/bot/database/initdb/schema.sql index 1f4f048..9defe50 100644 --- a/src/bot/database/initdb/schema.sql +++ b/src/bot/database/initdb/schema.sql @@ -3,13 +3,16 @@ CREATE TABLE IF NOT EXISTS "guilds" ( "native_guild_id" bigint UNIQUE NOT NULL ); -CREATE TABLE IF NOT EXISTS "settings" ( +CREATE TABLE IF NOT EXISTS "guild_settings" ( "id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "guild" integer UNIQUE NOT NULL REFERENCES guilds(id) ON DELETE CASCADE ON UPDATE RESTRICT, + "suppress_embeds" boolean NOT NULL DEFAULT true, + "delete_original_message" boolean NOT NULL DEFAULT false, "mention_user_in_reply" boolean NOT NULL DEFAULT false, diff --git a/src/bot/database/util/ExtractFirstRecord.ts b/src/bot/database/util/ExtractFirstRecord.ts new file mode 100644 index 0000000..b3bae10 --- /dev/null +++ b/src/bot/database/util/ExtractFirstRecord.ts @@ -0,0 +1,17 @@ +import { QueryResult } from "pg"; + +const extractFirstRecord: (res: QueryResult) => object | null = (res) => { + if (res.rowCount === null || res.rowCount < 1) { + console.debug("[extractFirstRecord] res is null."); + return null; + } + + if (typeof res.rows[0] === "object") { + console.debug("[extractFirstRecord] found record in database."); + return res.rows[0]; + } + + return null; +}; + +export default extractFirstRecord; From d6e4b4f62155645a888f2b5073fb8b119bdf3d97 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Sat, 30 Dec 2023 20:14:24 -0500 Subject: [PATCH 22/29] ci: fix call to prettier linting task --- .github/workflows/_lint.yml | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml index 7fdbd4a..35bae1a 100644 --- a/.github/workflows/_lint.yml +++ b/.github/workflows/_lint.yml @@ -16,7 +16,7 @@ jobs: - run: npm ci - - run: npm run prettier + - run: npm run prettier-check eslint: runs-on: ubuntu-latest diff --git a/package.json b/package.json index c9aceb6..b3a2ed7 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,9 @@ "start": "node dist/bot/index.js", "build": "rm -rf dist && tsc --project ./tsconfig.json", "build-prod": "rm -rf dist && tsc --project ./tsconfig.prod.json --listEmittedFiles", - "prettier-check": "prettier --check .", + "prettier-check": "prettier --log-level warn --check .", "eslint": "eslint .", - "format": "prettier --cache --write . && eslint --cache --fix .", + "format": "prettier --log-level warn --cache --write . && eslint --cache --fix .", "appcmd-cli": "ts-node src/cli/AppCommandsCLI.ts" }, "dependencies": { From 78877f3f039d13b7c9b2ac42e3a2aace7c1efa06 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Sat, 30 Dec 2023 22:56:01 -0500 Subject: [PATCH 23/29] refactor: move @types up one directory --- src/{bot => }/@types/CustomCommand.d.ts | 0 src/{bot => }/@types/DatabaseRecords.d.ts | 0 src/{bot => }/@types/Discord.d.ts | 2 -- 3 files changed, 2 deletions(-) rename src/{bot => }/@types/CustomCommand.d.ts (100%) rename src/{bot => }/@types/DatabaseRecords.d.ts (100%) rename src/{bot => }/@types/Discord.d.ts (85%) diff --git a/src/bot/@types/CustomCommand.d.ts b/src/@types/CustomCommand.d.ts similarity index 100% rename from src/bot/@types/CustomCommand.d.ts rename to src/@types/CustomCommand.d.ts diff --git a/src/bot/@types/DatabaseRecords.d.ts b/src/@types/DatabaseRecords.d.ts similarity index 100% rename from src/bot/@types/DatabaseRecords.d.ts rename to src/@types/DatabaseRecords.d.ts diff --git a/src/bot/@types/Discord.d.ts b/src/@types/Discord.d.ts similarity index 85% rename from src/bot/@types/Discord.d.ts rename to src/@types/Discord.d.ts index c6f4037..6404393 100644 --- a/src/bot/@types/Discord.d.ts +++ b/src/@types/Discord.d.ts @@ -1,11 +1,9 @@ import { Collection } from "discord.js"; import { CustomCommand } from "./CustomCommand"; -import { Pool } from "pg"; // extend discord.js Client type to allow commands collection declare module "discord.js" { export interface Client { commands: Collection; - pgPool: Pool; } } From f23c388563b274b51ccae76211e1ba6d29998e52 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Sat, 30 Dec 2023 22:56:16 -0500 Subject: [PATCH 24/29] feature: add missing pg types --- src/@types/pg.d.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/@types/pg.d.ts diff --git a/src/@types/pg.d.ts b/src/@types/pg.d.ts new file mode 100644 index 0000000..c5d6080 --- /dev/null +++ b/src/@types/pg.d.ts @@ -0,0 +1,19 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import * as pg from "pg"; + +// @types/pg doesn't include these defenitions /shrug +declare module "pg" { + /** + * Escapes a string as a SQL identifier. + * @param str The string to escape. + * @returns A token that has a fixed meaning in the SQL language. + */ + export function escapeIdentifier(str: string): string; + + /** + * Escapes a string as a SQL literal. + * @param str The string to escape. + * @returns A string representing a PostgreSQL string constant. + */ + export function escapeLiteral(str: string): string; +} From 20d8fded2acfced1b2419ba6d716365b61fac305 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Sat, 30 Dec 2023 23:59:42 -0500 Subject: [PATCH 25/29] fix: refactor tsconfig.json after moving @types --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 3ef212c..c9d98b3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "include": ["src/**/*.ts", "src/*.ts", "src/*.mts", "src/**/*.mts"], "compilerOptions": { - "typeRoots": ["src/bot/@types", "node_modules/@types"], + "typeRoots": ["src/@types", "node_modules/@types"], "declaration": true, "outDir": "dist", From e82c9ad3f88eed304d5fae1e7b96c82f561158e8 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Sun, 31 Dec 2023 01:56:18 -0500 Subject: [PATCH 26/29] feat: /configure reads from/writes to database wholly crap lois whole lotta bug testing I gotta do on this. also introduced some issues with AppCommandsCLI but I should really break that out into its own module anyway --- package-lock.json | 4 +- src/@types/DatabaseRecords.d.ts | 13 ++ src/bot/commands/Configure/configure.sql | 25 --- src/bot/commands/Configure/index.ts | 193 ++++++----------- src/bot/commands/Help.ts | 10 +- src/bot/commands/Invite.ts | 10 +- src/bot/commands/Vote.ts | 7 +- src/bot/commands/index.ts | 20 +- src/bot/database/CustomPool.ts | 30 ++- src/bot/database/index.ts | 50 ++++- src/bot/database/initdb/migrations/.gitkeep | 0 src/bot/database/queries/Guilds.ts | 29 +++ src/bot/database/queries/Settings.ts | 224 ++++++++++++++++++++ src/bot/database/util/ExtractFirstRecord.ts | 9 +- src/bot/index.ts | 22 +- src/cli/AppCommandsCLI.ts | 45 ++-- src/lib/GetFromEnvOrFile.ts | 10 +- tsconfig.json | 2 +- 18 files changed, 485 insertions(+), 218 deletions(-) delete mode 100644 src/bot/commands/Configure/configure.sql create mode 100644 src/bot/database/initdb/migrations/.gitkeep create mode 100644 src/bot/database/queries/Guilds.ts create mode 100644 src/bot/database/queries/Settings.ts diff --git a/package-lock.json b/package-lock.json index 835aeef..2db7820 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "linkfix-for-discord", - "version": "1.6.1", + "version": "1.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "linkfix-for-discord", - "version": "1.6.1", + "version": "1.6.2", "license": "AGPL-3.0-or-later", "dependencies": { "@tsconfig/node-lts": "^20.1.0", diff --git a/src/@types/DatabaseRecords.d.ts b/src/@types/DatabaseRecords.d.ts index c03f3d0..ed11f05 100644 --- a/src/@types/DatabaseRecords.d.ts +++ b/src/@types/DatabaseRecords.d.ts @@ -12,3 +12,16 @@ export type SettingsRecord = { fix_twitter: boolean; fix_yt_shorts: boolean; }; + +// idk lol.. I'll fix this "later" +export type OptionalSettings = { + suppress_embeds?: boolean; + delete_original_message?: boolean; + mention_user_in_reply?: boolean; + + fix_instagram?: boolean; + fix_reddit?: boolean; + fix_tiktok?: boolean; + fix_twitter?: boolean; + fix_yt_shorts?: boolean; +}; diff --git a/src/bot/commands/Configure/configure.sql b/src/bot/commands/Configure/configure.sql deleted file mode 100644 index 3fb9537..0000000 --- a/src/bot/commands/Configure/configure.sql +++ /dev/null @@ -1,25 +0,0 @@ --- SQL Queries in this file are unused, it's left over from using PgTyped. --- I find it helpful to have these here regardless so I haven't deleted it. - -/* @name findGuildByDiscordId */ -SELECT * FROM guilds WHERE native_guild_id = :guildId; - -/* @name insertNewServer */ -INSERT INTO guilds(id, native_guild_id) VALUES (default, :guildId) RETURNING *; - -/* @name getServerSettings */ -SELECT * FROM guild_settings WHERE guild = :guildIndex; - -/* @name createServerSettings */ -INSERT INTO guild_settings( - id, guild, - suppress_embeds, delete_original_message, mention_user_in_reply, - fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts -) VALUES ( - default, :guildIndex, - :suppressEmbeds, :deleteOriginal, :mentionUser, - :fixInstagram, :fixReddit, :fixTiktok, :fixTwitter, :fixYtShorts -) RETURNING *; - -/* @name updateSetting */ --- in code, we will replace these values with `default` diff --git a/src/bot/commands/Configure/index.ts b/src/bot/commands/Configure/index.ts index 874e57b..9e0bd3e 100644 --- a/src/bot/commands/Configure/index.ts +++ b/src/bot/commands/Configure/index.ts @@ -1,121 +1,20 @@ -import { GuildRecord, SettingsRecord } from "../../@types/DatabaseRecords"; -import { CustomCommand } from "../../@types/CustomCommand"; -import { PoolClient } from "pg"; +import { GuildRecord, OptionalSettings, SettingsRecord } from "../../../@types/DatabaseRecords"; +import { getSettings, setSettings } from "../../database/queries/Settings"; +import { CustomCommand } from "../../../@types/CustomCommand"; import { SlashCommandBuilder } from "discord.js"; -import extractFirstRecord from "../../database/util/ExtractFirstRecord"; - -const findGuildRecord = "SELECT * FROM guilds WHERE native_guild_id = $1"; -const insertGuildRecord = - "INSERT INTO guilds(id, native_guild_id) VALUES(default,$1) RETURNING *"; -const findSettingsRecord = "SELECT * FROM guild_settings WHERE guild = $1"; -// after some googling it appears impossible to insert DEFAULT for a parameter -// in a query. brianc/node-postgres#1219 -const insertDefaultSettings = - "INSERT INTO guild_settings(" + - "id, guild," + - "suppress_embeds, delete_original_message, mention_user_in_reply," + - "fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts" + - ") VALUES (" + - "DEFAULT, $1," + // id, guild - "DEFAULT, DEFAULT, DEFAULT," + // suppress_embeds, delete_original_message, mention_user_in_reply - "DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT" + // fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts - ") RETURNING *"; -// const insertSettingsRecord = -// "INSERT INTO guild_settings(" + -// "id, guild," + -// "suppress_embeds, delete_original_message, mention_user_in_reply," + -// "fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts" + -// ") VALUES (" + -// "default, $1," + // id, guild -// "$2, $3, $4," + // suppress_embeds, delete_original_message, mention_user_in_reply -// "$5, $6, $7, $8, $9" + // fix_instagram, fix_reddit, fix_tiktok, fix_twitter, fix_yt_shorts -// ") RETURNING *"; - -// todo: move this into src/bot/database -/** - * Look up a guild's record in the database. Add it to the database if one doesn't exist - * @param poolClient Checked out client from a pg pool - * @param guildId The native discord ID of a guild (i.e. 643644919751376899) - * @returns An opject representing the guild in the database. - */ -const getOrInsertGuildRecord: ( - poolClient: PoolClient, - guildId: bigint, -) => Promise = async (poolClient, guildId) => { - console.debug(`[getOrInsertGuildRecord] querying ${guildId} from database.`); - - let res = await poolClient.query(findGuildRecord, [guildId]); - - const record: object | null | GuildRecord = extractFirstRecord(res); - - if (record === null) { - console.debug( - `[getOrInsertGuildRecord] guild ${guildId} not found in database. Inserting...`, - ); - res = await poolClient.query(insertGuildRecord, [guildId]); - - return res.rows[0]; - } - - // guild exists in database - if (typeof record === "object") { - console.debug(`[getOrInsertGuildRecord] guild ${guildId} found in database.`); - return record; - } - - // idk if we should actually do this but it seems logical - poolClient.release(); - throw Error(`Unable to insert guild ${guildId} into database!`); -}; - -/** - * Look up a guild's settings in the database. Insert them if the record doesn't exist. - * @param poolClient Checked out client from a pg pool. - * @param id The index of the record for the guild we're dealing with. - * @returns An object represnting the settings for this guild in the database. - */ -const getOrInsertSettingsRecord: ( - poolClient: PoolClient, - id: number, -) => Promise = async (poolClient, id) => { - let res = await poolClient.query(findSettingsRecord, [id]); - - const record: object | null | SettingsRecord = extractFirstRecord(res); +import { getGuildRecord } from "../../database/queries/Guilds"; - console.debug(`[getOrInsertSettingsRecord] record is of type ${typeof record}`); - - if (record === null) { - console.debug( - "[getOrInsertSettingsRecord] " + - `settings record related to guild with id ${id} not found in database. ` + - "Inserting...", - ); - // lol... ill fix this later... - res = await poolClient.query(insertDefaultSettings, [id]); - - return res.rows[0]; - } - - // guild exists in database - if (typeof record === "object") { - console.debug( - "[getOrInsertSettingsRecord] " + - `settings record related to guild with id ${id} found in database.`, - ); - return record; - } - - // idk if we should actually do this but it seems logical - poolClient.release(); - throw Error(`Unable to insert settings record for guild with id ${id} into database!`); -}; - -const commandData = new SlashCommandBuilder() +export const commandData = new SlashCommandBuilder() .setName("configure") + .addBooleanOption((option) => + option + .setName("suppress_embeds") + .setDescription("Should linkfix remove embeds from messages it replies to?"), + ) .addBooleanOption((option) => option .setName("delete_on_reply") - .setDescription("Should LinkFix delete messages when it replies to them?"), + .setDescription("Should LinkFix delete messages it replies to??"), ) .addBooleanOption((option) => option @@ -145,16 +44,45 @@ const commandData = new SlashCommandBuilder() export const ConfigureCommand: CustomCommand = { data: commandData, - execute: async (i, pool) => { + execute: async (i) => { // FIXME: "Property 'getBoolean' does not exist on type 'Omiti.options.get("suppress_embeds")?.value; + updateSettings = true; + } + if (i.options.get("delete_on_reply")) { + userOptions.delete_original_message = i.options.get("delete_on_reply")?.value; + updateSettings = true; + } + if (i.options.get("mention_user")) { + userOptions.mention_user_in_reply = i.options.get("mention_user")?.value; + updateSettings = true; + } + if (i.options.get("fix_instagram")) { + userOptions.fix_instagram = i.options.get("fix_instagram")?.value; + updateSettings = true; + } + if (i.options.get("fix_reddit")) { + userOptions.fix_reddit = i.options.get("fix_reddit")?.value; + updateSettings = true; + } + if (i.options.get("fix_tiktok")) { + userOptions.fix_tiktok = i.options.get("fix_tiktok")?.value; + updateSettings = true; + } + if (i.options.get("fix_twitter")) { + userOptions.fix_twitter = i.options.get("fix_twitter")?.value; + updateSettings = true; + } + if (i.options.get("fix_youtube")) { + userOptions.fix_yt_shorts = i.options.get("fix_youtube")?.value; + updateSettings = true; } let guildId: string | bigint | null = i.guildId; @@ -167,25 +95,26 @@ export const ConfigureCommand: CustomCommand = { return; } - console.debug("[ConfigureCommand] guildId is valid. checking database..."); + console.debug("[ConfigureCommand]\tguildId is valid. checking database..."); guildId = BigInt(guildId); - const poolClient = await pool.connect(); - - const guildRecord: GuildRecord = await getOrInsertGuildRecord(poolClient, guildId); + const guildRecord: GuildRecord = await getGuildRecord(guildId); console.debug( - `[ConfigureCommand] looking up settings for guild with index ${guildRecord.id}`, + `[ConfigureCommand]\tlooking up settings for guild with index ${guildRecord.id}`, ); - const settingsRecord: SettingsRecord = await getOrInsertSettingsRecord( - poolClient, - guildRecord.id, - ); + let settingsRecord: SettingsRecord; + + if (updateSettings) { + settingsRecord = await setSettings(guildRecord, userOptions); + } else { + settingsRecord = await getSettings(guildRecord); + } console.debug( - `[ConfigureCommand] Settings record found or inserted into the database: ${settingsRecord.id}`, + `[ConfigureCommand]\tSettings record found or inserted into the database: ${settingsRecord.id}`, ); let responseMessage = @@ -193,7 +122,7 @@ export const ConfigureCommand: CustomCommand = { "```d\n" + `{ id: ${guildRecord.id}, native_guild_id: ${guildRecord.native_guild_id}}\n` + "```\n" + - "Your guild's settings from the database:\n" + + `Your guild's settings were ${updateSettings ? "updated" : "found"} in the database:\n` + "```d\n{\n"; for (const [k, v] of Object.entries(settingsRecord)) { @@ -207,8 +136,6 @@ export const ConfigureCommand: CustomCommand = { ephemeral: false, }); - poolClient.release(); - return; }, }; diff --git a/src/bot/commands/Help.ts b/src/bot/commands/Help.ts index d8cf08e..7ba5db9 100644 --- a/src/bot/commands/Help.ts +++ b/src/bot/commands/Help.ts @@ -1,11 +1,13 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; -import { CustomCommand } from "../@types/CustomCommand"; +import { CustomCommand } from "../../@types/CustomCommand"; + +export const commandData = new SlashCommandBuilder() + .setName("help") + .setDescription("Get a helpful message from LinkFix!"); export const HelpCommand: CustomCommand = { - data: new SlashCommandBuilder() - .setName("help") - .setDescription("Get a helpful message from LinkFix!"), + data: commandData, execute: async (interaction: CommandInteraction) => { await interaction.reply({ content: diff --git a/src/bot/commands/Invite.ts b/src/bot/commands/Invite.ts index 33ea350..c308d19 100644 --- a/src/bot/commands/Invite.ts +++ b/src/bot/commands/Invite.ts @@ -1,11 +1,13 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; -import { CustomCommand } from "../@types/CustomCommand"; +import { CustomCommand } from "../../@types/CustomCommand"; + +export const commandData = new SlashCommandBuilder() + .setName("invite") + .setDescription("Invite LinkFix to your server!"); export const InviteCommand: CustomCommand = { - data: new SlashCommandBuilder() - .setName("invite") - .setDescription("Invite LinkFix to your server!"), + data: commandData, execute: async (interaction: CommandInteraction) => { await interaction.reply({ content: diff --git a/src/bot/commands/Vote.ts b/src/bot/commands/Vote.ts index 7c29681..af49325 100644 --- a/src/bot/commands/Vote.ts +++ b/src/bot/commands/Vote.ts @@ -1,9 +1,12 @@ import { CommandInteraction, SlashCommandBuilder } from "discord.js"; +import { CustomCommand } from "../../@types/CustomCommand"; -import { CustomCommand } from "../@types/CustomCommand"; +export const commandData = new SlashCommandBuilder() + .setName("vote") + .setDescription("Vote for LinkFix on Top.gg!"); export const VoteCommand: CustomCommand = { - data: new SlashCommandBuilder().setName("vote").setDescription("Vote for LinkFix on Top.gg!"), + data: commandData, execute: async (interaction: CommandInteraction) => { await interaction.reply({ content: diff --git a/src/bot/commands/index.ts b/src/bot/commands/index.ts index 38e6de7..4b279dc 100644 --- a/src/bot/commands/index.ts +++ b/src/bot/commands/index.ts @@ -1,8 +1,11 @@ -import { ConfigureCommand } from "./Configure"; -import { CustomCommand } from "../@types/CustomCommand"; -import { HelpCommand } from "./Help"; -import { InviteCommand } from "./Invite"; -import { VoteCommand } from "./Vote"; +import { ConfigureCommand, commandData as ConfigureCommandData } from "./Configure"; +import { HelpCommand, commandData as HelpCommandData } from "./Help"; +import { InviteCommand, commandData as InviteCommandData } from "./Invite"; +import { VoteCommand, commandData as VoteCommandData } from "./Vote"; + +import { CustomCommand } from "../../@types/CustomCommand"; + +import { SlashCommandBuilder } from "discord.js"; export const Commands: Array = [ HelpCommand, @@ -10,3 +13,10 @@ export const Commands: Array = [ ConfigureCommand, VoteCommand, ]; + +export const commandData: Array = [ + ConfigureCommandData, + HelpCommandData, + InviteCommandData, + VoteCommandData, +]; diff --git a/src/bot/database/CustomPool.ts b/src/bot/database/CustomPool.ts index 5fd6233..c3e8775 100644 --- a/src/bot/database/CustomPool.ts +++ b/src/bot/database/CustomPool.ts @@ -67,37 +67,55 @@ const customPoolConfig: () => PoolConfig = () => { max: 16, idleTimeoutMillis: longTimeout, log: (msg) => { - console.debug(`[pgPool]\t${msg}`); + console.debug(`[pgPool]\t\t${msg}`); }, }; }; +const poolStats: (pool: Pool) => string = (pool) => { + return ( + "[poolStats]\t\t" + + `total: ${pool.totalCount}\t` + + `idle: ${pool.idleCount}\t` + + `waiting: ${pool.waitingCount}` + ); +}; + /** * Create a Pool object with the application configuration and attach event handlers. * @returns Configured Pool object with attached event handlers. */ const CustomPool: () => Pool = () => { + console.debug("[CustomPool]\t\tCreating a new connection pool."); + const pool = new Pool(customPoolConfig()); pool.on("connect", () => { - console.debug("[CustomPool]\tNew connection established"); + // TODO: Figure out how to set/clear a timeout on checked out clients so we + // get an alert if client.release() isn't called. + console.debug("[CustomPool]\t\tNew connection established."); + console.debug(poolStats(pool)); }); pool.on("acquire", () => { - console.debug("[CustomPool]\tClient checked out from pool"); + console.debug("[CustomPool]\t\tClient checked out from pool"); + console.debug(poolStats(pool)); }); pool.on("error", (err) => { - console.error(`[CustomPool]\tEncountered an error:\t${String(err)}`); + console.error(`[CustomPool]\t\tEncountered an error:\t${String(err)}`); + console.debug(poolStats(pool)); }); pool.on("release", (err) => { // Workaround: @types/pg doesn't specify err can be Error | null | undefined - console.debug(`[CustomPool]\tClient released back to pool:\t${String(err)}`); + console.debug(`[CustomPool]\t\tClient released back to pool:\t${String(err)}`); + console.debug(poolStats(pool)); }); pool.on("remove", () => { - console.debug("[CustomPool]\tClient closed and removed from pool"); + console.debug("[CustomPool]\t\tClient closed and removed from pool"); + console.debug(poolStats(pool)); }); return pool; diff --git a/src/bot/database/index.ts b/src/bot/database/index.ts index 1fe8043..3c7d7d9 100644 --- a/src/bot/database/index.ts +++ b/src/bot/database/index.ts @@ -1,3 +1,51 @@ +// Parts of this file are adapted from https://node-postgres.com/guides/project-structure + +import { QueryConfig, QueryResult } from "pg"; import CustomPool from "./CustomPool"; -export { CustomPool }; +// FIXME: This creates a database pool when we run AppCommandsCLI +const pool = CustomPool(); + +/** + * Easily execute a single query without checking a client out of the pool. + * @param queryString Optionally parameterized SQL query string + * @param values Array of values to pass into query parameters + * @returns The result of the query + */ +export const query: ( + query: string | QueryConfig, + /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- + * Parent library uses type `any` so we should use it here too. + **/ + values?: any[], +) => Promise = async (queryString, values?) => { + const start = Date.now(); + const res = await pool.query(queryString, values); + const duration = Date.now() - start; + + console.debug("[CustomPool.query]\texecuted query", { + queryString, + duration, + rows: res.rowCount, + }); + + return res; +}; + +/** + * Check a client out from our pool. Make sure to call client.release() when + * you're done! + * @returns A new client from the connection pool + */ +export const getClient = () => { + // TODO: Implement logic to check for clients not released back to the pool. + return pool.connect(); +}; + +/** + * Shut down all our clients and close the pool. Should only be called on + * sutdown. + */ +export const end = async () => { + await pool.end(); +}; diff --git a/src/bot/database/initdb/migrations/.gitkeep b/src/bot/database/initdb/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/bot/database/queries/Guilds.ts b/src/bot/database/queries/Guilds.ts new file mode 100644 index 0000000..cd1d4ca --- /dev/null +++ b/src/bot/database/queries/Guilds.ts @@ -0,0 +1,29 @@ +import * as db from ".."; +import { GuildRecord } from "../../../@types/DatabaseRecords"; +import extractFirstRecord from "../util/ExtractFirstRecord"; + +const findQuery = "SELECT * FROM guilds WHERE native_guild_id = $1"; + +const insertQuery = "INSERT INTO guilds(id, native_guild_id) VALUES(default,$1) RETURNING *"; + +/** + * Gets a guild from the database. Mainly so we can find the index and look up + * the associated settings record. + * @param guildId Guild ID from Discord, i.e. 643644919751376899 + * @returns The guild's record in our database + */ +export const getGuildRecord: (guildId: bigint | string) => Promise = async ( + guildId, +) => { + const client = await db.getClient(); + + let record = extractFirstRecord(await client.query(findQuery, [guildId])); + + if (record === null) { + record = extractFirstRecord(await client.query(insertQuery, [guildId])); + } + + client.release(); + + return record; +}; diff --git a/src/bot/database/queries/Settings.ts b/src/bot/database/queries/Settings.ts new file mode 100644 index 0000000..87e3e27 --- /dev/null +++ b/src/bot/database/queries/Settings.ts @@ -0,0 +1,224 @@ +import * as db from ".."; +import { GuildRecord, OptionalSettings, SettingsRecord } from "../../../@types/DatabaseRecords"; +import { PoolClient, escapeIdentifier } from "pg"; +import extractFirstRecord from "../util/ExtractFirstRecord"; + +const findQuery = "SELECT * FROM guild_settings WHERE guild = $1"; + +/** + * If `/configure` is run without arguments in a guild we don't have in the + * database, insert a settings record with all the default values. + * @param client The client that will run the query + * @param guild The target guild for inserting settings + * @returns The new settings record for the guild (all default values) + */ +async function initializeSettings( + client: PoolClient, + guild: GuildRecord, +): Promise { + console.debug("[initializeSettings]\tInitializing settings for guild " + String(guild.id)); + + // Defaults other than `guild` will be set by Postgres for us. + const res = await client.query( + "INSERT INTO guild_settings(guild) VALUES (" + guild.id + ") RETURNING *", + ); + + const settings = extractFirstRecord(res); + + if (settings === null) { + const err = Error("Failed to initialize guild settings record!"); + + client.release(err); + + throw err; + } + + console.debug("[initializeSettings]\tSuccessfully initialized settings!"); + + return settings; +} + +/** + * Insert a settings record into the database. Used when the record doesn't + * already exist. + * @param client The client that will run the query + * @param guild The target guild for inserting settings + * @param settings An array of settings (optionally empty) + * @returns The full settings of the guild. + */ +async function insertSettings( + client: PoolClient, + guild: GuildRecord, + settings: OptionalSettings, +): Promise { + console.debug(`[insertSettings]\tInserting settings...`); + + const values: Array = Object.values(settings); + + // I live in a nightmare world of my own creation. + const keys: string = Object.keys(settings) + .map((key) => { + return escapeIdentifier(key); + }) + .join(","); + + let queryString = "INSERT INTO guild_settings(guild," + keys + ") VALUES ($1,"; + + for (let i = 0; i < values.length; ++i) { + const twoBasedIndex = i + 2; + + queryString += "$" + String(twoBasedIndex); + + queryString += twoBasedIndex < values.length + 1 ? "," : ""; + } + + queryString += ") RETURNING *"; + + console.debug( + "[insertSettings]\tExecuting query " + + queryString + + " with values [" + + [guild.id, ...values].join(", ") + + "]", + ); + + const res = await client.query(queryString, [guild.id, ...values]); + + const newSettings = extractFirstRecord(res); + + if (newSettings === null) { + const err = Error("Failed to insert guild settings record!"); + + client.release(err); + + throw err; + } + + console.debug("[insertSettings]\tSuccessfully inserted settings!"); + + return newSettings; +} + +/** + * Update settings for a guild we already have in our database. + * @param client The client that will run the query + * @param guild The target guild for inserting settings + * @param settings An array of settings (optionally empty) + * @returns The full settings of the guild. + */ +async function updateSettings( + client: PoolClient, + guild: GuildRecord, + settings: OptionalSettings, +): Promise { + const keys = Object.keys(settings); + const values: Array = Object.values(settings); + + console.debug( + "[updateSettings]\t\tPreparing to update settings\tKeys: " + + String(keys) + + "\tValues: " + + String(values), + ); + + /** + * UPDATE guild_settings + * SET col_1 = val_1, + * col_2 = val_2, + * ... + * WHERE guild = guild.id; + */ + + let queryString = "UPDATE guild_settings SET "; + + // col_1 = $1 + keys.forEach((key, i) => { + const oneBasedIndex = i + 1; + + // add ANOTHER one because $1 is our guild ID + queryString += escapeIdentifier(key) + " = $" + String(oneBasedIndex + 1); + + // add a comma after every a = b except the last set + queryString += oneBasedIndex < keys.length ? ", " : " "; + }); + + queryString += "WHERE guild = $1 RETURNING *"; + + console.debug( + "[updateSettings]\t\tUpdating guild settings:\t" + + queryString + + `[${guild.id}, ${String(values)}]`, + ); + + const res = await client.query(queryString, [guild.id, ...values]); + + const newSettings = extractFirstRecord(res); + + if (newSettings === null) { + const err = Error("Failed to update guild settings record!"); + + client.release(err); + + throw err; + } + + console.debug("[updateSettings]\t\tSuccessfully updated settings!"); + + return newSettings; +} + +/** + * Get the settings of a guild without modifying them. + * @param guild The guild whose settings we want to fetch + * @returns The settings for that guild + */ +export async function getSettings(guild: GuildRecord): Promise { + console.debug( + "[getSettings]\t\tFetching settings for " + `{${guild.id}, ${guild.native_guild_id}}`, + ); + + const client = await db.getClient(); + + let record = extractFirstRecord(await client.query(findQuery, [guild.id])); + + if (record === null) { + console.debug("[getSettings]\t\tInitializing settings for guild " + String(guild.id)); + record = initializeSettings(client, guild); + } + + client.release(); + + return record; +} + +/** + * Set one or more settings for a guild. + * @param guild The guild whose settings we want to update + * @param settings The new settings for the guild + */ +export async function setSettings( + guild: GuildRecord, + newSettings: OptionalSettings, +): Promise { + console.debug( + "[setSettings]\t\tSetting settings for " + `{${guild.id}, ${guild.native_guild_id}}`, + ); + + const client = await db.getClient(); + + // check to see if the settings record already exists. INSERT if not, + // UPDATE if it does. + let record = extractFirstRecord(await client.query(findQuery, [guild.id])); + + if (record === null) { + console.debug("[setSettings]\t\tInserting settings for " + String(guild.id)); + record = await insertSettings(client, guild, newSettings); + } else { + console.debug("[setSettings]\t\tUpdating settings for " + String(guild.id)); + record = await updateSettings(client, guild, newSettings); + } + + client.release(); + + return record; +} diff --git a/src/bot/database/util/ExtractFirstRecord.ts b/src/bot/database/util/ExtractFirstRecord.ts index b3bae10..2ce912e 100644 --- a/src/bot/database/util/ExtractFirstRecord.ts +++ b/src/bot/database/util/ExtractFirstRecord.ts @@ -1,13 +1,18 @@ import { QueryResult } from "pg"; +/** + * Perform basic null checks on the results from a Postgres query + * @param res Response object from a postgres query + * @returns The first row if any data was returned, null otherwise + */ const extractFirstRecord: (res: QueryResult) => object | null = (res) => { if (res.rowCount === null || res.rowCount < 1) { - console.debug("[extractFirstRecord] res is null."); + console.debug("[extractFirstRecord]\tres is null."); return null; } if (typeof res.rows[0] === "object") { - console.debug("[extractFirstRecord] found record in database."); + console.debug("[extractFirstRecord]\tfound record in database."); return res.rows[0]; } diff --git a/src/bot/index.ts b/src/bot/index.ts index b2c469d..ca422e7 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -1,7 +1,7 @@ +import * as db from "./database"; import { Collection, Client as DiscordClient, Events, GatewayIntentBits } from "discord.js"; import { Commands } from "./commands"; -import { CustomCommand } from "./@types/CustomCommand"; -import { CustomPool } from "./database"; +import { CustomCommand } from "../@types/CustomCommand"; import dotenv from "dotenv"; import getFromEnvOrFile from "../lib/GetFromEnvOrFile"; import { replacements } from "./replacements"; @@ -37,22 +37,17 @@ client.once(Events.ClientReady, async (eventClient) => { `[Events.ClientReady]\tPresent in ${guildCount} ${guildCount === 1 ? "guild" : "guilds"}.`, ); - console.debug("[Events.ClientReady] Initializing Postgres connection pool..."); - - client.pgPool = CustomPool(); - - console.debug("[Events.ClientReady] Postgres connection pool established."); - // TODO: Remove this query. Just a sanity check for now :) - const res = await client.pgPool.query("SELECT * FROM guilds LIMIT 1"); + const res = await db.query("SELECT * FROM guilds LIMIT 1"); if (res.rowCount === null || res.rowCount < 1) { - console.debug("[Events.ClientReady] Database appears to be empty."); + console.debug("[Events.ClientReady]\tDatabase appears to be empty."); } else { const row = <{ id: number; native_guild_id: string }>res.rows[0]; console.debug( - `[Events.ClientReady] SELECT * FROM guilds LIMIT 1: { id: ${row.id}, native_guild_id: ${row.native_guild_id} }`, + "[Events.ClientReady]\tSELECT * FROM guilds LIMIT 1: " + + `{ id: ${row.id}, native_guild_id: ${row.native_guild_id} }`, ); } @@ -66,7 +61,8 @@ client.once(Events.ClientReady, async (eventClient) => { */ process.once("SIGTERM", () => { console.log("[process]\tSIGTERM\tShutting down..."); - void client.pgPool + + void db .end() .then( () => { @@ -95,7 +91,7 @@ client.on(Events.InteractionCreate, async (interaction) => { const command = interaction.client.commands.get(interaction.commandName); - await command.execute(interaction, client.pgPool); + await command.execute(interaction); }); /* diff --git a/src/cli/AppCommandsCLI.ts b/src/cli/AppCommandsCLI.ts index 68f54c6..c5c3ac3 100644 --- a/src/cli/AppCommandsCLI.ts +++ b/src/cli/AppCommandsCLI.ts @@ -3,7 +3,7 @@ */ import { Command, Option } from "commander"; import { REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes } from "discord.js"; -import { Commands } from "../bot/commands"; +import { commandData } from "../bot/commands"; import dotenv from "dotenv"; import getFromEnvOrFile from "../lib/GetFromEnvOrFile"; @@ -13,11 +13,13 @@ import getFromEnvOrFile from "../lib/GetFromEnvOrFile"; * @param delay Time to sleep, in seconds * @returns */ -const sleep: (delay: number) => Promise = async (delay) => { - return new Promise(() => { - setTimeout(() => {}, delay * 1000); - }); -}; +// const sleep: (delay: number) => Promise = async (delay) => { +// console.debug(`[sleep]\tsleeping for ${delay} seconds.`); + +// return new Promise(() => { +// setTimeout(() => {}, delay * 1000); +// }); +// }; /** * Make sure we are running either globally or on a guild. @@ -27,10 +29,13 @@ const sleep: (delay: number) => Promise = async (delay) => { const validateScope: (args: { global: boolean; guildId: string | undefined }) => boolean = ( args, ) => { + console.debug("[validateScope]"); + if (!args.global && !args.guildId) { console.error("error: please specify option '--global' or '--guild-id '."); return false; } + return true; }; @@ -43,12 +48,15 @@ const validateDeletionScope: (args: { deleteAll: boolean; commandId: string | undefined; }) => boolean = (args) => { + console.debug("[validateDeletionScope]"); + if (!args.deleteAll && !args.commandId) { console.error( "error: please specify option '--delete-all' or '--command-id '.", ); return false; } + return true; }; @@ -62,6 +70,7 @@ const syncCommands: (args: { guildId: string | undefined; }) => Promise = async (args) => { if (!validateScope(args)) { + console.debug("[syncCommands]\tvalidateScope() failed, bailing."); return; } @@ -70,8 +79,10 @@ const syncCommands: (args: { restClient.setToken(getFromEnvOrFile("DISCORD_BOT_TOKEN")); const commandsJSON: Array = []; - for (const cmd of Commands) { - commandsJSON.push(cmd.data.toJSON()); + + for (const cmd of commandData) { + console.debug(`[syncCommands]\tConverting command ${cmd.name} to JSON`); + commandsJSON.push(cmd.toJSON()); } try { @@ -117,6 +128,9 @@ const deleteCommands: (args: { commandId: string | undefined; }) => Promise = async (args) => { if (!validateScope(args) || !validateDeletionScope(args)) { + console.debug( + "[deleteCommands]\tvalidateScope() and validateDeletionScope() failed. bailing.", + ); return; } @@ -125,17 +139,18 @@ const deleteCommands: (args: { restClient.setToken(getFromEnvOrFile("DISCORD_BOT_TOKEN")); if (args.deleteAll && args.global) { - const timeout = 5; + // const timeout = 5; console.warn("WARNING: YOU ARE ABOUT TO DELETE **ALL** APPLICATION COMMANDS **GLOBALLY**!"); - console.log(`Waiting ${timeout} seconds to give you a chance to bail...`); + // console.log(`Waiting ${timeout} seconds to give you a chance to bail...`); - for (let i = timeout; i > 0; --i) { - console.log(`${i}...`); - // https://stackoverflow.com/a/49139664 - await sleep(1); - } + // FIXME: Idk why this stopped working + // for (let i = timeout; i > 0; --i) { + // console.log(`${i}...`); + // // https://stackoverflow.com/a/49139664 + // await sleep(1); + // } console.warn("Time's up!"); await new Promise((resolve) => setTimeout(resolve, 250)); diff --git a/src/lib/GetFromEnvOrFile.ts b/src/lib/GetFromEnvOrFile.ts index 519fbd4..a8584ad 100644 --- a/src/lib/GetFromEnvOrFile.ts +++ b/src/lib/GetFromEnvOrFile.ts @@ -13,16 +13,16 @@ const getFromEnvOrFile: (varName: string) => string = (varName) => { if (typeof varEnv === "undefined" && typeof varFile === "undefined") { throw Error( - `[getFromEnvOrFile] ${varName} and ${varName}_FILE environment variables are both undefined!`, + `[getFromEnvOrFile]\t${varName} and ${varName}_FILE environment variables are both undefined!`, ); } if (varEnv) { - console.debug(`[getFromEnvOrFile] ${varName} found in environment.`); + console.debug(`[getFromEnvOrFile]\t${varName} found in environment.`); return varEnv; } - console.debug(`[getFromEnvOrFile] Attempting to read contents of ${varFile}.`); + console.debug(`[getFromEnvOrFile]\tAttempting to read contents of ${varFile}.`); let fileContents = ""; @@ -30,10 +30,10 @@ const getFromEnvOrFile: (varName: string) => string = (varName) => { // Still have to cast varFile even though it's 100% defined at this point fileContents = fs.readFileSync(varFile, { encoding: "utf8" }); } catch (err) { - throw Error(`[getFromEnvOrFile] Error reading ${varFile}:\n` + (err).message); + throw Error(`[getFromEnvOrFile]\tError reading ${varFile}:\n` + (err).message); } - console.debug(`[getFromEnvOrFile] ${varFile} contents read successfully.`); + console.debug(`[getFromEnvOrFile]\t${varFile} contents read successfully.`); // I don't think removing newlines will ever be an issue. If it is, you can get mad at me. return fileContents.replaceAll(/\r?\n/g, ""); }; diff --git a/tsconfig.json b/tsconfig.json index c9d98b3..e1adef1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@tsconfig/node-lts/tsconfig.json", - "include": ["src/**/*.ts", "src/*.ts", "src/*.mts", "src/**/*.mts"], + "include": ["src/**/*.ts", "src/*.ts"], "compilerOptions": { "typeRoots": ["src/@types", "node_modules/@types"], From f7aba513dc6a85bc3beb13391c8a35c7fc492a92 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Sun, 31 Dec 2023 02:47:40 -0500 Subject: [PATCH 27/29] feat: add `created` and `updated` fields to initdb --- src/bot/database/initdb/schema.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bot/database/initdb/schema.sql b/src/bot/database/initdb/schema.sql index 9defe50..6a4663e 100644 --- a/src/bot/database/initdb/schema.sql +++ b/src/bot/database/initdb/schema.sql @@ -1,6 +1,10 @@ CREATE TABLE IF NOT EXISTS "guilds" ( "id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "native_guild_id" bigint UNIQUE NOT NULL + + "native_guild_id" bigint UNIQUE NOT NULL, + + "created" DATE NOT NULL DEFAULT current_date, + "updated" DATE NOT NULL DEFAULT current_date, ); CREATE TABLE IF NOT EXISTS "guild_settings" ( From 887d07fb20c45299f7bacce5f3a71a97159861fc Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Fri, 5 Jan 2024 00:58:50 -0500 Subject: [PATCH 28/29] fix: schema updates --- src/bot/database/initdb/schema.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bot/database/initdb/schema.sql b/src/bot/database/initdb/schema.sql index 6a4663e..296904d 100644 --- a/src/bot/database/initdb/schema.sql +++ b/src/bot/database/initdb/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS "guilds" ( "native_guild_id" bigint UNIQUE NOT NULL, "created" DATE NOT NULL DEFAULT current_date, - "updated" DATE NOT NULL DEFAULT current_date, + "updated" DATE NOT NULL DEFAULT current_date ); CREATE TABLE IF NOT EXISTS "guild_settings" ( @@ -31,6 +31,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS ux_guilds_id ON "guilds" ("id"); CREATE UNIQUE INDEX IF NOT EXISTS ux_guilds_native_guild_id ON "guilds" ("native_guild_id"); -CREATE UNIQUE INDEX IF NOT EXISTS ux_settings_guild ON "settings" ("guild"); +CREATE UNIQUE INDEX IF NOT EXISTS ux_settings_guild ON "guild_settings" ("guild"); COMMENT ON COLUMN "guilds"."native_guild_id" IS 'Discord Server ID'; From 309dc490f69c85fb5d7d84dfffddb0fb53d6f582 Mon Sep 17 00:00:00 2001 From: Ralph Drake Date: Fri, 5 Jan 2024 00:59:19 -0500 Subject: [PATCH 29/29] docker: only expose postgres to localhost --- docker-compose.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 1a8e979..5e9a89c 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -42,7 +42,7 @@ services: - pgdata:/var/lib/postgresql/data - ./src/bot/database/initdb/:/docker-entrypoint-initdb.d ports: - - 15432:5432 + - "127.0.0.1:15432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_USER} -U $${POSTGRES_USER}"] interval: 5s