From cf0c27ec19cda3fd105b80deaf60acdd36733232 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:04:28 +0200 Subject: [PATCH 01/35] refactor(gitignore): simpilified files affected by git --- .gitignore | 164 ++++++++--------------------------------------------- 1 file changed, 23 insertions(+), 141 deletions(-) diff --git a/.gitignore b/.gitignore index 8d0f034..0c739ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,141 +1,23 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -plugins.db - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.* -!.env.example - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Sveltekit cache directory -.svelte-kit/ - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# Firebase cache directory -.firebase/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v3 -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Vite logs files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* +# IDE files. +/.idea/ +/.vscode/ +/.vscode-test/ +/.vs/ +/.cursor/ + +# Dependency directories. +/node_modules/ +/jspm_packages/ +/web_modules/ +# And NPM's cache directory. +/npm-cache/ +/.npm/ + +# Distribution files & directories. +/dist/ +/build/ +/**/*.tsbuildinfo + +# Environment variables. +/**/*.env* +!/.env.example From ababefd10527203790d07d92ccd96cd1a366fea4 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:13:52 +0200 Subject: [PATCH 02/35] refactor(tsconfig): refactored rules of TypeScript --- bun.lock | 1 + tsconfig.json | 44 +++++++++++++++++++++++++------------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/bun.lock b/bun.lock index 5d59e1f..8d923c8 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "api-service", diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..a4ca148 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,35 @@ { + "compileOnSave": true, "compilerOptions": { - // Environment setup & latest features + // Environment and module settings. "lib": ["ESNext"], "target": "ESNext", - "module": "Preserve", + "module": "ESNext", "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, + "moduleResolution": "Bundler", - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices + // Language and structural settings. + "allowJs": false, + "checkJs": false, "strict": true, + // This applies to distribution builds. + "removeComments": true, + "forceConsistentCasingInFileNames": true, "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + "noImplicitReturns": true, + "noUnusedLocals": true, + "noImplicitAny": true, + "rootDir": "./src", + // This only applies to building the project. + "outDir": "./dist" + }, + "include": [ + "src", + ], + "exclude": [ + "node_modules/", + "dist/", + "build/", + ] } From 3d56d60da45d61ef1ad0b6cd673767b94219d33c Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:23:32 +0200 Subject: [PATCH 03/35] refactor(package.json): refactored data inside `package.json` --- package.json | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ae8c499..aaa10d9 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,29 @@ { + "name": "@serenityjs/api-service", + "description": "Rest API Service for SerenityJS.", + "version": "1.0.0", "private": true, - "name": "api-service", - "module": "src/index.ts", + "license": "MIT", + "author": { + "name": "SerenityJS", + "url": "https://serenityjs.com" + }, + "homepage": "https://github.com/SerenityJS/api-service", + "repository": { + "type": "git", + "url": "https://github.com/SerenityJS/api-service.git" + }, + "bugs": { + "url": "https://github.com/SerenityJS/api-service/issues" + }, + "engines": { + "bun": ">=1.3" + }, + "main": "dist/index.js", "type": "module", "scripts": { - "start": "bun run src/index.ts", + "build": "bun build src/index.ts --outdir dist --target bun", + "start": "bun run dist/index.js", "dev": "bun run --watch src/index.ts" }, "devDependencies": { From f46e81ed772610817d0ad573701e11fc24448663 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:25:44 +0200 Subject: [PATCH 04/35] chore(prettier): added Prettier to repository --- .gitignore | 4 ++++ .prettierignore | 23 +++++++++++++++++++++++ .prettierrc.json | 26 ++++++++++++++++++++++++++ bun.lock | 3 +++ package.json | 5 ++++- 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.gitignore b/.gitignore index 0c739ac..c2c988e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ # Environment variables. /**/*.env* !/.env.example + +# Operating system files. +/**/.DS_Store +/**/*.ini diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..0c739ac --- /dev/null +++ b/.prettierignore @@ -0,0 +1,23 @@ +# IDE files. +/.idea/ +/.vscode/ +/.vscode-test/ +/.vs/ +/.cursor/ + +# Dependency directories. +/node_modules/ +/jspm_packages/ +/web_modules/ +# And NPM's cache directory. +/npm-cache/ +/.npm/ + +# Distribution files & directories. +/dist/ +/build/ +/**/*.tsbuildinfo + +# Environment variables. +/**/*.env* +!/.env.example diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..ca4c24e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,26 @@ +{ + "useTabs": false, + "tabWidth": 4, + "printWidth": 120, + "singleQuote": false, + "trailingComma": "all", + "arrowParens": "always", + "semi": false, + "bracketSpacing": true, + "endOfLine": "auto", + "overrides": [ + { + "files": "**/*.{json,jsonc}", + "options": { + "printWidth": 80, + "trailingComma": "none" + } + }, + { + "files": "**/*.{yaml,yml}", + "options": { + "printWidth": 80 + } + } + ] +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 8d923c8..13c4664 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ }, "devDependencies": { "@types/bun": "latest", + "prettier": "^3.8.3", }, "peerDependencies": { "typescript": "^5", @@ -199,6 +200,8 @@ "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], diff --git a/package.json b/package.json index aaa10d9..da6fbaa 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,15 @@ "main": "dist/index.js", "type": "module", "scripts": { + "format": "bunx prettier --write .", + "format:check": "bunx prettier --check .", "build": "bun build src/index.ts --outdir dist --target bun", "start": "bun run dist/index.js", "dev": "bun run --watch src/index.ts" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "prettier": "^3.8.3" }, "peerDependencies": { "typescript": "^5" From fba4be16a921df35c5359aea78f9b45da44f26ab Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:38:34 +0200 Subject: [PATCH 05/35] chore(eslint): added ESLint to repository --- bun.lock | 333 +++++++++++++++++++++++++++++++++++++++++++++++ eslint.config.ts | 70 ++++++++++ package.json | 11 +- 3 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 eslint.config.ts diff --git a/bun.lock b/bun.lock index 13c4664..8ac0d9a 100644 --- a/bun.lock +++ b/bun.lock @@ -13,8 +13,15 @@ "express": "^5.1.0", }, "devDependencies": { + "@eslint/js": "^10.0.1", + "@eslint/json": "^1.2.0", + "@eslint/markdown": "^8.0.1", "@types/bun": "latest", + "eslint": "^10.2.0", + "globals": "^17.5.0", + "jiti": "^2.6.1", "prettier": "^3.8.3", + "typescript-eslint": "^8.58.2", }, "peerDependencies": { "typescript": "^5", @@ -34,6 +41,36 @@ "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="], + + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], + + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], + + "@eslint/json": ["@eslint/json@1.2.0", "", { "dependencies": { "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanwhocodes/momoa": "^3.3.10", "natural-compare": "^1.4.0" } }, "sha512-CEFEyNgvzu8zn5QwVYDg3FaG+ZKUeUsNYitFpMYJAqoAlnw68EQgNbUfheSmexZr4n0wZPrAkPLuvsLaXO6wRw=="], + + "@eslint/markdown": ["@eslint/markdown@8.0.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "github-slugger": "^2.0.0", "mdast-util-from-markdown": "^2.0.2", "mdast-util-frontmatter": "^2.0.1", "mdast-util-gfm": "^3.1.0", "mdast-util-math": "^3.0.0", "micromark-extension-frontmatter": "^2.0.0", "micromark-extension-gfm": "^3.0.0", "micromark-extension-math": "^3.1.0", "micromark-util-normalize-identifier": "^2.0.1" } }, "sha512-WWKmld/EyNdEB8GMq7JMPX1SDWgyJAM1uhtCi5ySrqYQM4HQjmg11EX/q3ZpnpRXHfdccFtli3NBvvGaYjWyQw=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/momoa": ["@humanwhocodes/momoa@3.3.10", "", {}, "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], @@ -48,14 +85,30 @@ "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], @@ -68,18 +121,50 @@ "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -88,8 +173,14 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -100,14 +191,24 @@ "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "discord-api-types": ["discord-api-types@0.38.22", "", {}, "sha512-2gnYrgXN3yTlv2cKBISI/A8btZwsSZLwKpIQXeI1cS8a7W7wP3sFVQOm3mPuuinTD8jJCKGPGNH399zE7Un1kA=="], "discord.js": ["discord.js@14.22.1", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.16", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w=="], @@ -128,18 +229,54 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.4", "@eslint/config-helpers": "^0.5.4", "@eslint/core": "^1.2.0", "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA=="], + + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -150,6 +287,12 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -162,30 +305,150 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -196,16 +459,32 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -218,12 +497,18 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -234,36 +519,84 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + "typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="], + "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@typescript-eslint/project-service/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@typescript-eslint/type-utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], } } diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..b7cce05 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,70 @@ +import js from "@eslint/js" +import globals from "globals" +import tseslint from "typescript-eslint" +import json from "@eslint/json" +import markdown from "@eslint/markdown" +import { defineConfig } from "eslint/config" + +/** + * @summary ESLint configuration. + * @description Configuration of the backend application. + * @see {@link https://eslint.org/docs/latest/use/configure/configuration-files} + */ +export default defineConfig([ + tseslint.configs.recommended, + { + ignores: ["**/*.js", "dist/**"], + }, + { + files: ["**/*.ts"], + plugins: { js: js as never }, + extends: ["js/recommended"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + "no-undef": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_{1,2}", + varsIgnorePattern: "^_{1,2}", + }, + ], + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_{1,2}", + varsIgnorePattern: "^_{1,2}", + }, + ], + }, + }, + { + files: ["**/*.json"], + plugins: { json: json as never }, + language: "json/json", + extends: ["json/recommended"], + }, + { + files: ["**/*.jsonc"], + plugins: { json: json as never }, + language: "json/jsonc", + extends: ["json/recommended"], + }, + { + files: ["**/*.json5"], + plugins: { json: json as never }, + language: "json/json5", + extends: ["json/recommended"], + }, + { + files: ["**/*.md"], + plugins: { markdown: markdown as never }, + language: "markdown/gfm", + extends: ["markdown/recommended"], + }, +]) diff --git a/package.json b/package.json index da6fbaa..83ed470 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,22 @@ "scripts": { "format": "bunx prettier --write .", "format:check": "bunx prettier --check .", + "lint": "bunx eslint .", + "lint:fix": "bunx eslint --fix .", "build": "bun build src/index.ts --outdir dist --target bun", "start": "bun run dist/index.js", "dev": "bun run --watch src/index.ts" }, "devDependencies": { + "@eslint/js": "^10.0.1", + "@eslint/json": "^1.2.0", + "@eslint/markdown": "^8.0.1", "@types/bun": "latest", - "prettier": "^3.8.3" + "eslint": "^10.2.0", + "globals": "^17.5.0", + "jiti": "^2.6.1", + "prettier": "^3.8.3", + "typescript-eslint": "^8.58.2" }, "peerDependencies": { "typescript": "^5" From 634dc0804126f1d7f50a46ccb6c0f590d7ad8600 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:39:50 +0200 Subject: [PATCH 06/35] chore(pre-commit): added Husky & lint-staged to repository --- .husky/pre-commit | 4 ++ bun.lock | 64 ++++++++++++++++++++++++- package.json | 118 +++++++++++++++++++++++++--------------------- 3 files changed, 132 insertions(+), 54 deletions(-) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..546d921 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/bash +# This file is being executed every single time before you commit via git. + +bunx lint-staged diff --git a/bun.lock b/bun.lock index 8ac0d9a..51707f4 100644 --- a/bun.lock +++ b/bun.lock @@ -19,7 +19,9 @@ "@types/bun": "latest", "eslint": "^10.2.0", "globals": "^17.5.0", + "husky": "^9.1.7", "jiti": "^2.6.1", + "lint-staged": "^16.4.0", "prettier": "^3.8.3", "typescript-eslint": "^8.58.2", }, @@ -155,6 +157,12 @@ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], @@ -177,9 +185,15 @@ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], @@ -217,8 +231,12 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -249,6 +267,8 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -283,6 +303,8 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -303,6 +325,8 @@ "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -315,6 +339,8 @@ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], @@ -335,12 +361,18 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lint-staged": ["lint-staged@16.4.0", "", { "dependencies": { "commander": "^14.0.3", "listr2": "^9.0.5", "picomatch": "^4.0.3", "string-argv": "^0.3.2", "tinyexec": "^1.0.4", "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw=="], + + "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], @@ -443,6 +475,8 @@ "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -459,6 +493,8 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -491,6 +527,10 @@ "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -517,8 +557,20 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -561,10 +613,14 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -591,12 +647,18 @@ "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], } } diff --git a/package.json b/package.json index 83ed470..586fe85 100644 --- a/package.json +++ b/package.json @@ -1,55 +1,67 @@ { - "name": "@serenityjs/api-service", - "description": "Rest API Service for SerenityJS.", - "version": "1.0.0", - "private": true, - "license": "MIT", - "author": { - "name": "SerenityJS", - "url": "https://serenityjs.com" - }, - "homepage": "https://github.com/SerenityJS/api-service", - "repository": { - "type": "git", - "url": "https://github.com/SerenityJS/api-service.git" - }, - "bugs": { - "url": "https://github.com/SerenityJS/api-service/issues" - }, - "engines": { - "bun": ">=1.3" - }, - "main": "dist/index.js", - "type": "module", - "scripts": { - "format": "bunx prettier --write .", - "format:check": "bunx prettier --check .", - "lint": "bunx eslint .", - "lint:fix": "bunx eslint --fix .", - "build": "bun build src/index.ts --outdir dist --target bun", - "start": "bun run dist/index.js", - "dev": "bun run --watch src/index.ts" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@eslint/json": "^1.2.0", - "@eslint/markdown": "^8.0.1", - "@types/bun": "latest", - "eslint": "^10.2.0", - "globals": "^17.5.0", - "jiti": "^2.6.1", - "prettier": "^3.8.3", - "typescript-eslint": "^8.58.2" - }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "@types/cors": "^2.8.19", - "@types/express": "^5.0.3", - "axios": "^1.11.0", - "cors": "^2.8.5", - "discord.js": "^14.22.1", - "express": "^5.1.0" - } + "name": "@serenityjs/api-service", + "description": "Rest API Service for SerenityJS.", + "version": "1.0.0", + "private": true, + "license": "MIT", + "author": { + "name": "SerenityJS", + "url": "https://serenityjs.com" + }, + "homepage": "https://github.com/SerenityJS/api-service", + "repository": { + "type": "git", + "url": "https://github.com/SerenityJS/api-service.git" + }, + "bugs": { + "url": "https://github.com/SerenityJS/api-service/issues" + }, + "engines": { + "bun": ">=1.3" + }, + "main": "dist/index.js", + "type": "module", + "scripts": { + "format": "bunx prettier --write .", + "format:check": "bunx prettier --check .", + "lint": "bunx eslint .", + "lint:fix": "bunx eslint --fix .", + "build": "bun build src/index.ts --outdir dist --target bun", + "start": "bun run dist/index.js", + "dev": "bun run --watch src/index.ts", + "prepare": "husky" + }, + "lint-staged": { + "**/*.{ts,json,jsonc,md}": [ + "bunx eslint --fix", + "bunx prettier --write" + ], + "**/*.{yml,yaml}": [ + "bunx prettier --write" + ] + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@eslint/json": "^1.2.0", + "@eslint/markdown": "^8.0.1", + "@types/bun": "latest", + "eslint": "^10.2.0", + "globals": "^17.5.0", + "husky": "^9.1.7", + "jiti": "^2.6.1", + "lint-staged": "^16.4.0", + "prettier": "^3.8.3", + "typescript-eslint": "^8.58.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "axios": "^1.11.0", + "cors": "^2.8.5", + "discord.js": "^14.22.1", + "express": "^5.1.0" + } } From 19f4d4b90e046a513666ed5a2710bafe6f53f341 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:40:25 +0200 Subject: [PATCH 07/35] chore(husky): added more rulesets --- .husky/commit-msg | 13 +++++++++++++ .husky/pre-push | 6 ++++++ 2 files changed, 19 insertions(+) create mode 100644 .husky/commit-msg create mode 100644 .husky/pre-push diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..4fe9424 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,13 @@ +#!/bin/bash +# This script validates commits' message to make sure that they're +# correct with Conventional Commits standard. + +message="$(sed -n '1p' "$1")" + +if printf "%s" "$message" | grep -Eq '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+'; then + exit 0 +fi + +echo "Invalid commit message format." +echo "Use Conventional Commits, for example: feat(pages): improved login page's design" +exit 1 \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..b91221a --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,6 @@ +#!/bin/bash +# This file is being executed every single time before you push +# via git to GitHub repository. + +bun run format:check +bun run lint:fix From a458a22924563551f1c432077991cb4fec241c50 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:43:38 +0200 Subject: [PATCH 08/35] style(prettier): formatted whole repository --- .prettierrc.json | 2 +- README.md | 1 + eslint.config.ts | 2 + src/index.ts | 322 +++++++++-------- src/plugin.ts | 531 +++++++++++++++------------- src/routes/index.ts | 11 +- src/routes/plugin.ts | 38 +- src/routes/plugins.ts | 24 +- src/routes/route.ts | 36 +- src/services/discord.ts | 227 ++++++------ src/services/gh-fetch.ts | 215 +++++------ src/services/index.ts | 13 +- src/services/service.ts | 34 +- src/types/index.ts | 18 +- src/types/plugin.ts | 42 +-- src/types/release-asset.ts | 30 +- src/types/repository-commit.ts | 60 ++-- src/types/repository-contributor.ts | 6 +- src/types/repository-license.ts | 12 +- src/types/repository-query.ts | 10 +- src/types/repository-release.ts | 46 +-- src/types/repository-user.ts | 40 +-- src/types/repository.ts | 166 ++++----- src/types/stored-plugin.ts | 16 +- tsconfig.json | 58 ++- 25 files changed, 998 insertions(+), 962 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index ca4c24e..9f9528d 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -23,4 +23,4 @@ } } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 4ea0fa6..20a9a5f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # api-service + Rest API Service for SerenityJS diff --git a/eslint.config.ts b/eslint.config.ts index b7cce05..d4efb5a 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -27,6 +27,7 @@ export default defineConfig([ }, rules: { "no-undef": "off", + "no-empty": "off", "@typescript-eslint/no-unused-vars": [ "error", { @@ -44,6 +45,7 @@ export default defineConfig([ }, }, { + ignores: ["tsconfig.json"], files: ["**/*.json"], plugins: { json: json as never }, language: "json/json", diff --git a/src/index.ts b/src/index.ts index 598f267..2799ea0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,88 +1,90 @@ -import { Database } from "bun:sqlite"; -import { Server } from "node:http"; +import { Database } from "bun:sqlite" +import { Server } from "node:http" -import express, { type Express } from "express"; -import cors from "cors"; +import express, { type Express } from "express" +import cors from "cors" -import { Routes } from "./routes"; -import { Services, type Service } from "./services"; -import type { StoredPlugin } from "./types"; -import type { Plugin } from "./plugin"; +import { Routes } from "./routes" +import { Services, type Service } from "./services" +import type { StoredPlugin } from "./types" +import type { Plugin } from "./plugin" class RestAPIService { - /** - * The Express application instance - */ - private readonly express: Express = express(); - - /** - * The HTTP server instance - */ - private readonly server: Server = this.express.listen(4000); - - /** - * The map of service instances - */ - private readonly services = new Map(); - - /** - * The SQLite database instance - */ - private readonly db: Database = new Database("plugins.db"); - - /** - * The in-memory cache of plugins - */ - private readonly plugins = new Map(); - - public constructor() { - // Enable JSON body parsing middleware - this.express.use(express.json()); - this.express.use(cors()); - - // Iterate over each route and register it with the Express app - for (const route of Routes) { - // Switch on the HTTP method and register the route accordingly - switch(route.method as string) { - default: - case "GET": { - this.express.get(route.path, route.handle); - - continue; + /** + * The Express application instance + */ + private readonly express: Express = express() + + /** + * The HTTP server instance + */ + private readonly server: Server = this.express.listen(4000) + + /** + * The map of service instances + */ + private readonly services = new Map() + + /** + * The SQLite database instance + */ + private readonly db: Database = new Database("plugins.db") + + /** + * The in-memory cache of plugins + */ + private readonly plugins = new Map() + + public constructor() { + // Enable JSON body parsing middleware + this.express.use(express.json()) + this.express.use(cors()) + + // Iterate over each route and register it with the Express app + for (const route of Routes) { + // Switch on the HTTP method and register the route accordingly + switch (route.method as string) { + default: + case "GET": { + this.express.get(route.path, route.handle) + + continue + } + + case "POST": { + this.express.post(route.path, route.handle) + + continue + } + } } - case "POST": { - this.express.post(route.path, route.handle); + // Iterate over each service and create an instance + for (const service of Services) { + // Create an instance of the service + const instance = new service(this) - continue; + // Store the service instance in the map + this.services.set(instance.name, instance) } - } - } - - // Iterate over each service and create an instance - for (const service of Services) { - // Create an instance of the service - const instance = new service(this); - // Store the service instance in the map - this.services.set(instance.name, instance); + // Prepare the database + this.prepareDatabase() } - // Prepare the database - this.prepareDatabase(); - } - - public getService(name: string): T | null { - // Get the service instance from the map - const service = this.services.get(name) as T | undefined; + public getService(name: string): T | null { + // Get the service instance from the map + const service = this.services.get(name) as T | undefined - // Return the service instance or null if not found - return service ?? null; - } + // Return the service instance or null if not found + return service ?? null + } - private prepareDatabase(): void { - // Create a table to store aproved plugins - this.db.prepare(` + private prepareDatabase(): void { + // Create a table to store aproved plugins + this.db + .prepare( + ` CREATE TABLE IF NOT EXISTS plugins ( id INTEGER PRIMARY KEY AUTOINCREMENT, approved BOOLEAN DEFAULT 0, @@ -90,99 +92,109 @@ class RestAPIService { owner TEXT NOT NULL, url TEXT NOT NULL, branch TEXT DEFAULT 'main' - );`).run(); - } - - public hasPlugin(id: number): boolean { - // Check if a plugin with the given id exists in the database - const row = this.db.prepare("SELECT id FROM plugins WHERE id = ?").get(id); - - // Return true if the plugin exists, false otherwise - return !!row; - } - - public addStoredPlugin(plugin: StoredPlugin): void { - // Insert a new plugin into the database - this.db.prepare("INSERT INTO plugins (id, name, owner, url, approved, branch) VALUES (?, ?, ?, ?, ?, ?)") - .run(plugin.id, plugin.name, JSON.stringify(plugin.owner), plugin.url, plugin.approved, plugin.branch); - } - - public getStoredPlugin(id: number): StoredPlugin | null { - // Get a plugin with the given id from the database - const row = this.db.prepare("SELECT * FROM plugins WHERE id = ?").get(id); - - // Return the plugin or null if not found - return row ? { - id: row.id, - name: row.name, - owner: JSON.parse(row.owner), - url: row.url, - branch: row.branch, - approved: row.approved === 1, - } : null; - } - - public updateStoredPlugin(id: number, plugin: Partial): void { - // Prepare a field list and values for the update query - const fields = []; - const values = []; - - // Iterate over the plugin properties and prepare the fields and values - for (const [key, value] of Object.entries(plugin)) { - fields.push(`${key} = ?`); - - // Check if the value is an object and needs to be stringified - if (key === "owner" && typeof value === "object") { - values.push(JSON.stringify(value)); - continue; - } else { - values.push(value as string | number | boolean); - } + );`, + ) + .run() + } + + public hasPlugin(id: number): boolean { + // Check if a plugin with the given id exists in the database + const row = this.db.prepare("SELECT id FROM plugins WHERE id = ?").get(id) + + // Return true if the plugin exists, false otherwise + return !!row + } + + public addStoredPlugin(plugin: StoredPlugin): void { + // Insert a new plugin into the database + this.db + .prepare("INSERT INTO plugins (id, name, owner, url, approved, branch) VALUES (?, ?, ?, ?, ?, ?)") + .run(plugin.id, plugin.name, JSON.stringify(plugin.owner), plugin.url, plugin.approved, plugin.branch) + } + + public getStoredPlugin(id: number): StoredPlugin | null { + // Get a plugin with the given id from the database + const row = this.db + .prepare< + { id: number; name: string; owner: string; url: string; branch: string; approved: number }, + number + >("SELECT * FROM plugins WHERE id = ?") + .get(id) + + // Return the plugin or null if not found + return row + ? { + id: row.id, + name: row.name, + owner: JSON.parse(row.owner), + url: row.url, + branch: row.branch, + approved: row.approved === 1, + } + : null } - // Add the id to the values array for the WHERE clause - values.push(id); + public updateStoredPlugin(id: number, plugin: Partial): void { + // Prepare a field list and values for the update query + const fields = [] + const values = [] + + // Iterate over the plugin properties and prepare the fields and values + for (const [key, value] of Object.entries(plugin)) { + fields.push(`${key} = ?`) + + // Check if the value is an object and needs to be stringified + if (key === "owner" && typeof value === "object") { + values.push(JSON.stringify(value)) + continue + } else { + values.push(value as string | number | boolean) + } + } - // Update the plugin in the database - this.db.prepare(`UPDATE plugins SET ${fields.join(", ")} WHERE id = ?`).run(...values); - } + // Add the id to the values array for the WHERE clause + values.push(id) - public addCachedPlugin(plugin: Plugin): void { - // Add a plugin to the in-memory cache - this.plugins.set(plugin.id, plugin); - } + // Update the plugin in the database + this.db.prepare(`UPDATE plugins SET ${fields.join(", ")} WHERE id = ?`).run(...values) + } + + public addCachedPlugin(plugin: Plugin): void { + // Add a plugin to the in-memory cache + this.plugins.set(plugin.id, plugin) + } - public clearPluginCache(): void { - // Clear the in-memory cache of plugins - this.plugins.clear(); - } + public clearPluginCache(): void { + // Clear the in-memory cache of plugins + this.plugins.clear() + } - public isPluginApproved(id: number): boolean { - // Check if a plugin with the given id is approved in the database - const row = this.db.prepare("SELECT approved FROM plugins WHERE id = ?").get(id); + public isPluginApproved(id: number): boolean { + // Check if a plugin with the given id is approved in the database + const row = this.db.prepare<{ approved: number }, number>("SELECT approved FROM plugins WHERE id = ?").get(id) - // Return true if the plugin is approved, false otherwise - return row?.approved === 1; - } + // Return true if the plugin is approved, false otherwise + return row?.approved === 1 + } - public setPluginApproval(id: number, approved: boolean): void { - // Update the approval status of a plugin in the database - this.db.prepare("UPDATE plugins SET approved = ? WHERE id = ?").run(approved ? 1 : 0, id); - } + public setPluginApproval(id: number, approved: boolean): void { + // Update the approval status of a plugin in the database + this.db.prepare("UPDATE plugins SET approved = ? WHERE id = ?").run(approved ? 1 : 0, id) + } - public getPluginFromCache(id: number): Plugin | null { - // Get the plugin from the in-memory cache - const plugin = this.plugins.get(id); + public getPluginFromCache(id: number): Plugin | null { + // Get the plugin from the in-memory cache + const plugin = this.plugins.get(id) - // Return the plugin or null if not found - return plugin ?? null; - } + // Return the plugin or null if not found + return plugin ?? null + } - public getAllPluginsFromCache(): Array { - // Return all plugins from the in-memory cache as an array - return Array.from(this.plugins.values()); - } + public getAllPluginsFromCache(): Array { + // Return all plugins from the in-memory cache as an array + return Array.from(this.plugins.values()) + } } -export default new RestAPIService(); -export { RestAPIService }; +export default new RestAPIService() +export { RestAPIService } diff --git a/src/plugin.ts b/src/plugin.ts index c54ffc9..a7ff348 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,306 +1,333 @@ -import axios from "axios"; - -import type { PluginCommit, PluginContributor, PluginRelease, Repository, RepositoryContributor, RepositoryRelease, StoredPlugin } from "./types"; -import type { RepositoryCommit } from "./types/repository-commit"; +import axios from "axios" + +import type { + PluginCommit, + PluginContributor, + PluginRelease, + Repository, + RepositoryContributor, + RepositoryRelease, + StoredPlugin, +} from "./types" +import type { RepositoryCommit } from "./types/repository-commit" class Plugin implements StoredPlugin { - /** - * The unique identifier of the plugin (GitHub repository ID) - */ - public readonly id: number; + /** + * The unique identifier of the plugin (GitHub repository ID) + */ + public readonly id: number - /** - * The name of the plugin (GitHub repository name) - */ - public readonly name: string; + /** + * The name of the plugin (GitHub repository name) + */ + public readonly name: string - /** - * The owner of the plugin (GitHub repository owner) - */ - public readonly owner: PluginContributor; + /** + * The owner of the plugin (GitHub repository owner) + */ + public readonly owner: PluginContributor - /** - * The URL of the plugin (GitHub repository URL) - */ - public readonly url: string; + /** + * The URL of the plugin (GitHub repository URL) + */ + public readonly url: string - /** - * The default branch of the plugin repository (assumed to be "main") - */ - public readonly branch: string = "main"; + /** + * The default branch of the plugin repository (assumed to be "main") + */ + public readonly branch: string = "main" - /** - * Whether the plugin has been approved for listing - */ - public readonly approved: boolean; + /** + * Whether the plugin has been approved for listing + */ + public readonly approved: boolean - public description: string | null = null; + public description: string | null = null - public version: string | null = null; + public version: string | null = null - public stars: number | null = null; + public stars: number | null = null - public downloads: number | null = null; + public downloads: number | null = null - public forks: number | null = null; + public forks: number | null = null - public issues: number | null = null; + public issues: number | null = null - public keywords: Array | null = null; + public keywords: Array | null = null - public logo: string | null = null; + public logo: string | null = null - public banner: string | null = null; + public banner: string | null = null - public published: string | null = null; + public published: string | null = null - public updated: string | null = null; + public updated: string | null = null - public readme: string | null = null; + public readme: string | null = null - public gallery: Array = []; + public gallery: Array = [] - public contributors: Array = []; + public contributors: Array = [] - public commits: Array = []; + public commits: Array = [] - public releases: Array = []; + public releases: Array = [] - /** - * Creates an instance of the Plugin. - * @param data The stored plugin data. - */ - private constructor(data: StoredPlugin, repository: Repository) { - // Load the basic data from the stored plugin object - this.id = data.id; - this.name = data.name; - this.owner = data.owner; - this.url = data.url; - this.approved = data.approved; - + /** + * Creates an instance of the Plugin. + * @param data The stored plugin data. + */ + private constructor(data: StoredPlugin, repository: Repository) { + // Load the basic data from the stored plugin object + this.id = data.id + this.name = data.name + this.owner = data.owner + this.url = data.url + this.approved = data.approved - // Load additional data from the repository object - this.description = repository.description; - this.branch = repository.default_branch; - this.stars = repository.stargazers_count; - this.forks = repository.forks_count; - this.issues = repository.open_issues_count; - this.published = repository.created_at; - this.updated = repository.updated_at; - } + // Load additional data from the repository object + this.description = repository.description + this.branch = repository.default_branch + this.stars = repository.stargazers_count + this.forks = repository.forks_count + this.issues = repository.open_issues_count + this.published = repository.created_at + this.updated = repository.updated_at + } - public static async create(data: StoredPlugin, repository: Repository): Promise { - // Create a new Plugin instance - const plugin = new Plugin(data, repository); + public static async create(data: StoredPlugin, repository: Repository): Promise { + // Create a new Plugin instance + const plugin = new Plugin(data, repository) - // Fetch the logo URL - plugin.logo = await this.getLogoURL(data); + // Fetch the logo URL + plugin.logo = await this.getLogoURL(data) - // Fetch the banner URL - plugin.banner = await this.getBannerURL(data); + // Fetch the banner URL + plugin.banner = await this.getBannerURL(data) - // Fetch the releases, commits & map the total downloads - plugin.releases = await this.getReleases(data); - plugin.commits = await this.getCommitHistory(data); - plugin.downloads = plugin.releases.reduce((acc, release) => acc + release.assets.reduce((a, asset) => a + asset.download_count, 0), 0); + // Fetch the releases, commits & map the total downloads + plugin.releases = await this.getReleases(data) + plugin.commits = await this.getCommitHistory(data) + plugin.downloads = plugin.releases.reduce( + (acc, release) => acc + release.assets.reduce((a, asset) => a + asset.download_count, 0), + 0, + ) - // Fetch the contributors - plugin.contributors = await this.getContributors(data); + // Fetch the contributors + plugin.contributors = await this.getContributors(data) - // Fetch the gallery images - plugin.gallery = await plugin.getGallery(); + // Fetch the gallery images + plugin.gallery = await plugin.getGallery() - // Fetch the readme - plugin.readme = await this.getReadme(data); + // Fetch the readme + plugin.readme = await this.getReadme(data) - // Fetch the package.json to get the version and keywords - const packageData = await this.getPackageJSON(data); - if (packageData) { - plugin.version = packageData.version; - plugin.keywords = packageData.keywords; + // Fetch the package.json to get the version and keywords + const packageData = await this.getPackageJSON(data) + if (packageData) { + plugin.version = packageData.version + plugin.keywords = packageData.keywords + } + + // Return the plugin instance + return plugin } - // Return the plugin instance - return plugin; - } - - public static async getReleases(plugin: StoredPlugin): Promise> { - try { - // Check if the plugin has any releases on GitHub - const response = await axios.get>(`https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/releases`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the releases if the request was successful - return response.data.map(release => ({ - name: release.name || release.tag_name, - tag: release.tag_name, - url: release.html_url, - description: release.body ?? "", - prerelease: release.prerelease, - date: release.published_at, - assets: release.assets.map((asset) => ({ - name: asset.name, - size: asset.size, - download_url: asset.browser_download_url, - download_count: asset.download_count, - })), - })); - } catch { - // Return an empty array if the request failed - return []; + public static async getReleases(plugin: StoredPlugin): Promise> { + try { + // Check if the plugin has any releases on GitHub + const response = await axios.get>( + `https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/releases`, + { + headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, + }, + ) + + // Return the releases if the request was successful + return response.data.map((release) => ({ + name: release.name || release.tag_name, + tag: release.tag_name, + url: release.html_url, + description: release.body ?? "", + prerelease: release.prerelease, + date: release.published_at, + assets: release.assets.map((asset) => ({ + name: asset.name, + size: asset.size, + download_url: asset.browser_download_url, + download_count: asset.download_count, + })), + })) + } catch { + // Return an empty array if the request failed + return [] + } } - } - - public static async getContributors(plugin: StoredPlugin): Promise> { - try { - // Check if the plugin has any contributors on GitHub - const response = await axios.get>(`https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/contributors`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the contributors if the request was successful - return response.data.map(contributor => ({ - username: contributor.login, - profile_url: contributor.html_url, - avatar_url: contributor.avatar_url, - contributions: contributor.contributions, - })); - } catch { - // Return an empty array if the request failed - return []; + + public static async getContributors(plugin: StoredPlugin): Promise> { + try { + // Check if the plugin has any contributors on GitHub + const response = await axios.get>( + `https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/contributors`, + { + headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, + }, + ) + + // Return the contributors if the request was successful + return response.data.map((contributor) => ({ + username: contributor.login, + profile_url: contributor.html_url, + avatar_url: contributor.avatar_url, + contributions: contributor.contributions, + })) + } catch { + // Return an empty array if the request failed + return [] + } } - } - public static async getLogoURL(plugin: StoredPlugin): Promise { - // The logo URL is assumed to be at a standard location in the repository - const url = `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/public/logo.png`; + public static async getLogoURL(plugin: StoredPlugin): Promise { + // The logo URL is assumed to be at a standard location in the repository + const url = `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/public/logo.png` - try { - // Check if the logo exists by making a HEAD request - const response = await axios.head(url); + try { + // Check if the logo exists by making a HEAD request + const response = await axios.head(url) - // Return the logo URL if it exists, otherwise return a default logo - return response.status === 200 ? url : "https://avatars.githubusercontent.com/u/92610726?s=88&v=4"; - } catch { - return "https://avatars.githubusercontent.com/u/92610726?s=88&v=4"; + // Return the logo URL if it exists, otherwise return a default logo + return response.status === 200 ? url : "https://avatars.githubusercontent.com/u/92610726?s=88&v=4" + } catch { + return "https://avatars.githubusercontent.com/u/92610726?s=88&v=4" + } } - } - public static async getBannerURL(plugin: StoredPlugin): Promise { - // The banner URL is assumed to be at a standard location in the repository - const url = `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/public/banner.png`; + public static async getBannerURL(plugin: StoredPlugin): Promise { + // The banner URL is assumed to be at a standard location in the repository + const url = `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/public/banner.png` - try { - // Check if the banner exists by making a HEAD request - const response = await axios.head(url); + try { + // Check if the banner exists by making a HEAD request + const response = await axios.head(url) - // Return the banner URL if it exists, otherwise return null - return response.status === 200 ? url : null; - } catch { - return null; + // Return the banner URL if it exists, otherwise return null + return response.status === 200 ? url : null + } catch { + return null + } } - } - - public static async getReadme(plugin: StoredPlugin): Promise { - try { - // Get the readme file from the repository - const response = await axios.get(`https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/README.md`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the readme content - return response.data; - } catch { - // Return null if the request failed - return null; + + public static async getReadme(plugin: StoredPlugin): Promise { + try { + // Get the readme file from the repository + const response = await axios.get( + `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/README.md`, + { + headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, + }, + ) + + // Return the readme content + return response.data + } catch { + // Return null if the request failed + return null + } } - } - - public static async getPackageJSON(plugin: StoredPlugin): Promise<{ version: string, keywords: Array } | null> { - try { - // Get the package.json file from the repository - const response = await axios.get<{ version: string, keywords: Array }>(`https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/package.json`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the package.json content - return response.data; - } catch { - console.log("Failed to fetch package.json"); - - // Return null if the request failed - return null; + + public static async getPackageJSON( + plugin: StoredPlugin, + ): Promise<{ version: string; keywords: Array } | null> { + try { + // Get the package.json file from the repository + const response = await axios.get<{ version: string; keywords: Array }>( + `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/package.json`, + { + headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, + }, + ) + + // Return the package.json content + return response.data + } catch { + console.log("Failed to fetch package.json") + + // Return null if the request failed + return null + } } - } - - public async getGallery(): Promise> { - // The gallery images are assumed to be in a standard location in the repository - // Assumed path ./public/gallery/*.png - const images = []; - - // Fetch all images in the gallery (up to 10 images) - for (let i = 1; i <= 10; i++) { - try { - // Construct the image URL - const url = `https://raw.githubusercontent.com/${this.owner.username}/${this.name}/${this.branch}/public/gallery/image${i}.png`; - - // Check if the image exists by making a HEAD request - const response = await axios.head(url); - - // If the image exists, add it to the gallery - if (response.status === 200) { - images.push(url); - } else { - break; + + public async getGallery(): Promise> { + // The gallery images are assumed to be in a standard location in the repository + // Assumed path ./public/gallery/*.png + const images = [] + + // Fetch all images in the gallery (up to 10 images) + for (let i = 1; i <= 10; i++) { + try { + // Construct the image URL + const url = `https://raw.githubusercontent.com/${this.owner.username}/${this.name}/${this.branch}/public/gallery/image${i}.png` + + // Check if the image exists by making a HEAD request + const response = await axios.head(url) + + // If the image exists, add it to the gallery + if (response.status === 200) { + images.push(url) + } else { + break + } + } catch { + break + } } - } catch { - break; - } + + // Return the gallery images + return images } - // Return the gallery images - return images; - } - - public static async getRepository(plugin: StoredPlugin): Promise { - try { - // Get the repository details from GitHub - const response = await axios.get(`https://api.github.com/repositories/${plugin.id}`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }); - - // Return the repository details - return response.data; - } catch { - // Return null if the request failed - return null; + public static async getRepository(plugin: StoredPlugin): Promise { + try { + // Get the repository details from GitHub + const response = await axios.get(`https://api.github.com/repositories/${plugin.id}`, { + headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, + }) + + // Return the repository details + return response.data + } catch { + // Return null if the request failed + return null + } } - } - - public static async getCommitHistory(plugin: StoredPlugin): Promise> { - try { - // Get the commit history from GitHub - const response = await axios.get>(`https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/commits`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - params: { per_page: 100 }, - }); - - console.log(response.data) - - // Return the commit history - return response.data.map(commit => ({ - sha: commit.sha, - html_url: commit.html_url, - message: commit.commit.message, - date: commit.commit.author.date, - author: commit.author?.login || commit.commit.author.name, - })); - } catch { - // Return an empty array if the request failed - return []; + + public static async getCommitHistory(plugin: StoredPlugin): Promise> { + try { + // Get the commit history from GitHub + const response = await axios.get>( + `https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/commits`, + { + headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, + params: { per_page: 100 }, + }, + ) + + console.log(response.data) + + // Return the commit history + return response.data.map((commit) => ({ + sha: commit.sha, + html_url: commit.html_url, + message: commit.commit.message, + date: commit.commit.author.date, + author: commit.author?.login || commit.commit.author.name, + })) + } catch { + // Return an empty array if the request failed + return [] + } } - } } -export { Plugin }; \ No newline at end of file +export { Plugin } diff --git a/src/routes/index.ts b/src/routes/index.ts index 84f77e9..4d39413 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,9 +1,6 @@ -import { GetPluginRoute } from "./plugin"; -import { GetPluginsRoute } from "./plugins"; +import { GetPluginRoute } from "./plugin" +import { GetPluginsRoute } from "./plugins" -const Routes = [ - GetPluginRoute, - GetPluginsRoute -] +const Routes = [GetPluginRoute, GetPluginsRoute] -export { Routes }; +export { Routes } diff --git a/src/routes/plugin.ts b/src/routes/plugin.ts index d745b2a..0ad8038 100644 --- a/src/routes/plugin.ts +++ b/src/routes/plugin.ts @@ -1,29 +1,29 @@ -import type { Request, Response } from "express"; -import { Route } from "./route"; +import type { Request, Response } from "express" +import { Route } from "./route" -import RestAPIService from "../index"; +import RestAPIService from "../index" class GetPluginRoute extends Route { - public static override readonly path: string = "/plugin/{:id}"; + public static override readonly path: string = "/plugin/{:id}" - public static override readonly method = "GET"; + public static override readonly method = "GET" - public static override async handle(req: Request, res: Response): Promise { - // Get the plugin ID from the request parameters - const id = Number(req.params.id); + public static override async handle(req: Request, res: Response): Promise { + // Get the plugin ID from the request parameters + const id = Number(req.params.id) - // Get the plugin from the cache - const plugin = RestAPIService.getPluginFromCache(id); + // Get the plugin from the cache + const plugin = RestAPIService.getPluginFromCache(id) - // Check if the plugin was found - if (!plugin) { - // Send a 404 response if the plugin was not found - res.status(404).send({ message: `Plugin with ID ${id} not found` }); - } else { - // Send the plugin as the response - res.status(200).send(plugin); + // Check if the plugin was found + if (!plugin) { + // Send a 404 response if the plugin was not found + res.status(404).send({ message: `Plugin with ID ${id} not found` }) + } else { + // Send the plugin as the response + res.status(200).send(plugin) + } } - } } -export { GetPluginRoute }; \ No newline at end of file +export { GetPluginRoute } diff --git a/src/routes/plugins.ts b/src/routes/plugins.ts index 7d595cd..09988f5 100644 --- a/src/routes/plugins.ts +++ b/src/routes/plugins.ts @@ -1,20 +1,20 @@ -import type { Request, Response } from "express"; +import type { Request, Response } from "express" -import { Route } from "./route"; -import RestAPIService from "../index"; +import { Route } from "./route" +import RestAPIService from "../index" class GetPluginsRoute extends Route { - public static override readonly path: string = "/plugins"; + public static override readonly path: string = "/plugins" - public static override readonly method = "GET"; + public static override readonly method = "GET" - public static override async handle(_req: Request, res: Response): Promise { - // Get all plugins from the cache - const plugins = RestAPIService.getAllPluginsFromCache(); + public static override async handle(_req: Request, res: Response): Promise { + // Get all plugins from the cache + const plugins = RestAPIService.getAllPluginsFromCache() - // Send the plugins as the response - res.status(200).send(plugins); - } + // Send the plugins as the response + res.status(200).send(plugins) + } } -export { GetPluginsRoute }; \ No newline at end of file +export { GetPluginsRoute } diff --git a/src/routes/route.ts b/src/routes/route.ts index 0370894..6bc1bbe 100644 --- a/src/routes/route.ts +++ b/src/routes/route.ts @@ -1,24 +1,24 @@ -import type { Request, Response } from "express"; +import type { Request, Response } from "express" abstract class Route { - /** - * The path for this route (e.g., "/plugins"). - */ - public static readonly path: string = "/"; + /** + * The path for this route (e.g., "/plugins"). + */ + public static readonly path: string = "/" - /** - * The HTTP method for this route (e.g., "GET", "POST"). - */ - public static readonly method: "GET" | "POST" | "PUT" | "DELETE"; + /** + * The HTTP method for this route (e.g., "GET", "POST"). + */ + public static readonly method: "GET" | "POST" | "PUT" | "DELETE" - /** - * Handle an incoming HTTP request. - * @param req The incoming request object. - * @param res The outgoing response object. - */ - public static async handle(_req: Request, _res: Response): Promise { - _res.status(501).send("Route::handle - Not Implemented"); - } + /** + * Handle an incoming HTTP request. + * @param req The incoming request object. + * @param res The outgoing response object. + */ + public static async handle(_req: Request, _res: Response): Promise { + _res.status(501).send("Route::handle - Not Implemented") + } } -export { Route }; +export { Route } diff --git a/src/services/discord.ts b/src/services/discord.ts index ec790d9..42b16f9 100644 --- a/src/services/discord.ts +++ b/src/services/discord.ts @@ -1,33 +1,43 @@ -import { ActionRow, ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder, Events, GatewayIntentBits, type GuildTextBasedChannel, type Interaction, type Snowflake, type TextBasedChannel } from "discord.js"; - -import type { RestAPIService } from "../index"; -import { Service } from "./service"; -import type { StoredPlugin } from "../types"; -import type { GHFetch } from "./gh-fetch"; -import { Plugin } from "../plugin"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + Client, + EmbedBuilder, + Events, + GatewayIntentBits, + type GuildTextBasedChannel, + type Interaction, + type Snowflake, +} from "discord.js" + +import type { RestAPIService } from "../index" +import { Service } from "./service" +import type { StoredPlugin } from "../types" +import { Plugin } from "../plugin" class Discord extends Service { - private readonly pluginApprovalChannelId: Snowflake = "1411567293552529462"; - - /** - * The Discord bot token for the client to login - */ - private readonly token: string = process.env.DISCORD_TOKEN ?? ""; - - /** - * The Discord client instance - */ - private readonly client: Client = new Client({ intents: [GatewayIntentBits.Guilds] }); - - /** - * A promise that resolves when the client is ready - */ - private readonly ready = new Promise((resolve) => this.client.once(Events.ClientReady, () => resolve())); - - /** - * Instructions for approving or rejecting a plugin - */ - private readonly approvalInstructions: string = ` + private readonly pluginApprovalChannelId: Snowflake = "1411567293552529462" + + /** + * The Discord bot token for the client to login + */ + private readonly token: string = process.env.DISCORD_TOKEN ?? "" + + /** + * The Discord client instance + */ + private readonly client: Client = new Client({ intents: [GatewayIntentBits.Guilds] }) + + /** + * A promise that resolves when the client is ready + */ + private readonly ready = new Promise((resolve) => this.client.once(Events.ClientReady, () => resolve())) + + /** + * Instructions for approving or rejecting a plugin + */ + private readonly approvalInstructions: string = ` **Verify that the plugin meets the following criteria:** - The plugin is relevant to SerenityJS and its ecosystem.\n @@ -37,95 +47,90 @@ class Discord extends Service { - The plugin follows best practices for coding and design.\n Please review the plugin and **approve** or **reject** it by clicking one of the buttons below. - `; - - public constructor(api: RestAPIService) { - super(api); + ` - // Login to Discord with the bot token - this.client.login(this.token); + public constructor(api: RestAPIService) { + super(api) - // Bind the interaction handler to the client - this.client.on(Events.InteractionCreate, this.handleInteraction.bind(this)); - } + // Login to Discord with the bot token + this.client.login(this.token) - public async sendPluginApprovalRequest(plugin: StoredPlugin): Promise { - // Wait until the client is ready - await this.ready; + // Bind the interaction handler to the client + this.client.on(Events.InteractionCreate, this.handleInteraction.bind(this)) + } - // Fetch the channel by its ID - const channel = await this.client.channels.fetch(this.pluginApprovalChannelId) as GuildTextBasedChannel | null; - - // Check if the channel exists and is text-based - if (!channel || !channel.isTextBased()) { - console.error(`Channel with ID ${this.pluginApprovalChannelId} not found or is not text-based.`); - return; + public async sendPluginApprovalRequest(plugin: StoredPlugin): Promise { + // Wait until the client is ready + await this.ready + + // Fetch the channel by its ID + const channel = (await this.client.channels.fetch(this.pluginApprovalChannelId)) as GuildTextBasedChannel | null + + // Check if the channel exists and is text-based + if (!channel || !channel.isTextBased()) { + console.error(`Channel with ID ${this.pluginApprovalChannelId} not found or is not text-based.`) + return + } + + // Create the embed message + const embed = new EmbedBuilder() + .setTitle("New Plugin Approval Request") + .setDescription( + `A new plugin has been submitted for approval:\n\n**Name:** ${plugin.name}\n**Owner:** ${plugin.owner.username}\n**URL:** ${plugin.url}\n\n${this.approvalInstructions}`, + ) + .setColor(0x8560e9) + .setThumbnail(await Plugin.getLogoURL(plugin)) + + // Create the action row with approve and reject buttons + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`approve:${plugin.id}`).setLabel("Approve").setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId(`reject:${plugin.id}`).setLabel("Reject").setStyle(ButtonStyle.Danger), + ) + + // Send the message to the channel + await channel.send({ embeds: [embed], components: [row] }) } - // Create the embed message - const embed = new EmbedBuilder() - .setTitle("New Plugin Approval Request") - .setDescription(`A new plugin has been submitted for approval:\n\n**Name:** ${plugin.name}\n**Owner:** ${plugin.owner.username}\n**URL:** ${plugin.url}\n\n${this.approvalInstructions}`) - .setColor(0x8560E9) - .setThumbnail(await Plugin.getLogoURL(plugin)); - - // Create the action row with approve and reject buttons - const row = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(`approve:${plugin.id}`) - .setLabel("Approve") - .setStyle(ButtonStyle.Success), - new ButtonBuilder() - .setCustomId(`reject:${plugin.id}`) - .setLabel("Reject") - .setStyle(ButtonStyle.Danger), - ); - - // Send the message to the channel - await channel.send({ embeds: [embed], components: [row] }); - } - - private async handleInteraction(interaction: Interaction): Promise { - // Check if the interaction is a button interaction - if (interaction.isButton()) { - // Get the action and plugin ID from the button custom ID - const [action, pluginId] = interaction.customId.split(":"); - - // Check if the action is valid - if (action === "approve" || action === "reject") { - // Determine if the plugin is approved or rejected - const approved = action === "approve"; - - // Update the plugin approval status in the database - this.api.setPluginApproval(Number(pluginId), approved); - - // Disable the buttons after the action is taken - interaction.message.edit({ components: [] }); - - // Reply to the interaction to acknowledge it - await interaction.reply({ content: `Plugin ${approved ? "approved" : "rejected"}.` }); - - // Get the stored plugin from the database - const stored = this.api.getStoredPlugin(Number(pluginId)); - - // If the plugin is approved and the stored plugin exists - if (approved && stored) { - // Get the repository data from GitHub - const repository = await Plugin.getRepository(stored); - - // If the repository data exists - if (!repository) return; - - // Create a Plugin instance from the stored data and repository data - const plugin = await Plugin.create(stored, repository); - - // Add the plugin to the in-memory cache - this.api.addCachedPlugin(plugin); - } - } + private async handleInteraction(interaction: Interaction): Promise { + // Check if the interaction is a button interaction + if (interaction.isButton()) { + // Get the action and plugin ID from the button custom ID + const [action, pluginId] = interaction.customId.split(":") + + // Check if the action is valid + if (action === "approve" || action === "reject") { + // Determine if the plugin is approved or rejected + const approved = action === "approve" + + // Update the plugin approval status in the database + this.api.setPluginApproval(Number(pluginId), approved) + + // Disable the buttons after the action is taken + interaction.message.edit({ components: [] }) + + // Reply to the interaction to acknowledge it + await interaction.reply({ content: `Plugin ${approved ? "approved" : "rejected"}.` }) + + // Get the stored plugin from the database + const stored = this.api.getStoredPlugin(Number(pluginId)) + + // If the plugin is approved and the stored plugin exists + if (approved && stored) { + // Get the repository data from GitHub + const repository = await Plugin.getRepository(stored) + + // If the repository data exists + if (!repository) return + + // Create a Plugin instance from the stored data and repository data + const plugin = await Plugin.create(stored, repository) + + // Add the plugin to the in-memory cache + this.api.addCachedPlugin(plugin) + } + } + } } - } } -export { Discord }; +export { Discord } diff --git a/src/services/gh-fetch.ts b/src/services/gh-fetch.ts index 1a1260e..7060301 100644 --- a/src/services/gh-fetch.ts +++ b/src/services/gh-fetch.ts @@ -1,115 +1,116 @@ -import axios from "axios"; +import axios from "axios" -import type { RestAPIService } from "../index"; +import type { RestAPIService } from "../index" -import { Service } from "./service"; -import type { PluginContributor, PluginRelease, RepositoryContributor, RepositoryQuery, RepositoryRelease, StoredPlugin } from "../types"; -import type { Discord } from "./discord"; -import { Plugin } from "../plugin"; +import { Service } from "./service" +import type { RepositoryQuery, StoredPlugin } from "../types" +import type { Discord } from "./discord" +import { Plugin } from "../plugin" // This service will handle fetching plugins from GitHub class GHFetch extends Service { - /** - * The GitHub API endpoint for searching repositories - */ - private readonly endpoint = "https://api.github.com/search/repositories"; - - /** - * The query to search for Serenity/JS plugins - */ - private readonly query = "topic:serenityjs-plugin"; - - /** - * The time interval (in milliseconds) to refresh the plugin list from GitHub (default: 5 minutes) - */ - private refreshTime = 5 * 60 * 1000; - - /** - * The time interval (in milliseconds) to clear the plugin cache (default: 1 hour) - */ - private cacheClearTime = 60 * 60 * 1000; - - public constructor(api: RestAPIService) { - super(api); - - // Fetch the plugins immediately - this.fetchPlugins(); - - // Set an interval to fetch the plugins periodically - setInterval(() => this.fetchPlugins(), this.refreshTime); - - // Set an interval to clear the plugin cache periodically - setInterval(() => {this.api.clearPluginCache(); this.fetchPlugins()}, this.cacheClearTime); - } - - public async fetchPlugins(): Promise { - try { - // Get the list of repositories from the GitHub API - const response = await axios.get(this.endpoint, { - params: { - q: this.query, - }, - headers: { - Accept: "application/vnd.github.v3+json", - }, - }); - - // Iterate over the repositories and log their names - for (const repo of response.data.items) { - // Check if the plugin is already in the database - if (this.api.hasPlugin(repo.id)) { - // Verify if the plugin is approved or if the plugin id already exists in the cache - if (!this.api.isPluginApproved(repo.id) || this.api.getPluginFromCache(repo.id)) continue; - - // Fetch the stored plugin data from the database - const stored = this.api.getStoredPlugin(repo.id) as StoredPlugin; - - // Create a Plugin instance from the stored data and repository data - const plugin = await Plugin.create(stored, repo); - - this.api.addCachedPlugin(plugin); - - console.log(`Plugin: ${repo.name} (${repo.id}) by ${repo.owner.login} - ${repo.url}`); - - } else { - // Create a StoredPlugin object - const storage: StoredPlugin = { - id: repo.id, - name: repo.name, - owner: { - username: repo.owner.login, - profile_url: repo.owner.html_url, - avatar_url: repo.owner.avatar_url, - contributions: 0, - }, - branch: repo.default_branch, - url: repo.html_url, - approved: false, - }; - - // Check if the plugin has releases, if not, skip it - if ((await Plugin.getReleases(storage)).length === 0) { - // Log that the plugin has no releases - console.log(`Plugin: ${repo.name} by ${repo.owner.login} has no releases, skipping...`); - - // Continue to the next repository - continue; - } - - // If not, add it to the database - this.api.addStoredPlugin(storage); - - // Get the Discord service instance - const discord = this.api.getService("Discord"); - - // Send a message to the Discord channel for plugin approval - if (discord) discord.sendPluginApprovalRequest(storage) - } - } - - } catch (error) {} - } + /** + * The GitHub API endpoint for searching repositories + */ + private readonly endpoint = "https://api.github.com/search/repositories" + + /** + * The query to search for Serenity/JS plugins + */ + private readonly query = "topic:serenityjs-plugin" + + /** + * The time interval (in milliseconds) to refresh the plugin list from GitHub (default: 5 minutes) + */ + private refreshTime = 5 * 60 * 1000 + + /** + * The time interval (in milliseconds) to clear the plugin cache (default: 1 hour) + */ + private cacheClearTime = 60 * 60 * 1000 + + public constructor(api: RestAPIService) { + super(api) + + // Fetch the plugins immediately + this.fetchPlugins() + + // Set an interval to fetch the plugins periodically + setInterval(() => this.fetchPlugins(), this.refreshTime) + + // Set an interval to clear the plugin cache periodically + setInterval(() => { + this.api.clearPluginCache() + this.fetchPlugins() + }, this.cacheClearTime) + } + + public async fetchPlugins(): Promise { + try { + // Get the list of repositories from the GitHub API + const response = await axios.get(this.endpoint, { + params: { + q: this.query, + }, + headers: { + Accept: "application/vnd.github.v3+json", + }, + }) + + // Iterate over the repositories and log their names + for (const repo of response.data.items) { + // Check if the plugin is already in the database + if (this.api.hasPlugin(repo.id)) { + // Verify if the plugin is approved or if the plugin id already exists in the cache + if (!this.api.isPluginApproved(repo.id) || this.api.getPluginFromCache(repo.id)) continue + + // Fetch the stored plugin data from the database + const stored = this.api.getStoredPlugin(repo.id) as StoredPlugin + + // Create a Plugin instance from the stored data and repository data + const plugin = await Plugin.create(stored, repo) + + this.api.addCachedPlugin(plugin) + + console.log(`Plugin: ${repo.name} (${repo.id}) by ${repo.owner.login} - ${repo.url}`) + } else { + // Create a StoredPlugin object + const storage: StoredPlugin = { + id: repo.id, + name: repo.name, + owner: { + username: repo.owner.login, + profile_url: repo.owner.html_url, + avatar_url: repo.owner.avatar_url, + contributions: 0, + }, + branch: repo.default_branch, + url: repo.html_url, + approved: false, + } + + // Check if the plugin has releases, if not, skip it + if ((await Plugin.getReleases(storage)).length === 0) { + // Log that the plugin has no releases + console.log(`Plugin: ${repo.name} by ${repo.owner.login} has no releases, skipping...`) + + // Continue to the next repository + continue + } + + // If not, add it to the database + this.api.addStoredPlugin(storage) + + // Get the Discord service instance + const discord = this.api.getService("Discord") + + // Send a message to the Discord channel for plugin approval + if (discord) discord.sendPluginApprovalRequest(storage) + } + } + } catch {} + } } -export { GHFetch }; +export { GHFetch } diff --git a/src/services/index.ts b/src/services/index.ts index c0304da..11aabd1 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,11 +1,8 @@ -import { Discord } from "./discord"; -import { GHFetch } from "./gh-fetch"; +import { Discord } from "./discord" +import { GHFetch } from "./gh-fetch" -export * from "./service"; +export * from "./service" -const Services = [ - GHFetch, - Discord -]; +const Services = [GHFetch, Discord] -export { Services }; \ No newline at end of file +export { Services } diff --git a/src/services/service.ts b/src/services/service.ts index 4f2b1d1..ce75adb 100644 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1,23 +1,23 @@ -import type { RestAPIService } from "../index"; +import type { RestAPIService } from "../index" abstract class Service { - /** - * The name of the service - */ - public readonly name: string = this.constructor.name; + /** + * The name of the service + */ + public readonly name: string = this.constructor.name - /** - * The api service instance - */ - public readonly api: RestAPIService; + /** + * The api service instance + */ + public readonly api: RestAPIService - /** - * Creates an instance of the service. - * @param api The api service instance. - */ - public constructor(api: RestAPIService) { - this.api = api; - } + /** + * Creates an instance of the service. + * @param api The api service instance. + */ + public constructor(api: RestAPIService) { + this.api = api + } } -export { Service }; +export { Service } diff --git a/src/types/index.ts b/src/types/index.ts index 7489c0a..8d581a9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,9 +1,9 @@ -export * from "./repository"; -export * from "./release-asset"; -export * from "./repository-user"; -export * from "./repository-query"; -export * from "./repository-license"; -export * from "./repository-release"; -export * from "./repository-contributor"; -export * from "./stored-plugin"; -export * from "./plugin"; \ No newline at end of file +export * from "./repository" +export * from "./release-asset" +export * from "./repository-user" +export * from "./repository-query" +export * from "./repository-license" +export * from "./repository-release" +export * from "./repository-contributor" +export * from "./stored-plugin" +export * from "./plugin" diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 57d5e03..afdae2a 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -1,33 +1,33 @@ interface PluginRelease { - name: string; - tag: string; - url: string; - description: string; - prerelease: boolean; - date: string; - assets: Array; + name: string + tag: string + url: string + description: string + prerelease: boolean + date: string + assets: Array } interface PluginReleaseAsset { - name: string; - size: number; - download_url: string; - download_count: number; + name: string + size: number + download_url: string + download_count: number } interface PluginContributor { - username: string; - profile_url: string; - avatar_url: string; - contributions: number; + username: string + profile_url: string + avatar_url: string + contributions: number } interface PluginCommit { - sha: string; - html_url: string; - message: string; - date: string; - author: string; + sha: string + html_url: string + message: string + date: string + author: string } -export type { PluginRelease, PluginReleaseAsset, PluginContributor, PluginCommit }; +export type { PluginRelease, PluginReleaseAsset, PluginContributor, PluginCommit } diff --git a/src/types/release-asset.ts b/src/types/release-asset.ts index d6057aa..621d05e 100644 --- a/src/types/release-asset.ts +++ b/src/types/release-asset.ts @@ -1,19 +1,19 @@ -import type { RepositoryUser } from "./repository-user"; +import type { RepositoryUser } from "./repository-user" interface ReleaseAsset { - url: string; - id: number; - node_id: string; - name: string; - label: string | null; - uploader: RepositoryUser; - content_type: string; - state: "uploaded" | "open" | "temporary" | string; - size: number; - download_count: number; - created_at: string; // ISO date - updated_at: string; // ISO date - browser_download_url: string; + url: string + id: number + node_id: string + name: string + label: string | null + uploader: RepositoryUser + content_type: string + state: "uploaded" | "open" | "temporary" | string + size: number + download_count: number + created_at: string // ISO date + updated_at: string // ISO date + browser_download_url: string } -export type { ReleaseAsset }; +export type { ReleaseAsset } diff --git a/src/types/repository-commit.ts b/src/types/repository-commit.ts index ee5b177..ce77999 100644 --- a/src/types/repository-commit.ts +++ b/src/types/repository-commit.ts @@ -1,46 +1,46 @@ -import type { RepositoryUser } from "./repository-user"; +import type { RepositoryUser } from "./repository-user" // GitHub "List commits" item → https://docs.github.com/rest/commits/commits#list-commits export interface RepositoryCommit { - sha: string; - node_id: string; - commit: GitCommit; // metadata from the commit object itself - url: string; // API URL for this commit - html_url: string; // Web URL for this commit - comments_url: string; - author: RepositoryUser | null; // GitHub account (may be null if email not linked) - committer: RepositoryUser | null; // GitHub account (may be null) - parents: GitParent[]; // parent commits + sha: string + node_id: string + commit: GitCommit // metadata from the commit object itself + url: string // API URL for this commit + html_url: string // Web URL for this commit + comments_url: string + author: RepositoryUser | null // GitHub account (may be null if email not linked) + committer: RepositoryUser | null // GitHub account (may be null) + parents: GitParent[] // parent commits } export interface GitCommit { - author: GitCommitAuthor; // name/email/date from the commit - committer: GitCommitAuthor; // name/email/date from the commit - message: string; - tree: { sha: string; url: string }; - url: string; - comment_count: number; - verification: GitCommitVerification; + author: GitCommitAuthor // name/email/date from the commit + committer: GitCommitAuthor // name/email/date from the commit + message: string + tree: { sha: string; url: string } + url: string + comment_count: number + verification: GitCommitVerification } export interface GitCommitAuthor { - name: string; - email: string; - date: string; // ISO date + name: string + email: string + date: string // ISO date } export interface GitCommitVerification { - verified: boolean; - reason: string; - signature: string | null; - payload: string | null; - // Some repos may include extra fields; keep optional: - verified_at?: string | null; - verifier?: RepositoryUser | null; + verified: boolean + reason: string + signature: string | null + payload: string | null + // Some repos may include extra fields; keep optional: + verified_at?: string | null + verifier?: RepositoryUser | null } export interface GitParent { - sha: string; - url: string; - html_url: string; + sha: string + url: string + html_url: string } diff --git a/src/types/repository-contributor.ts b/src/types/repository-contributor.ts index b046de7..78079fa 100644 --- a/src/types/repository-contributor.ts +++ b/src/types/repository-contributor.ts @@ -1,7 +1,7 @@ -import type { RepositoryUser } from "./repository-user"; +import type { RepositoryUser } from "./repository-user" interface RepositoryContributor extends RepositoryUser { - contributions: number; + contributions: number } -export type { RepositoryContributor }; +export type { RepositoryContributor } diff --git a/src/types/repository-license.ts b/src/types/repository-license.ts index f0d3c07..2be11ba 100644 --- a/src/types/repository-license.ts +++ b/src/types/repository-license.ts @@ -1,9 +1,9 @@ interface RepositoryLicense { - key: string; - name: string; - spdx_id: string | null; - url: string | null; - node_id: string; + key: string + name: string + spdx_id: string | null + url: string | null + node_id: string } -export type { RepositoryLicense }; +export type { RepositoryLicense } diff --git a/src/types/repository-query.ts b/src/types/repository-query.ts index 41ff05e..5f8245a 100644 --- a/src/types/repository-query.ts +++ b/src/types/repository-query.ts @@ -1,9 +1,9 @@ -import type { Repository } from "./repository"; +import type { Repository } from "./repository" interface RepositoryQuery { - total_count: number; - incomplete_results: boolean; - items: Repository[]; + total_count: number + incomplete_results: boolean + items: Repository[] } -export type { RepositoryQuery }; +export type { RepositoryQuery } diff --git a/src/types/repository-release.ts b/src/types/repository-release.ts index 532d898..2136cc1 100644 --- a/src/types/repository-release.ts +++ b/src/types/repository-release.ts @@ -1,27 +1,27 @@ -import type { ReleaseAsset } from "./release-asset"; -import type { RepositoryUser } from "./repository-user"; +import type { ReleaseAsset } from "./release-asset" +import type { RepositoryUser } from "./repository-user" interface RepositoryRelease { - url: string; - assets_url: string; - upload_url: string; // templated: ...{?name,label} - html_url: string; - id: number; - author: RepositoryUser; - node_id: string; - tag_name: string; - target_commitish: string; - name: string | null; - draft: boolean; - immutable: boolean; - prerelease: boolean; - created_at: string; // ISO date - updated_at: string; // ISO date - published_at: string; // ISO date - assets: Array; - tarball_url: string; - zipball_url: string; - body: string | null; + url: string + assets_url: string + upload_url: string // templated: ...{?name,label} + html_url: string + id: number + author: RepositoryUser + node_id: string + tag_name: string + target_commitish: string + name: string | null + draft: boolean + immutable: boolean + prerelease: boolean + created_at: string // ISO date + updated_at: string // ISO date + published_at: string // ISO date + assets: Array + tarball_url: string + zipball_url: string + body: string | null } -export type { RepositoryRelease }; +export type { RepositoryRelease } diff --git a/src/types/repository-user.ts b/src/types/repository-user.ts index 5b1353a..e0fef13 100644 --- a/src/types/repository-user.ts +++ b/src/types/repository-user.ts @@ -1,23 +1,23 @@ interface RepositoryUser { - login: string; - id: number; - node_id: string; - avatar_url: string; - gravatar_id: string; - url: string; - html_url: string; - followers_url: string; - following_url: string; - gists_url: string; - starred_url: string; - subscriptions_url: string; - organizations_url: string; - repos_url: string; - events_url: string; - received_events_url: string; - type: "User" | "Organization" | string; - user_view_type: "public" | "private" | string; - site_admin: boolean; + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: "User" | "Organization" | string + user_view_type: "public" | "private" | string + site_admin: boolean } -export type { RepositoryUser }; +export type { RepositoryUser } diff --git a/src/types/repository.ts b/src/types/repository.ts index 4899a65..5461270 100644 --- a/src/types/repository.ts +++ b/src/types/repository.ts @@ -1,87 +1,87 @@ -import type { RepositoryLicense } from "./repository-license"; -import type { RepositoryUser } from "./repository-user"; +import type { RepositoryLicense } from "./repository-license" +import type { RepositoryUser } from "./repository-user" interface Repository { - id: number; - node_id: string; - name: string; - full_name: string; - private: boolean; - owner: RepositoryUser; - html_url: string; - description: string | null; - fork: boolean; - url: string; - forks_url: string; - keys_url: string; - collaborators_url: string; - teams_url: string; - hooks_url: string; - issue_events_url: string; - events_url: string; - assignees_url: string; - branches_url: string; - tags_url: string; - blobs_url: string; - git_tags_url: string; - git_refs_url: string; - trees_url: string; - statuses_url: string; - languages_url: string; - stargazers_url: string; - contributors_url: string; - subscribers_url: string; - subscription_url: string; - commits_url: string; - git_commits_url: string; - comments_url: string; - issue_comment_url: string; - contents_url: string; - compare_url: string; - merges_url: string; - archive_url: string; - downloads_url: string; - issues_url: string; - pulls_url: string; - milestones_url: string; - notifications_url: string; - labels_url: string; - releases_url: string; - deployments_url: string; - created_at: string; // ISO date - updated_at: string; // ISO date - pushed_at: string; // ISO date - git_url: string; - ssh_url: string; - clone_url: string; - svn_url: string; - homepage: string | null; - size: number; - stargazers_count: number; - watchers_count: number; - language: string | null; - has_issues: boolean; - has_projects: boolean; - has_downloads: boolean; - has_wiki: boolean; - has_pages: boolean; - has_discussions: boolean; - forks_count: number; - mirror_url: string | null; - archived: boolean; - disabled: boolean; - open_issues_count: number; - license: RepositoryLicense | null; - allow_forking: boolean; - is_template: boolean; - web_commit_signoff_required: boolean; - topics: string[]; - visibility: "public" | "private" | string; - forks: number; - open_issues: number; - watchers: number; - default_branch: string; - score?: number; + id: number + node_id: string + name: string + full_name: string + private: boolean + owner: RepositoryUser + html_url: string + description: string | null + fork: boolean + url: string + forks_url: string + keys_url: string + collaborators_url: string + teams_url: string + hooks_url: string + issue_events_url: string + events_url: string + assignees_url: string + branches_url: string + tags_url: string + blobs_url: string + git_tags_url: string + git_refs_url: string + trees_url: string + statuses_url: string + languages_url: string + stargazers_url: string + contributors_url: string + subscribers_url: string + subscription_url: string + commits_url: string + git_commits_url: string + comments_url: string + issue_comment_url: string + contents_url: string + compare_url: string + merges_url: string + archive_url: string + downloads_url: string + issues_url: string + pulls_url: string + milestones_url: string + notifications_url: string + labels_url: string + releases_url: string + deployments_url: string + created_at: string // ISO date + updated_at: string // ISO date + pushed_at: string // ISO date + git_url: string + ssh_url: string + clone_url: string + svn_url: string + homepage: string | null + size: number + stargazers_count: number + watchers_count: number + language: string | null + has_issues: boolean + has_projects: boolean + has_downloads: boolean + has_wiki: boolean + has_pages: boolean + has_discussions: boolean + forks_count: number + mirror_url: string | null + archived: boolean + disabled: boolean + open_issues_count: number + license: RepositoryLicense | null + allow_forking: boolean + is_template: boolean + web_commit_signoff_required: boolean + topics: string[] + visibility: "public" | "private" | string + forks: number + open_issues: number + watchers: number + default_branch: string + score?: number } -export type { Repository }; +export type { Repository } diff --git a/src/types/stored-plugin.ts b/src/types/stored-plugin.ts index f985419..ab00e5f 100644 --- a/src/types/stored-plugin.ts +++ b/src/types/stored-plugin.ts @@ -1,12 +1,12 @@ -import type { PluginContributor } from "./plugin"; +import type { PluginContributor } from "./plugin" interface StoredPlugin { - id: number; - name: string; - owner: PluginContributor; - url: string; - branch: string; - approved: boolean; + id: number + name: string + owner: PluginContributor + url: string + branch: string + approved: boolean } -export type { StoredPlugin }; +export type { StoredPlugin } diff --git a/tsconfig.json b/tsconfig.json index a4ca148..a309f5a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,35 +1,29 @@ { - "compileOnSave": true, - "compilerOptions": { - // Environment and module settings. - "lib": ["ESNext"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "moduleResolution": "Bundler", + "compileOnSave": true, + "compilerOptions": { + // Environment and module settings. + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "Bundler", - // Language and structural settings. - "allowJs": false, - "checkJs": false, - "strict": true, - // This applies to distribution builds. - "removeComments": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noUnusedLocals": true, - "noImplicitAny": true, - "rootDir": "./src", - // This only applies to building the project. - "outDir": "./dist" - }, - "include": [ - "src", - ], - "exclude": [ - "node_modules/", - "dist/", - "build/", - ] + // Language and structural settings. + "allowJs": false, + "checkJs": false, + "strict": true, + // This applies to distribution builds. + "removeComments": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noImplicitAny": true, + "rootDir": "./src", + // This only applies to building the project. + "outDir": "./dist" + }, + "include": ["src"], + "exclude": ["node_modules/", "dist/", "build/"] } From 2b43a5f1738446ef07ca82b795f67631739f5a58 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:55:36 +0200 Subject: [PATCH 09/35] feat(funding): added funding information --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..90358dc --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: SerenityJS From 5db57ff2776d3ee974c66c8d2b2b26c63c69f41e Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:58:19 +0200 Subject: [PATCH 10/35] chore(docker): Dockerised app --- Dockerfile | 34 ++++++++++++++++++++++++++++++++++ docker-compose.yaml | 16 ++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..740dc02 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM oven/bun:canary AS builder + +WORKDIR /app +COPY package.json bun.lock ./ + +RUN bun install --frozen-lockfile +COPY . . + +RUN bun run build + +FROM oven/bun:canary-slim AS runner + +LABEL org.opencontainers.image.title="SerenityJS API Service" \ + org.opencontainers.image.description="Rest API Service for SerenityJS." \ + org.opencontainers.image.version="1.0.0" \ + org.opencontainers.image.authors="SerenityJS " \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.url="https://github.com/SerenityJS/api-service" \ + org.opencontainers.image.documentation="https://github.com/SerenityJS/api-service" \ + org.opencontainers.image.source="https://github.com/SerenityJS/api-service" + +WORKDIR /app +ENV NODE_ENV=production +ARG APP_PORT=${APP_PORT:-3000} + +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/public ./public +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/bun.lock ./bun.lock + +EXPOSE ${APP_PORT} +CMD ["bun", "run", "start"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..0196f6c --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,16 @@ +networks: + serenityjs-network: + driver: bridge + +services: + backend-app: + container_name: serenityjs-api-service + build: + dockerfile: Dockerfile + context: . + env_file: + - .env + networks: + - serenityjs-network + ports: + - "{APP_PORT:-4000}:{APP_PORT:-4000}" From ffb83eca73fa35b11f0b7706faf53e6b961cb45c Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 13:59:12 +0200 Subject: [PATCH 11/35] feat(hadolint): added configuration for Dockerfile's linter (Hadolint) --- .hadolint.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .hadolint.yaml diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..e3f411b --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,17 @@ +# It enforces the use of labels in every single Dockerfile. +strict-labels: true +# Labels that must be provided in every single Dockerfile. +label-schema: + org.opencontainers.image.title: text + org.opencontainers.image.description: text + org.opencontainers.image.version: semver + org.opencontainers.image.authors: text + org.opencontainers.image.licenses: spdx + org.opencontainers.image.url: url + org.opencontainers.image.documentation: url + org.opencontainers.image.source: url +# Overrides the default severity of the rules. +override: + info: + # This error is a warning related to pinning the version of a package. + - DL3008 From b218802302f3354387ba8a0f808629a4d18ddf02 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Thu, 16 Apr 2026 14:00:14 +0200 Subject: [PATCH 12/35] feat(dependabot): added Dependabot to repository --- .github/FUNDING.yml | 1 + .github/dependabot.yaml | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .github/dependabot.yaml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 90358dc..ea7e25a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +--- github: SerenityJS diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..bf5a8cf --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,34 @@ +--- +version: 2 +updates: + - package-ecosystem: "bun" + directory: "/" + target-branch: "development" + schedule: + # Libraries do not have to be updated as often as the code itself. + timezone: "Europe/Warsaw" + interval: "semiannually" + commit-message: + prefix: "chore(deps->root):" + # It disables all labels except the dedicated "dependencies" label. + labels: ["🔄 dependencies"] + + - package-ecosystem: "github-actions" + directory: "./github/workflows" + target-branch: "development" + schedule: + timezone: "Europe/Warsaw" + interval: "semiannually" + commit-message: + prefix: "chore(actions->github):" + labels: ["🔄 dependencies"] + + - package-ecosystem: "docker" + directory: "/" + target-branch: "development" + schedule: + timezone: "Europe/Warsaw" + interval: "semiannually" + commit-message: + prefix: "chore(backend->docker):" + labels: ["🔄 dependencies"] From 270a9027950754b17c75e8a8794d67017ac521b3 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:27:35 +0200 Subject: [PATCH 13/35] feat(services->docker): added PostgreSQL to Docker's services --- .env.example | 13 +++++++++++++ docker-compose.postgres.yaml | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .env.example create mode 100644 docker-compose.postgres.yaml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8dfed68 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Options related to the database engine. +# URL end-point of the database engine. +# Format: postgresql://:@:/ +DATABASE_URL=postgresql://serenityjs:serenityjs@localhost:5432/serenityjs +# Variables below are used by the database engine in Docker compose file. +# User of the database engine. +DATABASE_ENGINE_USER=serenityjs +# Password of the database engine. +DATABASE_ENGINE_PASSWORD=serenityjs +# Name of the database engine. +DATABASE_ENGINE_NAME=serenityjs +# Port of the database engine. +DATABASE_ENGINE_PORT=5432 \ No newline at end of file diff --git a/docker-compose.postgres.yaml b/docker-compose.postgres.yaml new file mode 100644 index 0000000..f8838bf --- /dev/null +++ b/docker-compose.postgres.yaml @@ -0,0 +1,37 @@ +volumes: + serenityjs-postgres-volume: + +services: + backend-app: + depends_on: + postgres: + condition: service_healthy + + postgres: + container_name: serenityjs-postgres + image: postgres:18.1-alpine + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_USER: ${DATABASE_ENGINE_USER:-serenityjs} + POSTGRES_PASSWORD: ${DATABASE_ENGINE_PASSWORD:-serenityjs} + POSTGRES_DB: ${DATABASE_ENGINE_NAME:-serenityjs} + ports: + - "${DATABASE_ENGINE_PORT:-5432}:5432" + healthcheck: + test: + [ + "CMD", + "pg_isready", + "-U", + "${DATABASE_ENGINE_USER:-serenityjs}", + ] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + volumes: + - serenityjs-postgres-volume:/var/lib/postgresql/data + networks: + - serenityjs-network From 271dcf3fd481e28c4548818a74d00c295a4494b4 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:28:21 +0200 Subject: [PATCH 14/35] feat(services->redis): added Redis to Docker's services --- .env.example | 13 ++++++++++++- docker-compose.redis.yaml | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 docker-compose.redis.yaml diff --git a/.env.example b/.env.example index 8dfed68..ebab8d5 100644 --- a/.env.example +++ b/.env.example @@ -10,4 +10,15 @@ DATABASE_ENGINE_PASSWORD=serenityjs # Name of the database engine. DATABASE_ENGINE_NAME=serenityjs # Port of the database engine. -DATABASE_ENGINE_PORT=5432 \ No newline at end of file +DATABASE_ENGINE_PORT=5432 + +# Options related to the cache engine. +# Whether the cache engine is enabled. +CACHE_ENGINE_ENABLED=true +# This end-point is only required if the cache engine is enabled. +# URL end-point of the cache engine. +# Format: redis://: +CACHE_ENGINE_URL=redis://localhost:6379 +# Variables below are used by the cache engine in Docker compose file. +# Port of the cache engine. +CACHE_ENGINE_PORT=6379 diff --git a/docker-compose.redis.yaml b/docker-compose.redis.yaml new file mode 100644 index 0000000..a237980 --- /dev/null +++ b/docker-compose.redis.yaml @@ -0,0 +1,27 @@ +volumes: + serenityjs-redis-volume: + +services: + backend-app: + depends_on: + redis: + condition: service_healthy + + redis: + container_name: serenityjs-redis + image: redis:8.6-alpine + restart: unless-stopped + env_file: + - .env + ports: + - "${CACHE_ENGINE_PORT:-6379}:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + volumes: + - serenityjs-redis-volume:/data + networks: + - serenityjs-network From 2e7a79b16688a2f53a882dfecddb7f102933c692 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:31:41 +0200 Subject: [PATCH 15/35] feat(ci): added CI workflow --- .github/workflows/CI.yaml | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/CI.yaml diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml new file mode 100644 index 0000000..17a4fd1 --- /dev/null +++ b/.github/workflows/CI.yaml @@ -0,0 +1,51 @@ +name: Validate REST API service's codebase. + +on: + push: + pull_request: + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + validate-code: + name: Validate codebase. + runs-on: ubuntu-latest + env: + NODE_ENV: CI + + steps: + - name: Checkout repository. + uses: actions/checkout@v6 + + - name: Set up Bun. + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies. + run: bun install --frozen-lockfile + + - name: Lint codebase using ESLint. + run: bun run lint + + - name: Lint Dockerfile using Hadolint. + uses: hadolint/hadolint-action@v3.3.0 + with: + dockerfile: Dockerfile + + - name: Check types using TypeScript. + run: bunx tsc --noEmit + + - name: Check formatting of all files using Prettier. + run: bun run format:Check + + - name: Run tests. + run: bun run test + + - name: Build a production-ready application. + run: bun run build From 19d4d954ce9757f87c4d704be11a42cd057992e5 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:32:54 +0200 Subject: [PATCH 16/35] feat(tests): added an example test --- tests/ExampleTest.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/ExampleTest.test.ts diff --git a/tests/ExampleTest.test.ts b/tests/ExampleTest.test.ts new file mode 100644 index 0000000..511bab1 --- /dev/null +++ b/tests/ExampleTest.test.ts @@ -0,0 +1,9 @@ +import { test, expect } from "bun:test" + +/** + * @summary Example test. + * @description Delete this entire file when you start writing your own tests. + */ +test("Example test.", (): void => { + expect(true).toBe(true) +}) From f1c899ad3a99e8ccdbbc8276c7d0bb01a7d3176e Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:34:42 +0200 Subject: [PATCH 17/35] feat(src): clean up --- .gitignore | 3 + src/index.ts | 200 ----------------- src/plugin.ts | 333 ---------------------------- src/routes/index.ts | 6 - src/routes/plugin.ts | 29 --- src/routes/plugins.ts | 20 -- src/routes/route.ts | 24 -- src/services/discord.ts | 136 ------------ src/services/gh-fetch.ts | 116 ---------- src/services/index.ts | 8 - src/services/service.ts | 23 -- src/types/index.ts | 9 - src/types/plugin.ts | 33 --- src/types/release-asset.ts | 19 -- src/types/repository-commit.ts | 46 ---- src/types/repository-contributor.ts | 7 - src/types/repository-license.ts | 9 - src/types/repository-query.ts | 9 - src/types/repository-release.ts | 27 --- src/types/repository-user.ts | 23 -- src/types/repository.ts | 87 -------- src/types/stored-plugin.ts | 12 - tsconfig.json | 4 +- 23 files changed, 5 insertions(+), 1178 deletions(-) delete mode 100644 src/index.ts delete mode 100644 src/plugin.ts delete mode 100644 src/routes/index.ts delete mode 100644 src/routes/plugin.ts delete mode 100644 src/routes/plugins.ts delete mode 100644 src/routes/route.ts delete mode 100644 src/services/discord.ts delete mode 100644 src/services/gh-fetch.ts delete mode 100644 src/services/index.ts delete mode 100644 src/services/service.ts delete mode 100644 src/types/index.ts delete mode 100644 src/types/plugin.ts delete mode 100644 src/types/release-asset.ts delete mode 100644 src/types/repository-commit.ts delete mode 100644 src/types/repository-contributor.ts delete mode 100644 src/types/repository-license.ts delete mode 100644 src/types/repository-query.ts delete mode 100644 src/types/repository-release.ts delete mode 100644 src/types/repository-user.ts delete mode 100644 src/types/repository.ts delete mode 100644 src/types/stored-plugin.ts diff --git a/.gitignore b/.gitignore index c2c988e..da68b8d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ # Operating system files. /**/.DS_Store /**/*.ini + +# I'll delete it later. +/old_src/ diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 2799ea0..0000000 --- a/src/index.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { Database } from "bun:sqlite" -import { Server } from "node:http" - -import express, { type Express } from "express" -import cors from "cors" - -import { Routes } from "./routes" -import { Services, type Service } from "./services" -import type { StoredPlugin } from "./types" -import type { Plugin } from "./plugin" - -class RestAPIService { - /** - * The Express application instance - */ - private readonly express: Express = express() - - /** - * The HTTP server instance - */ - private readonly server: Server = this.express.listen(4000) - - /** - * The map of service instances - */ - private readonly services = new Map() - - /** - * The SQLite database instance - */ - private readonly db: Database = new Database("plugins.db") - - /** - * The in-memory cache of plugins - */ - private readonly plugins = new Map() - - public constructor() { - // Enable JSON body parsing middleware - this.express.use(express.json()) - this.express.use(cors()) - - // Iterate over each route and register it with the Express app - for (const route of Routes) { - // Switch on the HTTP method and register the route accordingly - switch (route.method as string) { - default: - case "GET": { - this.express.get(route.path, route.handle) - - continue - } - - case "POST": { - this.express.post(route.path, route.handle) - - continue - } - } - } - - // Iterate over each service and create an instance - for (const service of Services) { - // Create an instance of the service - const instance = new service(this) - - // Store the service instance in the map - this.services.set(instance.name, instance) - } - - // Prepare the database - this.prepareDatabase() - } - - public getService(name: string): T | null { - // Get the service instance from the map - const service = this.services.get(name) as T | undefined - - // Return the service instance or null if not found - return service ?? null - } - - private prepareDatabase(): void { - // Create a table to store aproved plugins - this.db - .prepare( - ` - CREATE TABLE IF NOT EXISTS plugins ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - approved BOOLEAN DEFAULT 0, - name TEXT NOT NULL, - owner TEXT NOT NULL, - url TEXT NOT NULL, - branch TEXT DEFAULT 'main' - );`, - ) - .run() - } - - public hasPlugin(id: number): boolean { - // Check if a plugin with the given id exists in the database - const row = this.db.prepare("SELECT id FROM plugins WHERE id = ?").get(id) - - // Return true if the plugin exists, false otherwise - return !!row - } - - public addStoredPlugin(plugin: StoredPlugin): void { - // Insert a new plugin into the database - this.db - .prepare("INSERT INTO plugins (id, name, owner, url, approved, branch) VALUES (?, ?, ?, ?, ?, ?)") - .run(plugin.id, plugin.name, JSON.stringify(plugin.owner), plugin.url, plugin.approved, plugin.branch) - } - - public getStoredPlugin(id: number): StoredPlugin | null { - // Get a plugin with the given id from the database - const row = this.db - .prepare< - { id: number; name: string; owner: string; url: string; branch: string; approved: number }, - number - >("SELECT * FROM plugins WHERE id = ?") - .get(id) - - // Return the plugin or null if not found - return row - ? { - id: row.id, - name: row.name, - owner: JSON.parse(row.owner), - url: row.url, - branch: row.branch, - approved: row.approved === 1, - } - : null - } - - public updateStoredPlugin(id: number, plugin: Partial): void { - // Prepare a field list and values for the update query - const fields = [] - const values = [] - - // Iterate over the plugin properties and prepare the fields and values - for (const [key, value] of Object.entries(plugin)) { - fields.push(`${key} = ?`) - - // Check if the value is an object and needs to be stringified - if (key === "owner" && typeof value === "object") { - values.push(JSON.stringify(value)) - continue - } else { - values.push(value as string | number | boolean) - } - } - - // Add the id to the values array for the WHERE clause - values.push(id) - - // Update the plugin in the database - this.db.prepare(`UPDATE plugins SET ${fields.join(", ")} WHERE id = ?`).run(...values) - } - - public addCachedPlugin(plugin: Plugin): void { - // Add a plugin to the in-memory cache - this.plugins.set(plugin.id, plugin) - } - - public clearPluginCache(): void { - // Clear the in-memory cache of plugins - this.plugins.clear() - } - - public isPluginApproved(id: number): boolean { - // Check if a plugin with the given id is approved in the database - const row = this.db.prepare<{ approved: number }, number>("SELECT approved FROM plugins WHERE id = ?").get(id) - - // Return true if the plugin is approved, false otherwise - return row?.approved === 1 - } - - public setPluginApproval(id: number, approved: boolean): void { - // Update the approval status of a plugin in the database - this.db.prepare("UPDATE plugins SET approved = ? WHERE id = ?").run(approved ? 1 : 0, id) - } - - public getPluginFromCache(id: number): Plugin | null { - // Get the plugin from the in-memory cache - const plugin = this.plugins.get(id) - - // Return the plugin or null if not found - return plugin ?? null - } - - public getAllPluginsFromCache(): Array { - // Return all plugins from the in-memory cache as an array - return Array.from(this.plugins.values()) - } -} - -export default new RestAPIService() -export { RestAPIService } diff --git a/src/plugin.ts b/src/plugin.ts deleted file mode 100644 index a7ff348..0000000 --- a/src/plugin.ts +++ /dev/null @@ -1,333 +0,0 @@ -import axios from "axios" - -import type { - PluginCommit, - PluginContributor, - PluginRelease, - Repository, - RepositoryContributor, - RepositoryRelease, - StoredPlugin, -} from "./types" -import type { RepositoryCommit } from "./types/repository-commit" - -class Plugin implements StoredPlugin { - /** - * The unique identifier of the plugin (GitHub repository ID) - */ - public readonly id: number - - /** - * The name of the plugin (GitHub repository name) - */ - public readonly name: string - - /** - * The owner of the plugin (GitHub repository owner) - */ - public readonly owner: PluginContributor - - /** - * The URL of the plugin (GitHub repository URL) - */ - public readonly url: string - - /** - * The default branch of the plugin repository (assumed to be "main") - */ - public readonly branch: string = "main" - - /** - * Whether the plugin has been approved for listing - */ - public readonly approved: boolean - - public description: string | null = null - - public version: string | null = null - - public stars: number | null = null - - public downloads: number | null = null - - public forks: number | null = null - - public issues: number | null = null - - public keywords: Array | null = null - - public logo: string | null = null - - public banner: string | null = null - - public published: string | null = null - - public updated: string | null = null - - public readme: string | null = null - - public gallery: Array = [] - - public contributors: Array = [] - - public commits: Array = [] - - public releases: Array = [] - - /** - * Creates an instance of the Plugin. - * @param data The stored plugin data. - */ - private constructor(data: StoredPlugin, repository: Repository) { - // Load the basic data from the stored plugin object - this.id = data.id - this.name = data.name - this.owner = data.owner - this.url = data.url - this.approved = data.approved - - // Load additional data from the repository object - this.description = repository.description - this.branch = repository.default_branch - this.stars = repository.stargazers_count - this.forks = repository.forks_count - this.issues = repository.open_issues_count - this.published = repository.created_at - this.updated = repository.updated_at - } - - public static async create(data: StoredPlugin, repository: Repository): Promise { - // Create a new Plugin instance - const plugin = new Plugin(data, repository) - - // Fetch the logo URL - plugin.logo = await this.getLogoURL(data) - - // Fetch the banner URL - plugin.banner = await this.getBannerURL(data) - - // Fetch the releases, commits & map the total downloads - plugin.releases = await this.getReleases(data) - plugin.commits = await this.getCommitHistory(data) - plugin.downloads = plugin.releases.reduce( - (acc, release) => acc + release.assets.reduce((a, asset) => a + asset.download_count, 0), - 0, - ) - - // Fetch the contributors - plugin.contributors = await this.getContributors(data) - - // Fetch the gallery images - plugin.gallery = await plugin.getGallery() - - // Fetch the readme - plugin.readme = await this.getReadme(data) - - // Fetch the package.json to get the version and keywords - const packageData = await this.getPackageJSON(data) - if (packageData) { - plugin.version = packageData.version - plugin.keywords = packageData.keywords - } - - // Return the plugin instance - return plugin - } - - public static async getReleases(plugin: StoredPlugin): Promise> { - try { - // Check if the plugin has any releases on GitHub - const response = await axios.get>( - `https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/releases`, - { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }, - ) - - // Return the releases if the request was successful - return response.data.map((release) => ({ - name: release.name || release.tag_name, - tag: release.tag_name, - url: release.html_url, - description: release.body ?? "", - prerelease: release.prerelease, - date: release.published_at, - assets: release.assets.map((asset) => ({ - name: asset.name, - size: asset.size, - download_url: asset.browser_download_url, - download_count: asset.download_count, - })), - })) - } catch { - // Return an empty array if the request failed - return [] - } - } - - public static async getContributors(plugin: StoredPlugin): Promise> { - try { - // Check if the plugin has any contributors on GitHub - const response = await axios.get>( - `https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/contributors`, - { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }, - ) - - // Return the contributors if the request was successful - return response.data.map((contributor) => ({ - username: contributor.login, - profile_url: contributor.html_url, - avatar_url: contributor.avatar_url, - contributions: contributor.contributions, - })) - } catch { - // Return an empty array if the request failed - return [] - } - } - - public static async getLogoURL(plugin: StoredPlugin): Promise { - // The logo URL is assumed to be at a standard location in the repository - const url = `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/public/logo.png` - - try { - // Check if the logo exists by making a HEAD request - const response = await axios.head(url) - - // Return the logo URL if it exists, otherwise return a default logo - return response.status === 200 ? url : "https://avatars.githubusercontent.com/u/92610726?s=88&v=4" - } catch { - return "https://avatars.githubusercontent.com/u/92610726?s=88&v=4" - } - } - - public static async getBannerURL(plugin: StoredPlugin): Promise { - // The banner URL is assumed to be at a standard location in the repository - const url = `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/public/banner.png` - - try { - // Check if the banner exists by making a HEAD request - const response = await axios.head(url) - - // Return the banner URL if it exists, otherwise return null - return response.status === 200 ? url : null - } catch { - return null - } - } - - public static async getReadme(plugin: StoredPlugin): Promise { - try { - // Get the readme file from the repository - const response = await axios.get( - `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/README.md`, - { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }, - ) - - // Return the readme content - return response.data - } catch { - // Return null if the request failed - return null - } - } - - public static async getPackageJSON( - plugin: StoredPlugin, - ): Promise<{ version: string; keywords: Array } | null> { - try { - // Get the package.json file from the repository - const response = await axios.get<{ version: string; keywords: Array }>( - `https://raw.githubusercontent.com/${plugin.owner.username}/${plugin.name}/${plugin.branch}/package.json`, - { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }, - ) - - // Return the package.json content - return response.data - } catch { - console.log("Failed to fetch package.json") - - // Return null if the request failed - return null - } - } - - public async getGallery(): Promise> { - // The gallery images are assumed to be in a standard location in the repository - // Assumed path ./public/gallery/*.png - const images = [] - - // Fetch all images in the gallery (up to 10 images) - for (let i = 1; i <= 10; i++) { - try { - // Construct the image URL - const url = `https://raw.githubusercontent.com/${this.owner.username}/${this.name}/${this.branch}/public/gallery/image${i}.png` - - // Check if the image exists by making a HEAD request - const response = await axios.head(url) - - // If the image exists, add it to the gallery - if (response.status === 200) { - images.push(url) - } else { - break - } - } catch { - break - } - } - - // Return the gallery images - return images - } - - public static async getRepository(plugin: StoredPlugin): Promise { - try { - // Get the repository details from GitHub - const response = await axios.get(`https://api.github.com/repositories/${plugin.id}`, { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - }) - - // Return the repository details - return response.data - } catch { - // Return null if the request failed - return null - } - } - - public static async getCommitHistory(plugin: StoredPlugin): Promise> { - try { - // Get the commit history from GitHub - const response = await axios.get>( - `https://api.github.com/repos/${plugin.owner.username}/${plugin.name}/commits`, - { - headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN ?? ""}` }, - params: { per_page: 100 }, - }, - ) - - console.log(response.data) - - // Return the commit history - return response.data.map((commit) => ({ - sha: commit.sha, - html_url: commit.html_url, - message: commit.commit.message, - date: commit.commit.author.date, - author: commit.author?.login || commit.commit.author.name, - })) - } catch { - // Return an empty array if the request failed - return [] - } - } -} - -export { Plugin } diff --git a/src/routes/index.ts b/src/routes/index.ts deleted file mode 100644 index 4d39413..0000000 --- a/src/routes/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { GetPluginRoute } from "./plugin" -import { GetPluginsRoute } from "./plugins" - -const Routes = [GetPluginRoute, GetPluginsRoute] - -export { Routes } diff --git a/src/routes/plugin.ts b/src/routes/plugin.ts deleted file mode 100644 index 0ad8038..0000000 --- a/src/routes/plugin.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Request, Response } from "express" -import { Route } from "./route" - -import RestAPIService from "../index" - -class GetPluginRoute extends Route { - public static override readonly path: string = "/plugin/{:id}" - - public static override readonly method = "GET" - - public static override async handle(req: Request, res: Response): Promise { - // Get the plugin ID from the request parameters - const id = Number(req.params.id) - - // Get the plugin from the cache - const plugin = RestAPIService.getPluginFromCache(id) - - // Check if the plugin was found - if (!plugin) { - // Send a 404 response if the plugin was not found - res.status(404).send({ message: `Plugin with ID ${id} not found` }) - } else { - // Send the plugin as the response - res.status(200).send(plugin) - } - } -} - -export { GetPluginRoute } diff --git a/src/routes/plugins.ts b/src/routes/plugins.ts deleted file mode 100644 index 09988f5..0000000 --- a/src/routes/plugins.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Request, Response } from "express" - -import { Route } from "./route" -import RestAPIService from "../index" - -class GetPluginsRoute extends Route { - public static override readonly path: string = "/plugins" - - public static override readonly method = "GET" - - public static override async handle(_req: Request, res: Response): Promise { - // Get all plugins from the cache - const plugins = RestAPIService.getAllPluginsFromCache() - - // Send the plugins as the response - res.status(200).send(plugins) - } -} - -export { GetPluginsRoute } diff --git a/src/routes/route.ts b/src/routes/route.ts deleted file mode 100644 index 6bc1bbe..0000000 --- a/src/routes/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Request, Response } from "express" - -abstract class Route { - /** - * The path for this route (e.g., "/plugins"). - */ - public static readonly path: string = "/" - - /** - * The HTTP method for this route (e.g., "GET", "POST"). - */ - public static readonly method: "GET" | "POST" | "PUT" | "DELETE" - - /** - * Handle an incoming HTTP request. - * @param req The incoming request object. - * @param res The outgoing response object. - */ - public static async handle(_req: Request, _res: Response): Promise { - _res.status(501).send("Route::handle - Not Implemented") - } -} - -export { Route } diff --git a/src/services/discord.ts b/src/services/discord.ts deleted file mode 100644 index 42b16f9..0000000 --- a/src/services/discord.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - Client, - EmbedBuilder, - Events, - GatewayIntentBits, - type GuildTextBasedChannel, - type Interaction, - type Snowflake, -} from "discord.js" - -import type { RestAPIService } from "../index" -import { Service } from "./service" -import type { StoredPlugin } from "../types" -import { Plugin } from "../plugin" - -class Discord extends Service { - private readonly pluginApprovalChannelId: Snowflake = "1411567293552529462" - - /** - * The Discord bot token for the client to login - */ - private readonly token: string = process.env.DISCORD_TOKEN ?? "" - - /** - * The Discord client instance - */ - private readonly client: Client = new Client({ intents: [GatewayIntentBits.Guilds] }) - - /** - * A promise that resolves when the client is ready - */ - private readonly ready = new Promise((resolve) => this.client.once(Events.ClientReady, () => resolve())) - - /** - * Instructions for approving or rejecting a plugin - */ - private readonly approvalInstructions: string = ` - **Verify that the plugin meets the following criteria:** - - - The plugin is relevant to SerenityJS and its ecosystem.\n - - The plugin is well-maintained and has at least a release on GitHub.\n - - The plugin has a proper README file and documentation.\n - - The plugin does not contain any malicious code or vulnerabilities.\n - - The plugin follows best practices for coding and design.\n - - Please review the plugin and **approve** or **reject** it by clicking one of the buttons below. - ` - - public constructor(api: RestAPIService) { - super(api) - - // Login to Discord with the bot token - this.client.login(this.token) - - // Bind the interaction handler to the client - this.client.on(Events.InteractionCreate, this.handleInteraction.bind(this)) - } - - public async sendPluginApprovalRequest(plugin: StoredPlugin): Promise { - // Wait until the client is ready - await this.ready - - // Fetch the channel by its ID - const channel = (await this.client.channels.fetch(this.pluginApprovalChannelId)) as GuildTextBasedChannel | null - - // Check if the channel exists and is text-based - if (!channel || !channel.isTextBased()) { - console.error(`Channel with ID ${this.pluginApprovalChannelId} not found or is not text-based.`) - return - } - - // Create the embed message - const embed = new EmbedBuilder() - .setTitle("New Plugin Approval Request") - .setDescription( - `A new plugin has been submitted for approval:\n\n**Name:** ${plugin.name}\n**Owner:** ${plugin.owner.username}\n**URL:** ${plugin.url}\n\n${this.approvalInstructions}`, - ) - .setColor(0x8560e9) - .setThumbnail(await Plugin.getLogoURL(plugin)) - - // Create the action row with approve and reject buttons - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId(`approve:${plugin.id}`).setLabel("Approve").setStyle(ButtonStyle.Success), - new ButtonBuilder().setCustomId(`reject:${plugin.id}`).setLabel("Reject").setStyle(ButtonStyle.Danger), - ) - - // Send the message to the channel - await channel.send({ embeds: [embed], components: [row] }) - } - - private async handleInteraction(interaction: Interaction): Promise { - // Check if the interaction is a button interaction - if (interaction.isButton()) { - // Get the action and plugin ID from the button custom ID - const [action, pluginId] = interaction.customId.split(":") - - // Check if the action is valid - if (action === "approve" || action === "reject") { - // Determine if the plugin is approved or rejected - const approved = action === "approve" - - // Update the plugin approval status in the database - this.api.setPluginApproval(Number(pluginId), approved) - - // Disable the buttons after the action is taken - interaction.message.edit({ components: [] }) - - // Reply to the interaction to acknowledge it - await interaction.reply({ content: `Plugin ${approved ? "approved" : "rejected"}.` }) - - // Get the stored plugin from the database - const stored = this.api.getStoredPlugin(Number(pluginId)) - - // If the plugin is approved and the stored plugin exists - if (approved && stored) { - // Get the repository data from GitHub - const repository = await Plugin.getRepository(stored) - - // If the repository data exists - if (!repository) return - - // Create a Plugin instance from the stored data and repository data - const plugin = await Plugin.create(stored, repository) - - // Add the plugin to the in-memory cache - this.api.addCachedPlugin(plugin) - } - } - } - } -} - -export { Discord } diff --git a/src/services/gh-fetch.ts b/src/services/gh-fetch.ts deleted file mode 100644 index 7060301..0000000 --- a/src/services/gh-fetch.ts +++ /dev/null @@ -1,116 +0,0 @@ -import axios from "axios" - -import type { RestAPIService } from "../index" - -import { Service } from "./service" -import type { RepositoryQuery, StoredPlugin } from "../types" -import type { Discord } from "./discord" -import { Plugin } from "../plugin" - -// This service will handle fetching plugins from GitHub - -class GHFetch extends Service { - /** - * The GitHub API endpoint for searching repositories - */ - private readonly endpoint = "https://api.github.com/search/repositories" - - /** - * The query to search for Serenity/JS plugins - */ - private readonly query = "topic:serenityjs-plugin" - - /** - * The time interval (in milliseconds) to refresh the plugin list from GitHub (default: 5 minutes) - */ - private refreshTime = 5 * 60 * 1000 - - /** - * The time interval (in milliseconds) to clear the plugin cache (default: 1 hour) - */ - private cacheClearTime = 60 * 60 * 1000 - - public constructor(api: RestAPIService) { - super(api) - - // Fetch the plugins immediately - this.fetchPlugins() - - // Set an interval to fetch the plugins periodically - setInterval(() => this.fetchPlugins(), this.refreshTime) - - // Set an interval to clear the plugin cache periodically - setInterval(() => { - this.api.clearPluginCache() - this.fetchPlugins() - }, this.cacheClearTime) - } - - public async fetchPlugins(): Promise { - try { - // Get the list of repositories from the GitHub API - const response = await axios.get(this.endpoint, { - params: { - q: this.query, - }, - headers: { - Accept: "application/vnd.github.v3+json", - }, - }) - - // Iterate over the repositories and log their names - for (const repo of response.data.items) { - // Check if the plugin is already in the database - if (this.api.hasPlugin(repo.id)) { - // Verify if the plugin is approved or if the plugin id already exists in the cache - if (!this.api.isPluginApproved(repo.id) || this.api.getPluginFromCache(repo.id)) continue - - // Fetch the stored plugin data from the database - const stored = this.api.getStoredPlugin(repo.id) as StoredPlugin - - // Create a Plugin instance from the stored data and repository data - const plugin = await Plugin.create(stored, repo) - - this.api.addCachedPlugin(plugin) - - console.log(`Plugin: ${repo.name} (${repo.id}) by ${repo.owner.login} - ${repo.url}`) - } else { - // Create a StoredPlugin object - const storage: StoredPlugin = { - id: repo.id, - name: repo.name, - owner: { - username: repo.owner.login, - profile_url: repo.owner.html_url, - avatar_url: repo.owner.avatar_url, - contributions: 0, - }, - branch: repo.default_branch, - url: repo.html_url, - approved: false, - } - - // Check if the plugin has releases, if not, skip it - if ((await Plugin.getReleases(storage)).length === 0) { - // Log that the plugin has no releases - console.log(`Plugin: ${repo.name} by ${repo.owner.login} has no releases, skipping...`) - - // Continue to the next repository - continue - } - - // If not, add it to the database - this.api.addStoredPlugin(storage) - - // Get the Discord service instance - const discord = this.api.getService("Discord") - - // Send a message to the Discord channel for plugin approval - if (discord) discord.sendPluginApprovalRequest(storage) - } - } - } catch {} - } -} - -export { GHFetch } diff --git a/src/services/index.ts b/src/services/index.ts deleted file mode 100644 index 11aabd1..0000000 --- a/src/services/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Discord } from "./discord" -import { GHFetch } from "./gh-fetch" - -export * from "./service" - -const Services = [GHFetch, Discord] - -export { Services } diff --git a/src/services/service.ts b/src/services/service.ts deleted file mode 100644 index ce75adb..0000000 --- a/src/services/service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { RestAPIService } from "../index" - -abstract class Service { - /** - * The name of the service - */ - public readonly name: string = this.constructor.name - - /** - * The api service instance - */ - public readonly api: RestAPIService - - /** - * Creates an instance of the service. - * @param api The api service instance. - */ - public constructor(api: RestAPIService) { - this.api = api - } -} - -export { Service } diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 8d581a9..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from "./repository" -export * from "./release-asset" -export * from "./repository-user" -export * from "./repository-query" -export * from "./repository-license" -export * from "./repository-release" -export * from "./repository-contributor" -export * from "./stored-plugin" -export * from "./plugin" diff --git a/src/types/plugin.ts b/src/types/plugin.ts deleted file mode 100644 index afdae2a..0000000 --- a/src/types/plugin.ts +++ /dev/null @@ -1,33 +0,0 @@ -interface PluginRelease { - name: string - tag: string - url: string - description: string - prerelease: boolean - date: string - assets: Array -} - -interface PluginReleaseAsset { - name: string - size: number - download_url: string - download_count: number -} - -interface PluginContributor { - username: string - profile_url: string - avatar_url: string - contributions: number -} - -interface PluginCommit { - sha: string - html_url: string - message: string - date: string - author: string -} - -export type { PluginRelease, PluginReleaseAsset, PluginContributor, PluginCommit } diff --git a/src/types/release-asset.ts b/src/types/release-asset.ts deleted file mode 100644 index 621d05e..0000000 --- a/src/types/release-asset.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { RepositoryUser } from "./repository-user" - -interface ReleaseAsset { - url: string - id: number - node_id: string - name: string - label: string | null - uploader: RepositoryUser - content_type: string - state: "uploaded" | "open" | "temporary" | string - size: number - download_count: number - created_at: string // ISO date - updated_at: string // ISO date - browser_download_url: string -} - -export type { ReleaseAsset } diff --git a/src/types/repository-commit.ts b/src/types/repository-commit.ts deleted file mode 100644 index ce77999..0000000 --- a/src/types/repository-commit.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { RepositoryUser } from "./repository-user" - -// GitHub "List commits" item → https://docs.github.com/rest/commits/commits#list-commits -export interface RepositoryCommit { - sha: string - node_id: string - commit: GitCommit // metadata from the commit object itself - url: string // API URL for this commit - html_url: string // Web URL for this commit - comments_url: string - author: RepositoryUser | null // GitHub account (may be null if email not linked) - committer: RepositoryUser | null // GitHub account (may be null) - parents: GitParent[] // parent commits -} - -export interface GitCommit { - author: GitCommitAuthor // name/email/date from the commit - committer: GitCommitAuthor // name/email/date from the commit - message: string - tree: { sha: string; url: string } - url: string - comment_count: number - verification: GitCommitVerification -} - -export interface GitCommitAuthor { - name: string - email: string - date: string // ISO date -} - -export interface GitCommitVerification { - verified: boolean - reason: string - signature: string | null - payload: string | null - // Some repos may include extra fields; keep optional: - verified_at?: string | null - verifier?: RepositoryUser | null -} - -export interface GitParent { - sha: string - url: string - html_url: string -} diff --git a/src/types/repository-contributor.ts b/src/types/repository-contributor.ts deleted file mode 100644 index 78079fa..0000000 --- a/src/types/repository-contributor.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { RepositoryUser } from "./repository-user" - -interface RepositoryContributor extends RepositoryUser { - contributions: number -} - -export type { RepositoryContributor } diff --git a/src/types/repository-license.ts b/src/types/repository-license.ts deleted file mode 100644 index 2be11ba..0000000 --- a/src/types/repository-license.ts +++ /dev/null @@ -1,9 +0,0 @@ -interface RepositoryLicense { - key: string - name: string - spdx_id: string | null - url: string | null - node_id: string -} - -export type { RepositoryLicense } diff --git a/src/types/repository-query.ts b/src/types/repository-query.ts deleted file mode 100644 index 5f8245a..0000000 --- a/src/types/repository-query.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Repository } from "./repository" - -interface RepositoryQuery { - total_count: number - incomplete_results: boolean - items: Repository[] -} - -export type { RepositoryQuery } diff --git a/src/types/repository-release.ts b/src/types/repository-release.ts deleted file mode 100644 index 2136cc1..0000000 --- a/src/types/repository-release.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ReleaseAsset } from "./release-asset" -import type { RepositoryUser } from "./repository-user" - -interface RepositoryRelease { - url: string - assets_url: string - upload_url: string // templated: ...{?name,label} - html_url: string - id: number - author: RepositoryUser - node_id: string - tag_name: string - target_commitish: string - name: string | null - draft: boolean - immutable: boolean - prerelease: boolean - created_at: string // ISO date - updated_at: string // ISO date - published_at: string // ISO date - assets: Array - tarball_url: string - zipball_url: string - body: string | null -} - -export type { RepositoryRelease } diff --git a/src/types/repository-user.ts b/src/types/repository-user.ts deleted file mode 100644 index e0fef13..0000000 --- a/src/types/repository-user.ts +++ /dev/null @@ -1,23 +0,0 @@ -interface RepositoryUser { - login: string - id: number - node_id: string - avatar_url: string - gravatar_id: string - url: string - html_url: string - followers_url: string - following_url: string - gists_url: string - starred_url: string - subscriptions_url: string - organizations_url: string - repos_url: string - events_url: string - received_events_url: string - type: "User" | "Organization" | string - user_view_type: "public" | "private" | string - site_admin: boolean -} - -export type { RepositoryUser } diff --git a/src/types/repository.ts b/src/types/repository.ts deleted file mode 100644 index 5461270..0000000 --- a/src/types/repository.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { RepositoryLicense } from "./repository-license" -import type { RepositoryUser } from "./repository-user" - -interface Repository { - id: number - node_id: string - name: string - full_name: string - private: boolean - owner: RepositoryUser - html_url: string - description: string | null - fork: boolean - url: string - forks_url: string - keys_url: string - collaborators_url: string - teams_url: string - hooks_url: string - issue_events_url: string - events_url: string - assignees_url: string - branches_url: string - tags_url: string - blobs_url: string - git_tags_url: string - git_refs_url: string - trees_url: string - statuses_url: string - languages_url: string - stargazers_url: string - contributors_url: string - subscribers_url: string - subscription_url: string - commits_url: string - git_commits_url: string - comments_url: string - issue_comment_url: string - contents_url: string - compare_url: string - merges_url: string - archive_url: string - downloads_url: string - issues_url: string - pulls_url: string - milestones_url: string - notifications_url: string - labels_url: string - releases_url: string - deployments_url: string - created_at: string // ISO date - updated_at: string // ISO date - pushed_at: string // ISO date - git_url: string - ssh_url: string - clone_url: string - svn_url: string - homepage: string | null - size: number - stargazers_count: number - watchers_count: number - language: string | null - has_issues: boolean - has_projects: boolean - has_downloads: boolean - has_wiki: boolean - has_pages: boolean - has_discussions: boolean - forks_count: number - mirror_url: string | null - archived: boolean - disabled: boolean - open_issues_count: number - license: RepositoryLicense | null - allow_forking: boolean - is_template: boolean - web_commit_signoff_required: boolean - topics: string[] - visibility: "public" | "private" | string - forks: number - open_issues: number - watchers: number - default_branch: string - score?: number -} - -export type { Repository } diff --git a/src/types/stored-plugin.ts b/src/types/stored-plugin.ts deleted file mode 100644 index ab00e5f..0000000 --- a/src/types/stored-plugin.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { PluginContributor } from "./plugin" - -interface StoredPlugin { - id: number - name: string - owner: PluginContributor - url: string - branch: string - approved: boolean -} - -export type { StoredPlugin } diff --git a/tsconfig.json b/tsconfig.json index a309f5a..be618b6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,10 +20,10 @@ "noImplicitReturns": true, "noUnusedLocals": true, "noImplicitAny": true, - "rootDir": "./src", + "rootDir": "old_src", // This only applies to building the project. "outDir": "./dist" }, - "include": ["src"], + "include": ["old_src"], "exclude": ["node_modules/", "dist/", "build/"] } From 04b918c5d10b617175d47f935fa36a2bfb454659 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:42:04 +0200 Subject: [PATCH 18/35] feat(app): added sample app entrypoint --- .env.example | 4 ++ bun.lock | 28 +++++++---- package.json | 14 +++--- src/Main.ts | 33 +++++++++++++ src/globals/EnvironmentVariables.ts | 26 +++++++++++ .../managers/ApplicationInstanceManager.ts | 46 +++++++++++++++++++ 6 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 src/Main.ts create mode 100644 src/globals/EnvironmentVariables.ts create mode 100644 src/globals/managers/ApplicationInstanceManager.ts diff --git a/.env.example b/.env.example index ebab8d5..5102a12 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,7 @@ +# Options related to the application. +# Port of the application. +APP_PORT=3000 + # Options related to the database engine. # URL end-point of the database engine. # Format: postgresql://:@:/ diff --git a/bun.lock b/bun.lock index 51707f4..4452dc2 100644 --- a/bun.lock +++ b/bun.lock @@ -6,11 +6,11 @@ "name": "api-service", "dependencies": { "@types/cors": "^2.8.19", - "@types/express": "^5.0.3", + "@types/express": "^5.0.6", "axios": "^1.11.0", - "cors": "^2.8.5", + "cors": "^2.8.6", "discord.js": "^14.22.1", - "express": "^5.1.0", + "express": "^5.2.1", }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -93,7 +93,7 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="], @@ -121,7 +121,7 @@ "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], - "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -169,7 +169,7 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], @@ -203,7 +203,7 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -269,7 +269,7 @@ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -327,7 +327,7 @@ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -525,7 +525,7 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], @@ -641,8 +641,14 @@ "@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "body-parser/qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + "eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], + "express/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], @@ -657,6 +663,8 @@ "micromark/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], diff --git a/package.json b/package.json index 586fe85..4e02632 100644 --- a/package.json +++ b/package.json @@ -19,16 +19,16 @@ "engines": { "bun": ">=1.3" }, - "main": "dist/index.js", + "main": "dist/Main.js", "type": "module", "scripts": { "format": "bunx prettier --write .", "format:check": "bunx prettier --check .", "lint": "bunx eslint .", "lint:fix": "bunx eslint --fix .", - "build": "bun build src/index.ts --outdir dist --target bun", - "start": "bun run dist/index.js", - "dev": "bun run --watch src/index.ts", + "build": "bun build src/Main.ts --outdir dist --target bun", + "start": "bun run dist/Main.js", + "dev": "bun run --watch src/Main.ts", "prepare": "husky" }, "lint-staged": { @@ -58,10 +58,10 @@ }, "dependencies": { "@types/cors": "^2.8.19", - "@types/express": "^5.0.3", + "@types/express": "^5.0.6", "axios": "^1.11.0", - "cors": "^2.8.5", + "cors": "^2.8.6", "discord.js": "^14.22.1", - "express": "^5.1.0" + "express": "^5.2.1" } } diff --git a/src/Main.ts b/src/Main.ts new file mode 100644 index 0000000..2ccd09a --- /dev/null +++ b/src/Main.ts @@ -0,0 +1,33 @@ +import { ApplicationInstanceManager } from "./globals/managers/ApplicationInstanceManager" +import { EnvironmentVariables } from "./globals/EnvironmentVariables" + +/** + * @summary Main class. + * @description Main class for the application. + */ +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +class Main { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * @summary Static initializer. + * @description Static initializer that initializes the application. + */ + static { + void this.init() + } + + /** + * @summary Initialize the application. + * @description Initialize the application. + */ + private static async init(): Promise { + ApplicationInstanceManager.instance.listen(EnvironmentVariables.APP_PORT, () => { + console.log(`Server is running on port ${EnvironmentVariables.APP_PORT}.`) + }) + } +} diff --git a/src/globals/EnvironmentVariables.ts b/src/globals/EnvironmentVariables.ts new file mode 100644 index 0000000..b618903 --- /dev/null +++ b/src/globals/EnvironmentVariables.ts @@ -0,0 +1,26 @@ +/** + * @summary Manager for the environment variables. + * @description Manager for the environment variables. + */ +class EnvironmentVariables { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * @summary Get the port of the application. + * @description Get the port of the application. + * @returns The port of the application. + */ + public static get APP_PORT(): number { + const value: string | undefined = process.env.APP_PORT + if (value === undefined) throw new Error("APP_PORT is not set.") + const number: number = Number.parseInt(value, 10) + if (Number.isNaN(number)) throw new Error("APP_PORT is not a number.") + return number + } +} + +export { EnvironmentVariables } diff --git a/src/globals/managers/ApplicationInstanceManager.ts b/src/globals/managers/ApplicationInstanceManager.ts new file mode 100644 index 0000000..5e21314 --- /dev/null +++ b/src/globals/managers/ApplicationInstanceManager.ts @@ -0,0 +1,46 @@ +import cors from "cors" +import express from "express" + +/** + * @summary Manager for the application instance. + * @description Manager for the application instance. + */ +class ApplicationInstanceManager { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * The internal instance of the application. + * @private + */ + private static internalInstance: express.Express | null = null + + /** + * @summary Get the instance of the application. + * @description Get the instance of the application. + * @returns The instance of the application. + */ + public static get instance(): express.Express { + if (this.internalInstance === null) { + this.internalInstance = express() + this.internalInstance.use(express.json(), cors()) + this.loadRoutes() + } + + return this.internalInstance + } + + /** + * @summary Load the routes of the application. + * @description Load the routes of the application. + * @remarks Every single router you add to the application must be loaded here. + */ + private static loadRoutes(): void { + if (this.internalInstance === null) return + } +} + +export { ApplicationInstanceManager } From 489d8fb755960a24e488b6b586fb22582eb7fe84 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:42:48 +0200 Subject: [PATCH 19/35] fix(tsconfig): adjusted paths --- tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index be618b6..a309f5a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,10 +20,10 @@ "noImplicitReturns": true, "noUnusedLocals": true, "noImplicitAny": true, - "rootDir": "old_src", + "rootDir": "./src", // This only applies to building the project. "outDir": "./dist" }, - "include": ["old_src"], + "include": ["src"], "exclude": ["node_modules/", "dist/", "build/"] } From cd988c189fde24b33fe8d4aa0f5ae25a734cc606 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:45:48 +0200 Subject: [PATCH 20/35] fix(tsconfig): removed an option that's covered already by ESLint --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index a309f5a..93e6284 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,6 @@ "skipLibCheck": true, "noImplicitOverride": true, "noImplicitReturns": true, - "noUnusedLocals": true, "noImplicitAny": true, "rootDir": "./src", // This only applies to building the project. From cf5fa3b68dfbc059b31516f59acc368bd75e3add Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:52:26 +0200 Subject: [PATCH 21/35] feat(clients->redis): added Redis client --- src/globals/EnvironmentVariables.ts | 20 ++++++ src/globals/clients/RedisClient.ts | 107 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/globals/clients/RedisClient.ts diff --git a/src/globals/EnvironmentVariables.ts b/src/globals/EnvironmentVariables.ts index b618903..becc83c 100644 --- a/src/globals/EnvironmentVariables.ts +++ b/src/globals/EnvironmentVariables.ts @@ -21,6 +21,26 @@ class EnvironmentVariables { if (Number.isNaN(number)) throw new Error("APP_PORT is not a number.") return number } + + public static get CACHE_ENGINE_ENABLED(): boolean { + const value: string | undefined = process.env.CACHE_ENGINE_ENABLED + if (value === undefined) throw new Error("CACHE_ENGINE_ENABLED is not set.") + switch (value.toLowerCase().trim()) { + case "true": + return true + case "false": + return false + default: + throw new Error("CACHE_ENGINE_ENABLED is not a boolean.") + } + } + + public static get CACHE_ENGINE_URL(): string | undefined { + if (!this.CACHE_ENGINE_ENABLED) return undefined + const value: string | undefined = process.env.CACHE_ENGINE_URL + if (value === undefined) throw new Error("CACHE_ENGINE_URL is not set.") + return value + } } export { EnvironmentVariables } diff --git a/src/globals/clients/RedisClient.ts b/src/globals/clients/RedisClient.ts new file mode 100644 index 0000000..ef7552f --- /dev/null +++ b/src/globals/clients/RedisClient.ts @@ -0,0 +1,107 @@ +import { RedisClient as NativeRedisClient } from "bun" +import { EnvironmentVariables } from "../EnvironmentVariables" + +/** + * @summary Client for the Redis cache engine. + * @description Client for the Redis cache engine. + */ +class RedisClient { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * The internal instance of the Redis client. + * @private + */ + private static internalInstance: NativeRedisClient | null = null + + /** + * @summary Get the instance of the Redis client. + * @description Get the instance of the Redis client. + * @returns The instance of the Redis client. + */ + public static get instance(): NativeRedisClient { + if (this.internalInstance === null) { + this.internalInstance = new NativeRedisClient(EnvironmentVariables.CACHE_ENGINE_URL!) + } + + return this.internalInstance + } + + /** + * @summary Get the value of a key. + * @description Get the value of a key. + * @param key The key to get the value of. + * @param defaultValue The default value to return if the key is not found. + * @returns The value of the key. + */ + public static async getValue(key: string, defaultValue: T): Promise { + if (!EnvironmentVariables.CACHE_ENGINE_ENABLED) return defaultValue + const value: string | null = await this.instance.get(key) + if (value === null) return defaultValue + return JSON.parse(value) as T + } + + /** + * @summary Get the keys of a pattern. + * @description Get the keys of a pattern. + * @param patterns The patterns to get the keys of. + * @returns The keys of the patterns. + */ + public static async getKeys(...patterns: readonly string[]): Promise { + if (!EnvironmentVariables.CACHE_ENGINE_ENABLED) return [] + switch (patterns.length) { + case 0: + return [] + case 1: + return await this.instance.keys(patterns[0]) + default: + return await Promise.all(patterns.map((pattern) => this.instance.keys(pattern))).then((keys) => + keys.flat(), + ) + } + } + + /** + * @summary Set the value of a key. + * @description Set the value of a key. + * @param key The key to set the value of. + * @param value The value to set. + * @param timeToLive The time to live in seconds. + */ + public static async setValue(key: string, value: unknown, timeToLive: number = 60): Promise { + if (!EnvironmentVariables.CACHE_ENGINE_ENABLED) return + await Promise.all([this.instance.set(key, JSON.stringify(value)), this.instance.expire(key, timeToLive)]) + } + + /** + * @summary Delete the values of a key. + * @description Delete the values of a key. + * @param keysOrPatterns The keys or patterns to delete. + */ + public static async deleteValues(...keysOrPatterns: readonly string[]): Promise { + if (!EnvironmentVariables.CACHE_ENGINE_ENABLED || keysOrPatterns.length === 0) return + const keysToDelete: string[] = [] + const patternsToDelete: string[] = [] + + for (const keyOrPattern of keysOrPatterns) { + if (keyOrPattern.includes("*")) { + patternsToDelete.push(keyOrPattern) + } else { + keysToDelete.push(keyOrPattern) + } + } + + await this.instance.del( + ...keysToDelete, + ...(await Promise.all(patternsToDelete.map((pattern) => this.instance.keys(pattern))).then((keys) => + keys.flat(), + )), + ) + } +} + +export { RedisClient } From 1a228d723d2b34a1f62bdfaf54c24ab2ac1b0bb0 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:54:30 +0200 Subject: [PATCH 22/35] feat(database->orm): added DrizzleORM --- .prettierignore | 3 + bun.lock | 180 +++++++++++++++++++++++ drizzle.config.ts | 29 ++++ package.json | 2 + src/globals/databases/DatabaseSchemas.ts | 1 + 5 files changed, 215 insertions(+) create mode 100644 drizzle.config.ts create mode 100644 src/globals/databases/DatabaseSchemas.ts diff --git a/.prettierignore b/.prettierignore index 0c739ac..e720b74 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,6 +18,9 @@ /build/ /**/*.tsbuildinfo +# Migration files. +/drizzle/ + # Environment variables. /**/*.env* !/.env.example diff --git a/bun.lock b/bun.lock index 4452dc2..5e69e6f 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "axios": "^1.11.0", "cors": "^2.8.6", "discord.js": "^14.22.1", + "drizzle-orm": "^0.45.2", "express": "^5.2.1", }, "devDependencies": { @@ -17,6 +18,7 @@ "@eslint/json": "^1.2.0", "@eslint/markdown": "^8.0.1", "@types/bun": "latest", + "drizzle-kit": "^0.31.10", "eslint": "^10.2.0", "globals": "^17.5.0", "husky": "^9.1.7", @@ -43,6 +45,64 @@ "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -173,6 +233,8 @@ "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -227,6 +289,10 @@ "discord.js": ["discord.js@14.22.1", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.16", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w=="], + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], + + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -245,6 +311,8 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -301,6 +369,8 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], @@ -309,6 +379,8 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -527,6 +599,8 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], @@ -561,6 +635,10 @@ "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], @@ -581,6 +659,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], @@ -629,6 +709,8 @@ "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -665,8 +747,106 @@ "raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], } } diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..9870cc3 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "drizzle-kit" + +/** + * @summary Get the database URL. + * @description Get the database URL. + * @returns The database URL. + */ +const DATABASE_URL = (): string => { + const rawValue: string | undefined = process.env.DATABASE_URL + + if (!rawValue) { + throw new Error("DATABASE_URL is not set and Drizzle cannot handle this operation.") + } + + return rawValue +} + +/** + * @summary Drizzle configuration. + * @description Drizzle configuration. + */ +export default defineConfig({ + schema: "./src/globals/databases/DatabaseSchemas.ts", + dialect: "postgresql", + out: "./drizzle", + dbCredentials: { + url: DATABASE_URL(), + }, +}) diff --git a/package.json b/package.json index 4e02632..74d3f46 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@eslint/json": "^1.2.0", "@eslint/markdown": "^8.0.1", "@types/bun": "latest", + "drizzle-kit": "^0.31.10", "eslint": "^10.2.0", "globals": "^17.5.0", "husky": "^9.1.7", @@ -62,6 +63,7 @@ "axios": "^1.11.0", "cors": "^2.8.6", "discord.js": "^14.22.1", + "drizzle-orm": "^0.45.2", "express": "^5.2.1" } } diff --git a/src/globals/databases/DatabaseSchemas.ts b/src/globals/databases/DatabaseSchemas.ts new file mode 100644 index 0000000..336ce12 --- /dev/null +++ b/src/globals/databases/DatabaseSchemas.ts @@ -0,0 +1 @@ +export {} From 66daea46aa22bb721eef5878b1ac2ec0e0d28f10 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 09:55:33 +0200 Subject: [PATCH 23/35] feat(clients->db): added client for database connection --- src/globals/EnvironmentVariables.ts | 21 ++++++++ src/globals/clients/DatabaseClient.ts | 71 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/globals/clients/DatabaseClient.ts diff --git a/src/globals/EnvironmentVariables.ts b/src/globals/EnvironmentVariables.ts index becc83c..fd3a7fb 100644 --- a/src/globals/EnvironmentVariables.ts +++ b/src/globals/EnvironmentVariables.ts @@ -22,6 +22,22 @@ class EnvironmentVariables { return number } + /** + * @summary Get the URL of the database. + * @description Get the URL of the database. + * @returns The URL of the database. + */ + public static get DATABASE_URL(): string { + const value: string | undefined = process.env.DATABASE_URL + if (value === undefined) throw new Error("DATABASE_URL is not set.") + return value + } + + /** + * @summary Get the enabled status of the cache engine. + * @description Get the enabled status of the cache engine. + * @returns The enabled status of the cache engine. + */ public static get CACHE_ENGINE_ENABLED(): boolean { const value: string | undefined = process.env.CACHE_ENGINE_ENABLED if (value === undefined) throw new Error("CACHE_ENGINE_ENABLED is not set.") @@ -35,6 +51,11 @@ class EnvironmentVariables { } } + /** + * @summary Get the URL of the cache engine. + * @description Get the URL of the cache engine. + * @returns The URL of the cache engine. + */ public static get CACHE_ENGINE_URL(): string | undefined { if (!this.CACHE_ENGINE_ENABLED) return undefined const value: string | undefined = process.env.CACHE_ENGINE_URL diff --git a/src/globals/clients/DatabaseClient.ts b/src/globals/clients/DatabaseClient.ts new file mode 100644 index 0000000..129014a --- /dev/null +++ b/src/globals/clients/DatabaseClient.ts @@ -0,0 +1,71 @@ +import { BunSQLDatabase, drizzle } from "drizzle-orm/bun-sql" +import { SQL, sql } from "bun" +import { EnvironmentVariables } from "../EnvironmentVariables" +import * as databaseSchemas from "../databases/DatabaseSchemas" + +/** + * @summary Client of the database. + * @description Client of the database. + */ +class DatabaseClient { + /** + * @summary Private constructor. + * @description Private constructor to prevent instantiation of the class. + */ + private constructor() {} + + /** + * @summary Internal instance of the Drizzle. + */ + private static internalDrizzleInstance: BunSQLDatabase | null = null + + /** + * @summary Internal instance of the native database. + */ + private static internalNativeInstance: SQL | null = null + + /** + * @summary Get the Drizzle instance. + * @description Get the Drizzle instance. + * @returns The Drizzle instance. + */ + public static get drizzleInstance(): BunSQLDatabase { + if (this.internalDrizzleInstance === null) { + this.internalDrizzleInstance = drizzle(this.nativeInstance, { + schema: databaseSchemas, + logger: false, + }) + } + + return this.internalDrizzleInstance + } + + /** + * @summary Get the native instance. + * @description Get the native instance from BunSQL. + * @returns The native instance. + */ + public static get nativeInstance(): SQL { + if (this.internalNativeInstance === null) { + this.internalNativeInstance = new SQL(EnvironmentVariables.DATABASE_URL) + } + + return this.internalNativeInstance + } + + /** + * @summary Ping the database. + * @description Ping the database. + * @returns The status of the database. + */ + public static async ping(): Promise { + try { + await this.nativeInstance(sql`SELECT 1`) + return true + } catch { + return false + } + } +} + +export { DatabaseClient } From e6e391951e8b24d09a59ab34f63858afd58eebf3 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 10:02:16 +0200 Subject: [PATCH 24/35] feat(schema): added schema related to plugins --- src/globals/databases/DatabaseSchemas.ts | 5 ++++- src/globals/databases/plugins/PluginsSchemas.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/globals/databases/plugins/PluginsSchemas.ts diff --git a/src/globals/databases/DatabaseSchemas.ts b/src/globals/databases/DatabaseSchemas.ts index 336ce12..50dc3cd 100644 --- a/src/globals/databases/DatabaseSchemas.ts +++ b/src/globals/databases/DatabaseSchemas.ts @@ -1 +1,4 @@ -export {} +/** + * @sumamry Schemas related to plugins. + */ +export * from "./plugins/PluginsSchemas" diff --git a/src/globals/databases/plugins/PluginsSchemas.ts b/src/globals/databases/plugins/PluginsSchemas.ts new file mode 100644 index 0000000..8411eb5 --- /dev/null +++ b/src/globals/databases/plugins/PluginsSchemas.ts @@ -0,0 +1,16 @@ +import { boolean, pgTable, serial, varchar } from "drizzle-orm/pg-core" + +/** + * @summary Plugins table. + * @description Plugins table. + */ +const pluginsTable = pgTable("plguins", { + id: serial("id").notNull().primaryKey(), + approved: boolean("approved").notNull().default(false), + name: varchar("name", { length: 255 }).notNull(), + owner: varchar("owner", { length: 255 }).notNull(), + url: varchar("url", { length: 255 }).notNull(), + branch: varchar("branch", { length: 255 }).notNull().default("main"), +}) + +export { pluginsTable } From fb2ac150f5b43d929160513bcdab938c06758a21 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 10:11:19 +0200 Subject: [PATCH 25/35] feat(models): added base database service & plugins --- .../databases/types/NotFoundInDatabaseType.ts | 7 ++++ .../plugins/base/interfaces/IPlugin.ts | 38 +++++++++++++++++++ .../base/interfaces/IPluginCreatePayload.ts | 33 ++++++++++++++++ .../base/interfaces/IPluginUpdatePayload.ts | 33 ++++++++++++++++ src/services/base/BaseDatabaseService.ts | 21 ++++++++++ 5 files changed, 132 insertions(+) create mode 100644 src/models/services/base/databases/types/NotFoundInDatabaseType.ts create mode 100644 src/models/services/databases/plugins/base/interfaces/IPlugin.ts create mode 100644 src/models/services/databases/plugins/base/interfaces/IPluginCreatePayload.ts create mode 100644 src/models/services/databases/plugins/base/interfaces/IPluginUpdatePayload.ts create mode 100644 src/services/base/BaseDatabaseService.ts diff --git a/src/models/services/base/databases/types/NotFoundInDatabaseType.ts b/src/models/services/base/databases/types/NotFoundInDatabaseType.ts new file mode 100644 index 0000000..51ebad4 --- /dev/null +++ b/src/models/services/base/databases/types/NotFoundInDatabaseType.ts @@ -0,0 +1,7 @@ +/** + * @summary Type for not found in cache. + * @description It is a type that represents that the item was not found in the databsae, so it saves it to + */ +type NotFoundInDatabaseType = "__NOT_FOUND_IN_DATABASE__" + +export type { NotFoundInDatabaseType } diff --git a/src/models/services/databases/plugins/base/interfaces/IPlugin.ts b/src/models/services/databases/plugins/base/interfaces/IPlugin.ts new file mode 100644 index 0000000..c0effa9 --- /dev/null +++ b/src/models/services/databases/plugins/base/interfaces/IPlugin.ts @@ -0,0 +1,38 @@ +/** + * @summary Interface for a plugin. + * @description Interface for a plugin. + */ +interface IPlugin { + /** + * @summary The ID of the plugin. + * @description The ID of the plugin. + */ + readonly id: number + /** + * @summary Whether the plugin is approved. + * @description Whether the plugin is approved. + */ + readonly approved: boolean + /** + * @summary The name of the plugin. + * @description The name of the plugin. + */ + readonly name: string + /** + * @summary The owner of the plugin. + * @description The owner of the plugin. + */ + readonly owner: string + /** + * @summary The URL of the plugin. + * @description The URL of the plugin. + */ + readonly url: string + /** + * @summary The branch of the plugin. + * @description The branch of the plugin. + */ + readonly branch: string +} + +export type { IPlugin } diff --git a/src/models/services/databases/plugins/base/interfaces/IPluginCreatePayload.ts b/src/models/services/databases/plugins/base/interfaces/IPluginCreatePayload.ts new file mode 100644 index 0000000..b302066 --- /dev/null +++ b/src/models/services/databases/plugins/base/interfaces/IPluginCreatePayload.ts @@ -0,0 +1,33 @@ +/** + * @summary Interface for a plugin create payload. + * @description Interface for a plugin create payload. + */ +interface IPluginCreatePayload { + /** + * @summary Whether the plugin is approved. + * @description Whether the plugin is approved. + */ + readonly approved: boolean + /** + * @summary The name of the plugin. + * @description The name of the plugin. + */ + readonly name: string + /** + * @summary The owner of the plugin. + * @description The owner of the plugin. + */ + readonly owner: string + /** + * @summary The URL of the plugin. + * @description The URL of the plugin. + */ + readonly url: string + /** + * @summary The branch of the plugin. + * @description The branch of the plugin. + */ + readonly branch: string +} + +export type { IPluginCreatePayload } diff --git a/src/models/services/databases/plugins/base/interfaces/IPluginUpdatePayload.ts b/src/models/services/databases/plugins/base/interfaces/IPluginUpdatePayload.ts new file mode 100644 index 0000000..c5787dd --- /dev/null +++ b/src/models/services/databases/plugins/base/interfaces/IPluginUpdatePayload.ts @@ -0,0 +1,33 @@ +/** + * @summary Interface for a plugin update payload. + * @description Interface for a plugin update payload. + */ +interface IPluginUpdatePayload { + /** + * @summary Whether the plugin is approved. + * @description Whether the plugin is approved. + */ + readonly approved?: boolean + /** + * @summary The name of the plugin. + * @description The name of the plugin. + */ + readonly name?: string + /** + * @summary The owner of the plugin. + * @description The owner of the plugin. + */ + readonly owner?: string + /** + * @summary The URL of the plugin. + * @description The URL of the plugin. + */ + readonly url?: string + /** + * @summary The branch of the plugin. + * @description The branch of the plugin. + */ + readonly branch?: string +} + +export type { IPluginUpdatePayload } diff --git a/src/services/base/BaseDatabaseService.ts b/src/services/base/BaseDatabaseService.ts new file mode 100644 index 0000000..36033c3 --- /dev/null +++ b/src/services/base/BaseDatabaseService.ts @@ -0,0 +1,21 @@ +import type { NotFoundInDatabaseType } from "../../models/services/base/databases/types/NotFoundInDatabaseType" + +/** + * @summary Base class for database services. + * @description Base class for database services. + */ +abstract class BaseDatabaseService { + /** + * @summary Private constructor. + * @description Private constructor to prevent instantiation of the class. + */ + private constructor() {} + + /** + * @summary Not found in database type. + * @description It is a type that represents that the item was not found in the databsae, so it saves it to the database. + */ + protected static readonly NOT_FOUND_IN_DATABASE: NotFoundInDatabaseType = "__NOT_FOUND_IN_DATABASE__" as const +} + +export { BaseDatabaseService } From 7cbbc1c80ebb89b9a410e81e8dc2086326906a92 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 10:18:24 +0200 Subject: [PATCH 26/35] feat(services->db): implemented a service to manage plugins in a database --- src/globals/clients/RedisClient.ts | 2 +- src/services/base/BaseDatabaseService.ts | 19 ++- .../databases/plugins/PluginsService.ts | 156 ++++++++++++++++++ 3 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 src/services/databases/plugins/PluginsService.ts diff --git a/src/globals/clients/RedisClient.ts b/src/globals/clients/RedisClient.ts index ef7552f..8342aa3 100644 --- a/src/globals/clients/RedisClient.ts +++ b/src/globals/clients/RedisClient.ts @@ -38,7 +38,7 @@ class RedisClient { * @param defaultValue The default value to return if the key is not found. * @returns The value of the key. */ - public static async getValue(key: string, defaultValue: T): Promise { + public static async getValue(key: string, defaultValue: T | null = null): Promise { if (!EnvironmentVariables.CACHE_ENGINE_ENABLED) return defaultValue const value: string | null = await this.instance.get(key) if (value === null) return defaultValue diff --git a/src/services/base/BaseDatabaseService.ts b/src/services/base/BaseDatabaseService.ts index 36033c3..f1c1940 100644 --- a/src/services/base/BaseDatabaseService.ts +++ b/src/services/base/BaseDatabaseService.ts @@ -6,16 +6,29 @@ import type { NotFoundInDatabaseType } from "../../models/services/base/database */ abstract class BaseDatabaseService { /** - * @summary Private constructor. - * @description Private constructor to prevent instantiation of the class. + * @summary Protected constructor. + * @description Protected constructor to prevent instantiation of the class, while allowing inheritance. */ - private constructor() {} + protected constructor() {} /** * @summary Not found in database type. * @description It is a type that represents that the item was not found in the databsae, so it saves it to the database. */ protected static readonly NOT_FOUND_IN_DATABASE: NotFoundInDatabaseType = "__NOT_FOUND_IN_DATABASE__" as const + + /** + * @summary Default page size. + * @description Default page size. Do not modify this value, it's only in case when you edit PAGE_SIZE. + * @see `this.PAGE_SIZE` If you want to change the page size, you should edit this value. + */ + protected static readonly DEFAULT_PAGE_SIZE: number = 50 + + /** + * @summary Page size. + * @description A property used with pagination. It's set to the default page size by default. + */ + protected static readonly PAGE_SIZE: number = this.DEFAULT_PAGE_SIZE } export { BaseDatabaseService } diff --git a/src/services/databases/plugins/PluginsService.ts b/src/services/databases/plugins/PluginsService.ts new file mode 100644 index 0000000..75e519c --- /dev/null +++ b/src/services/databases/plugins/PluginsService.ts @@ -0,0 +1,156 @@ +import { count, eq } from "drizzle-orm" +import { DatabaseClient } from "../../../globals/clients/DatabaseClient" +import { RedisClient } from "../../../globals/clients/RedisClient" +import { BaseDatabaseService } from "../../base/BaseDatabaseService" +import { pluginsTable } from "../../../globals/databases/DatabaseSchemas" +import type { IPlugin } from "../../../models/services/databases/plugins/base/interfaces/IPlugin" +import type { NotFoundInDatabaseType } from "../../../models/services/base/databases/types/NotFoundInDatabaseType" +import type { IPluginCreatePayload } from "../../../models/services/databases/plugins/base/interfaces/IPluginCreatePayload" +import type { IPluginUpdatePayload } from "../../../models/services/databases/plugins/base/interfaces/IPluginUpdatePayload" + +/** + * @summary Service for plugins. + * @description Service for plugins. + */ +class PluginsService extends BaseDatabaseService { + /** + * @summary Private constructor. + * @description Private constructor to prevent instantiation of the class. + */ + private constructor() { + super() + } + + /** + * @summary Plugins count caching key. + * @description Plugins count caching key. + */ + private static readonly PLUGINS_COUNT_CACHING_KEY: string = "pluginsCount" + + /** + * @summary Get the number of plugins. + * @description Get the number of plugins. + * @returns The number of plugins. + */ + public static async getPluginsCount(): Promise { + const cachedValue: number | null = await RedisClient.getValue(this.PLUGINS_COUNT_CACHING_KEY) + if (cachedValue !== null) return cachedValue + + const queriedValue = await DatabaseClient.drizzleInstance + .select({ count: count() }) + .from(pluginsTable) + .execute() + + const finalResponse: number = queriedValue[0].count + await RedisClient.setValue(this.PLUGINS_COUNT_CACHING_KEY, finalResponse) + return finalResponse + } + + /** + * @summary Get the plugins by page. + * @description Get the plugins by page. + * @param page The page number. + * @returns The plugins by page. + */ + public static async getPluginsByPage(page: number): Promise { + const cachingKey: string = `pluginsByPage:${page}` + const cachedValue: IPlugin[] | null = await RedisClient.getValue(cachingKey) + if (cachedValue !== null) return cachedValue + + const queriedValues: IPlugin[] = await DatabaseClient.drizzleInstance + .select() + .from(pluginsTable) + .offset((page - 1) * this.PAGE_SIZE) + .limit(this.PAGE_SIZE) + .execute() + + await RedisClient.setValue(cachingKey, queriedValues) + return queriedValues + } + + /** + * @summary Get the plugin by ID. + * @description Get the plugin by ID. + * @param id The ID of the plugin. + * @returns The plugin by ID. + */ + public static async getPluginById(id: number): Promise { + const cachingKey: string = `pluginById:${id}` + const cachedValue: IPlugin | NotFoundInDatabaseType | null = await RedisClient.getValue(cachingKey) + if (cachedValue !== null) return cachedValue === this.NOT_FOUND_IN_DATABASE ? null : cachedValue + + const queriedValue: IPlugin[] = await DatabaseClient.drizzleInstance + .select() + .from(pluginsTable) + .where(eq(pluginsTable.id, id)) + .execute() + + if (queriedValue.length === 0) { + await RedisClient.setValue(cachingKey, this.NOT_FOUND_IN_DATABASE) + return null + } + + const finalResponse: IPlugin = queriedValue[0] + await RedisClient.setValue(cachingKey, finalResponse) + return finalResponse + } + + /** + * @summary Create a plugin. + * @description Create a plugin. + * @param plugin The plugin to create. + * @returns The created plugin. + */ + public static async createPlugin(plugin: IPluginCreatePayload): Promise { + try { + const addedPlugin: IPlugin[] = await DatabaseClient.drizzleInstance + .insert(pluginsTable) + .values(plugin) + .returning() + .execute() + + await RedisClient.deleteValues( + this.PLUGINS_COUNT_CACHING_KEY, + `pluginsByPage:*`, + `pluginById:${addedPlugin[0].id}`, + ) + return addedPlugin[0] + } catch { + return null + } + } + + /** + * @summary Update a plugin. + * @description Update a plugin. + * @param id The ID of the plugin. + * @param plugin The plugin to update. + * @returns The updated plugin. + */ + public static async updatePlugin(id: number, plugin: IPluginUpdatePayload): Promise { + try { + const updatedPlugin: IPlugin[] = await DatabaseClient.drizzleInstance + .update(pluginsTable) + .set(plugin) + .where(eq(pluginsTable.id, id)) + .returning() + .execute() + + await RedisClient.deleteValues(`pluginsByPage:*`, `pluginById:${id}`) + return updatedPlugin[0] + } catch { + return null + } + } + + public static async deletePlugin(id: number): Promise { + try { + await DatabaseClient.drizzleInstance.delete(pluginsTable).where(eq(pluginsTable.id, id)).execute() + return true + } catch { + return false + } + } +} + +export { PluginsService } From 67897d254e3837639e3ac6e540f35829ea4fbbfd Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 10:18:42 +0200 Subject: [PATCH 27/35] feat(scripts->db): added scripts for DrizzleORM --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 74d3f46..c05356a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "format:check": "bunx prettier --check .", "lint": "bunx eslint .", "lint:fix": "bunx eslint --fix .", + "db:migrate": "bunx drizzle-kit migrate", + "db:push": "bunx drizzle-kit push", + "db:studio": "bunx drizzle-kit studio", "build": "bun build src/Main.ts --outdir dist --target bun", "start": "bun run dist/Main.js", "dev": "bun run --watch src/Main.ts", From e6f16a9bc73bd21cd735f24715bc651505d4e20f Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 10:21:00 +0200 Subject: [PATCH 28/35] fix(package.json): fixed URL to match a real one --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c05356a..9bc6bb7 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "MIT", "author": { "name": "SerenityJS", - "url": "https://serenityjs.com" + "url": "https://serenityjs.net" }, "homepage": "https://github.com/SerenityJS/api-service", "repository": { From d3709a42603d6ef8789e7d32337e83c9543843b0 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 10:34:07 +0200 Subject: [PATCH 29/35] feat(clients->discord): added Discord client --- .env.example | 6 + bun.lock | 30 ++- package.json | 2 +- src/Main.ts | 2 + src/globals/EnvironmentVariables.ts | 21 ++ src/globals/clients/DiscordClient.ts | 185 ++++++++++++++++++ .../discord/DiscordClientConfiguration.ts | 32 +++ 7 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 src/globals/clients/DiscordClient.ts create mode 100644 src/globals/configuration/discord/DiscordClientConfiguration.ts diff --git a/.env.example b/.env.example index 5102a12..991f0c1 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,12 @@ # Port of the application. APP_PORT=3000 +# Options related to the Discord bot. +# Token of the Discord bot. +DISCORD_TOKEN= +# Optional. Channel ID for plugin approval messages (defaults to the Serenity server channel used before). +# DISCORD_PLUGIN_APPROVAL_CHANNEL_ID= + # Options related to the database engine. # URL end-point of the database engine. # Format: postgresql://:@:/ diff --git a/bun.lock b/bun.lock index 5e69e6f..dc80059 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "@types/express": "^5.0.6", "axios": "^1.11.0", "cors": "^2.8.6", - "discord.js": "^14.22.1", + "discord.js": "^14.26.3", "drizzle-orm": "^0.45.2", "express": "^5.2.1", }, @@ -33,15 +33,15 @@ }, }, "packages": { - "@discordjs/builders": ["@discordjs/builders@1.11.3", "", { "dependencies": { "@discordjs/formatters": "^0.6.1", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.16", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw=="], + "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], - "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], - "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], + "@discordjs/rest": ["@discordjs/rest@2.6.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg=="], - "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], @@ -285,9 +285,9 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "discord-api-types": ["discord-api-types@0.38.22", "", {}, "sha512-2gnYrgXN3yTlv2cKBISI/A8btZwsSZLwKpIQXeI1cS8a7W7wP3sFVQOm3mPuuinTD8jJCKGPGNH399zE7Un1kA=="], + "discord-api-types": ["discord-api-types@0.38.47", "", {}, "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA=="], - "discord.js": ["discord.js@14.22.1", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.16", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w=="], + "discord.js": ["discord.js@14.26.3", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.1", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.40", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-XEKtYn28YFsiJ5l4fLRyikdbo6RD5oFyqfVHQlvXz2104JhH/E8slN28dbky05w3DCrJcNVWvhVvcJCTSl/KIg=="], "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], @@ -447,7 +447,7 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], + "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], @@ -669,7 +669,7 @@ "typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="], - "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], + "undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], @@ -707,8 +707,16 @@ "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@discordjs/rest/@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@discordjs/ws/@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], + + "@discordjs/ws/@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], + + "@discordjs/ws/discord-api-types": ["discord-api-types@0.38.22", "", {}, "sha512-2gnYrgXN3yTlv2cKBISI/A8btZwsSZLwKpIQXeI1cS8a7W7wP3sFVQOm3mPuuinTD8jJCKGPGNH399zE7Un1kA=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -751,6 +759,10 @@ "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "@discordjs/ws/@discordjs/rest/magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], + + "@discordjs/ws/@discordjs/rest/undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], diff --git a/package.json b/package.json index 9bc6bb7..15a575c 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@types/express": "^5.0.6", "axios": "^1.11.0", "cors": "^2.8.6", - "discord.js": "^14.22.1", + "discord.js": "^14.26.3", "drizzle-orm": "^0.45.2", "express": "^5.2.1" } diff --git a/src/Main.ts b/src/Main.ts index 2ccd09a..d9b839f 100644 --- a/src/Main.ts +++ b/src/Main.ts @@ -1,3 +1,4 @@ +import { DiscordClient } from "./globals/clients/DiscordClient" import { ApplicationInstanceManager } from "./globals/managers/ApplicationInstanceManager" import { EnvironmentVariables } from "./globals/EnvironmentVariables" @@ -26,6 +27,7 @@ class Main { * @description Initialize the application. */ private static async init(): Promise { + DiscordClient.connect() ApplicationInstanceManager.instance.listen(EnvironmentVariables.APP_PORT, () => { console.log(`Server is running on port ${EnvironmentVariables.APP_PORT}.`) }) diff --git a/src/globals/EnvironmentVariables.ts b/src/globals/EnvironmentVariables.ts index fd3a7fb..e2c1a44 100644 --- a/src/globals/EnvironmentVariables.ts +++ b/src/globals/EnvironmentVariables.ts @@ -22,6 +22,27 @@ class EnvironmentVariables { return number } + /** + * @summary Get the token of the Discord bot. + * @description Get the token of the Discord bot. + * @returns The token of the Discord bot. + */ + public static get DISCORD_TOKEN(): string { + const value: string | undefined = process.env.DISCORD_TOKEN + if (value === undefined) throw new Error("DISCORD_TOKEN is not set.") + return value + } + + /** + * @summary Get the ID of the Discord plugin approval channel. + * @description Get the ID of the Discord plugin approval channel. + * @returns The ID of the Discord plugin approval channel. + */ + public static get DISCORD_PLUGIN_APPROVAL_CHANNEL_ID(): string { + const value: string | undefined = process.env.DISCORD_PLUGIN_APPROVAL_CHANNEL_ID + return value ?? "1411567293552529462" + } + /** * @summary Get the URL of the database. * @description Get the URL of the database. diff --git a/src/globals/clients/DiscordClient.ts b/src/globals/clients/DiscordClient.ts new file mode 100644 index 0000000..7e22986 --- /dev/null +++ b/src/globals/clients/DiscordClient.ts @@ -0,0 +1,185 @@ +import axios from "axios" +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + Client, + EmbedBuilder, + Events, + GatewayIntentBits, + type GuildTextBasedChannel, + type Interaction, +} from "discord.js" +import type { IPlugin } from "../../models/services/databases/plugins/base/interfaces/IPlugin" +import { PluginsService } from "../../services/databases/plugins/PluginsService" +import { DiscordClientConfiguration } from "../configuration/discord/DiscordClientConfiguration" +import { EnvironmentVariables } from "../EnvironmentVariables" + +/** + * @summary Client for the Discord bot (login, plugin approval flow). + * @description Client for the Discord bot. Connects with `connect()`; posts approval requests to the + * configured channel and handles Approve / Reject button interactions. + */ +class DiscordClient { + /** + * @summary Private constructor. + * @description Private constructor that prevents the class from being instantiated. + */ + private constructor() {} + + /** + * @summary Internal instance of the Discord client. + * @description Internal instance of the Discord client. + * @returns The internal instance of the Discord client. + */ + private static internalInstance: Client | null = null + + /** + * @summary Has connected. + * @description Has connected. + * @returns The has connected. + */ + private static hasConnected: boolean = false + + /** + * @summary Interaction handler registered. + * @description Interaction handler registered. + * @returns The interaction handler registered. + */ + private static interactionHandlerRegistered: boolean = false + + /** + * @summary When ready. + * @description When ready. + * @returns The when ready. + */ + private static whenReady: Promise | null = null + + /** + * @summary Get the instance of the Discord client. + * @description Get the instance of the Discord client. + * @returns The instance of the Discord client. + */ + public static get instance(): Client { + if (this.internalInstance === null) { + this.internalInstance = new Client({ + intents: [GatewayIntentBits.Guilds], + }) + } + return this.internalInstance + } + + /** + * @summary Get the ready promise. + * @description Get the ready promise. + * @returns The ready promise. + */ + private static getReady(): Promise { + if (this.whenReady === null) { + const client: Client = this.instance + this.whenReady = client.isReady() + ? Promise.resolve() + : new Promise((resolve) => { + client.once(Events.ClientReady, () => { + resolve() + }) + }) + } + + return this.whenReady + } + + /** + * @summary Log in the bot and register interaction handling (no-op if `DISCORD_TOKEN` is unset). + * @description Log in the bot and register interaction handling. + */ + public static connect(): void { + const token: string | undefined = process.env.DISCORD_TOKEN?.trim() + if (!token) return + if (this.hasConnected) { + this.ensureInteractionHandler() + return + } + this.hasConnected = true + void this.instance.login(token) + this.ensureInteractionHandler() + } + + private static ensureInteractionHandler(): void { + if (this.interactionHandlerRegistered) return + this.interactionHandlerRegistered = true + this.instance.on(Events.InteractionCreate, (interaction: Interaction) => { + void this.handleInteraction(interaction).catch((error: unknown) => { + console.error("DiscordClient.handleInteraction failed:", error) + }) + }) + } + + /** + * @summary Send an embed with approve / reject actions for a plugin. + * @description Waits for the client to be ready, then posts to the plugin approval channel. + */ + public static async sendPluginApprovalRequest(plugin: IPlugin): Promise { + if (!process.env.DISCORD_TOKEN?.trim()) return + this.connect() + await this.getReady() + const channel = (await this.instance.channels.fetch( + EnvironmentVariables.DISCORD_PLUGIN_APPROVAL_CHANNEL_ID, + )) as GuildTextBasedChannel | null + if (!channel?.isTextBased()) { + console.error( + `Channel with ID ${EnvironmentVariables.DISCORD_PLUGIN_APPROVAL_CHANNEL_ID} not found or is not text-based.`, + ) + return + } + const embed: EmbedBuilder = new EmbedBuilder() + .setTitle("New Plugin Approval Request") + .setDescription( + `A new plugin has been submitted for approval:\n\n**Name:** ${plugin.name}\n**Owner:** ${plugin.owner}\n**URL:** ${plugin.url}\n\n${DiscordClientConfiguration.APPROVAL_INSTRUCTIONS}`, + ) + .setColor(0x8560e9) + .setThumbnail(await this.getPluginLogoUrl(plugin)) + const row: ActionRowBuilder = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`approve:${plugin.id}`).setLabel("Approve").setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId(`reject:${plugin.id}`).setLabel("Reject").setStyle(ButtonStyle.Danger), + ) + await channel.send({ embeds: [embed], components: [row] }) + } + + /** + * @summary Get the plugin logo URL. + * @description Get the plugin logo URL. + * @param plugin The plugin. + * @returns The plugin logo URL. + */ + private static async getPluginLogoUrl(plugin: IPlugin): Promise { + const url: string = `https://raw.githubusercontent.com/${plugin.owner}/${plugin.name}/${plugin.branch}/public/logo.png` + try { + const response = await axios.head(url) + return response.status === 200 ? url : DiscordClientConfiguration.DEFAULT_LOGO + } catch { + return DiscordClientConfiguration.DEFAULT_LOGO + } + } + + /** + * @summary Handle the interaction. + * @description Handle the interaction. + * @param interaction The interaction. + */ + private static async handleInteraction(interaction: Interaction): Promise { + if (!interaction.isButton()) return + const [action, idPart] = interaction.customId.split(":") + if (action !== "approve" && action !== "reject") return + const pluginId: number = Number(idPart) + if (Number.isNaN(pluginId)) return + const approved: boolean = action === "approve" + await PluginsService.updatePlugin(pluginId, { approved }) + await interaction.reply({ content: `Plugin ${approved ? "approved" : "rejected"}.` }) + if (interaction.message && "edit" in interaction.message) { + await interaction.message.edit({ components: [] }) + } + } +} + +export { DiscordClient } diff --git a/src/globals/configuration/discord/DiscordClientConfiguration.ts b/src/globals/configuration/discord/DiscordClientConfiguration.ts new file mode 100644 index 0000000..1c71def --- /dev/null +++ b/src/globals/configuration/discord/DiscordClientConfiguration.ts @@ -0,0 +1,32 @@ +/** + * @summary Configuration for the Discord client. + * @description Configuration for the Discord client. + */ +class DiscordClientConfiguration { + /** + * @summary Private constructor. + * @description Private constructor to prevent instantiation of the class. + */ + private constructor() {} + + public static readonly DEFAULT_LOGO: string = "https://avatars.githubusercontent.com/u/92610726?s=88&v=4" + + /** + * @summary Approval instructions. + * @description Approval instructions. + * @returns The approval instructions. + */ + public static readonly APPROVAL_INSTRUCTIONS: string = ` + **Verify that the plugin meets the following criteria:** + + - The plugin is relevant to SerenityJS and its ecosystem.\n + - The plugin is well-maintained and has at least a release on GitHub.\n + - The plugin has a proper README file and documentation.\n + - The plugin does not contain any malicious code or vulnerabilities.\n + - The plugin follows best practices for coding and design.\n + + Please review the plugin and **approve** or **reject** it by clicking one of the buttons below. + ` +} + +export { DiscordClientConfiguration } From 8c64284e98cf95405bfc3669fe908dd4c65bde9a Mon Sep 17 00:00:00 2001 From: Oliwier Date: Fri, 24 Apr 2026 10:45:54 +0200 Subject: [PATCH 30/35] fix(ci): fixed script casing --- .github/workflows/CI.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 17a4fd1..46535d8 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -42,7 +42,7 @@ jobs: run: bunx tsc --noEmit - name: Check formatting of all files using Prettier. - run: bun run format:Check + run: bun run format:check - name: Run tests. run: bun run test From 3b0c72e61c642d724441e184ef1c2d163a361c03 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Wed, 29 Apr 2026 13:52:42 +0200 Subject: [PATCH 31/35] feat(routes): added plugin routes --- src/Main.ts | 2 + src/controllers/plugins/PluginsController.ts | 60 ++++++++ src/globals/EnvironmentVariables.ts | 12 ++ .../managers/ApplicationInstanceManager.ts | 2 + .../github/interfaces/IEnrichedPlugin.ts | 30 ++++ .../github/interfaces/IGitHubReleaseAsset.ts | 8 + .../github/interfaces/IGitHubRepository.ts | 17 +++ .../interfaces/IGitHubRepositoryCommit.ts | 11 ++ .../IGitHubRepositoryCommitAuthor.ts | 6 + .../IGitHubRepositoryCommitAuthorUser.ts | 5 + .../IGitHubRepositoryCommitDetails.ts | 8 + .../IGitHubRepositoryContributor.ts | 8 + .../interfaces/IGitHubRepositoryOwner.ts | 7 + .../interfaces/IGitHubRepositoryQuery.ts | 9 ++ .../interfaces/IGitHubRepositoryRelease.ts | 13 ++ .../github/interfaces/IPluginCommit.ts | 9 ++ .../github/interfaces/IPluginContributor.ts | 8 + .../github/interfaces/IPluginPackageJSON.ts | 6 + .../github/interfaces/IPluginRelease.ts | 13 ++ .../github/interfaces/IPluginReleaseAsset.ts | 8 + .../services/github/interfaces/index.ts | 16 ++ src/routes/index.ts | 8 + src/routes/plugins.ts | 22 +++ src/services/base/BaseDatabaseService.ts | 4 +- src/services/base/BaseGitHubService.ts | 42 ++++++ .../databases/plugins/PluginsService.ts | 72 ++++++++- src/services/github/GitHubClientService.ts | 126 ++++++++++++++++ .../github/GitHubPluginEnrichmentService.ts | 139 ++++++++++++++++++ src/services/github/GitHubSyncService.ts | 116 +++++++++++++++ src/services/github/index.ts | 3 + 30 files changed, 786 insertions(+), 4 deletions(-) create mode 100644 src/controllers/plugins/PluginsController.ts create mode 100644 src/models/services/github/interfaces/IEnrichedPlugin.ts create mode 100644 src/models/services/github/interfaces/IGitHubReleaseAsset.ts create mode 100644 src/models/services/github/interfaces/IGitHubRepository.ts create mode 100644 src/models/services/github/interfaces/IGitHubRepositoryCommit.ts create mode 100644 src/models/services/github/interfaces/IGitHubRepositoryCommitAuthor.ts create mode 100644 src/models/services/github/interfaces/IGitHubRepositoryCommitAuthorUser.ts create mode 100644 src/models/services/github/interfaces/IGitHubRepositoryCommitDetails.ts create mode 100644 src/models/services/github/interfaces/IGitHubRepositoryContributor.ts create mode 100644 src/models/services/github/interfaces/IGitHubRepositoryOwner.ts create mode 100644 src/models/services/github/interfaces/IGitHubRepositoryQuery.ts create mode 100644 src/models/services/github/interfaces/IGitHubRepositoryRelease.ts create mode 100644 src/models/services/github/interfaces/IPluginCommit.ts create mode 100644 src/models/services/github/interfaces/IPluginContributor.ts create mode 100644 src/models/services/github/interfaces/IPluginPackageJSON.ts create mode 100644 src/models/services/github/interfaces/IPluginRelease.ts create mode 100644 src/models/services/github/interfaces/IPluginReleaseAsset.ts create mode 100644 src/models/services/github/interfaces/index.ts create mode 100644 src/routes/index.ts create mode 100644 src/routes/plugins.ts create mode 100644 src/services/base/BaseGitHubService.ts create mode 100644 src/services/github/GitHubClientService.ts create mode 100644 src/services/github/GitHubPluginEnrichmentService.ts create mode 100644 src/services/github/GitHubSyncService.ts create mode 100644 src/services/github/index.ts diff --git a/src/Main.ts b/src/Main.ts index d9b839f..0f04742 100644 --- a/src/Main.ts +++ b/src/Main.ts @@ -1,6 +1,7 @@ import { DiscordClient } from "./globals/clients/DiscordClient" import { ApplicationInstanceManager } from "./globals/managers/ApplicationInstanceManager" import { EnvironmentVariables } from "./globals/EnvironmentVariables" +import { GitHubSyncService } from "./services/github" /** * @summary Main class. @@ -28,6 +29,7 @@ class Main { */ private static async init(): Promise { DiscordClient.connect() + GitHubSyncService.start() ApplicationInstanceManager.instance.listen(EnvironmentVariables.APP_PORT, () => { console.log(`Server is running on port ${EnvironmentVariables.APP_PORT}.`) }) diff --git a/src/controllers/plugins/PluginsController.ts b/src/controllers/plugins/PluginsController.ts new file mode 100644 index 0000000..d40478c --- /dev/null +++ b/src/controllers/plugins/PluginsController.ts @@ -0,0 +1,60 @@ +import type { Request, Response } from "express" +import { GitHubSyncService } from "../../services/github" + +/** + * @summary Controller for plugins. + * @description Controller for plugins. + */ +class PluginsController { + private static readonly PAGINATION_PAGE_SIZE: number = 25 + + /** + * @summary Private constructor. + * @description Private constructor to prevent instantiation of the class. + */ + private constructor() {} + + /** + * @summary Get plugins pagination. + * @description Get plugins pagination. + * @param request The request object. + * @param response The response object. + * @returns The plugins pagination. + */ + public static async getPluginsPagination(request: Request, response: Response): Promise { + const pageQuery: string | undefined = typeof request.query.page === "string" ? request.query.page : undefined + const page: number = pageQuery === undefined ? 1 : Number.parseInt(pageQuery, 10) + if (!Number.isInteger(page) || page < 1) { + response.status(400).send({ message: "Query parameter `page` must be a positive integer." }) + return + } + + try { + const [count, items] = await Promise.all([ + GitHubSyncService.getPluginsCount(), + GitHubSyncService.getPluginsByPage(page, this.PAGINATION_PAGE_SIZE), + ]) + response.status(200).send({ count, items }) + } catch { + response.status(500).send({ message: "Failed to fetch paginated plugins." }) + } + } + + /** + * @summary Get all plugins. + * @description Get all plugins. + * @param request The request object. + * @param response The response object. + * @returns The all plugins. + */ + public static async getAllPlugins(_: Request, response: Response): Promise { + try { + const plugins = await GitHubSyncService.getAllPlugins() + response.status(200).send(plugins) + } catch { + response.status(500).send({ message: "Failed to fetch all plugins." }) + } + } +} + +export { PluginsController } diff --git a/src/globals/EnvironmentVariables.ts b/src/globals/EnvironmentVariables.ts index e2c1a44..808657c 100644 --- a/src/globals/EnvironmentVariables.ts +++ b/src/globals/EnvironmentVariables.ts @@ -83,6 +83,18 @@ class EnvironmentVariables { if (value === undefined) throw new Error("CACHE_ENGINE_URL is not set.") return value } + + /** + * @summary Get the GitHub token. + * @description Get the GitHub token used for authenticated GitHub API calls. + * @returns The GitHub token if provided. + */ + public static get GITHUB_TOKEN(): string | undefined { + const value: string | undefined = process.env.GITHUB_TOKEN + if (value === undefined) return undefined + const trimmedValue: string = value.trim() + return trimmedValue.length === 0 ? undefined : trimmedValue + } } export { EnvironmentVariables } diff --git a/src/globals/managers/ApplicationInstanceManager.ts b/src/globals/managers/ApplicationInstanceManager.ts index 5e21314..201759c 100644 --- a/src/globals/managers/ApplicationInstanceManager.ts +++ b/src/globals/managers/ApplicationInstanceManager.ts @@ -1,5 +1,6 @@ import cors from "cors" import express from "express" +import { pluginsRouter } from "../../routes/plugins" /** * @summary Manager for the application instance. @@ -40,6 +41,7 @@ class ApplicationInstanceManager { */ private static loadRoutes(): void { if (this.internalInstance === null) return + this.internalInstance.use("/v1/plugins", pluginsRouter) } } diff --git a/src/models/services/github/interfaces/IEnrichedPlugin.ts b/src/models/services/github/interfaces/IEnrichedPlugin.ts new file mode 100644 index 0000000..b0e0132 --- /dev/null +++ b/src/models/services/github/interfaces/IEnrichedPlugin.ts @@ -0,0 +1,30 @@ +import type { IPluginCommit } from "./IPluginCommit" +import type { IPluginContributor } from "./IPluginContributor" +import type { IPluginRelease } from "./IPluginRelease" + +interface IEnrichedPlugin { + readonly id: number + readonly approved: boolean + readonly name: string + readonly owner: string + readonly url: string + readonly branch: string + readonly description: string | null + readonly version: string | null + readonly stars: number + readonly downloads: number + readonly forks: number + readonly issues: number + readonly keywords: string[] + readonly logo: string + readonly banner: string | null + readonly published: string | null + readonly updated: string | null + readonly readme: string | null + readonly gallery: string[] + readonly contributors: IPluginContributor[] + readonly commits: IPluginCommit[] + readonly releases: IPluginRelease[] +} + +export type { IEnrichedPlugin } diff --git a/src/models/services/github/interfaces/IGitHubReleaseAsset.ts b/src/models/services/github/interfaces/IGitHubReleaseAsset.ts new file mode 100644 index 0000000..d7c2751 --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubReleaseAsset.ts @@ -0,0 +1,8 @@ +interface IGitHubReleaseAsset { + readonly name: string + readonly size: number + readonly browser_download_url: string + readonly download_count: number +} + +export type { IGitHubReleaseAsset } diff --git a/src/models/services/github/interfaces/IGitHubRepository.ts b/src/models/services/github/interfaces/IGitHubRepository.ts new file mode 100644 index 0000000..c1f8918 --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepository.ts @@ -0,0 +1,17 @@ +import type { IGitHubRepositoryOwner } from "./IGitHubRepositoryOwner" + +interface IGitHubRepository { + readonly id: number + readonly name: string + readonly owner: IGitHubRepositoryOwner + readonly html_url: string + readonly default_branch: string + readonly description: string | null + readonly stargazers_count: number + readonly forks_count: number + readonly open_issues_count: number + readonly created_at: string + readonly updated_at: string +} + +export type { IGitHubRepository } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryCommit.ts b/src/models/services/github/interfaces/IGitHubRepositoryCommit.ts new file mode 100644 index 0000000..e6f8b8e --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryCommit.ts @@ -0,0 +1,11 @@ +import type { IGitHubRepositoryCommitAuthorUser } from "./IGitHubRepositoryCommitAuthorUser" +import type { IGitHubRepositoryCommitDetails } from "./IGitHubRepositoryCommitDetails" + +interface IGitHubRepositoryCommit { + readonly sha: string + readonly html_url: string + readonly commit: IGitHubRepositoryCommitDetails + readonly author: IGitHubRepositoryCommitAuthorUser | null +} + +export type { IGitHubRepositoryCommit } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthor.ts b/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthor.ts new file mode 100644 index 0000000..a2dfafb --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthor.ts @@ -0,0 +1,6 @@ +interface IGitHubRepositoryCommitAuthor { + readonly name: string + readonly date: string +} + +export type { IGitHubRepositoryCommitAuthor } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthorUser.ts b/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthorUser.ts new file mode 100644 index 0000000..702436e --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryCommitAuthorUser.ts @@ -0,0 +1,5 @@ +interface IGitHubRepositoryCommitAuthorUser { + readonly login: string +} + +export type { IGitHubRepositoryCommitAuthorUser } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryCommitDetails.ts b/src/models/services/github/interfaces/IGitHubRepositoryCommitDetails.ts new file mode 100644 index 0000000..b68d570 --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryCommitDetails.ts @@ -0,0 +1,8 @@ +import type { IGitHubRepositoryCommitAuthor } from "./IGitHubRepositoryCommitAuthor" + +interface IGitHubRepositoryCommitDetails { + readonly message: string + readonly author: IGitHubRepositoryCommitAuthor +} + +export type { IGitHubRepositoryCommitDetails } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryContributor.ts b/src/models/services/github/interfaces/IGitHubRepositoryContributor.ts new file mode 100644 index 0000000..cc7d6a2 --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryContributor.ts @@ -0,0 +1,8 @@ +interface IGitHubRepositoryContributor { + readonly login: string + readonly html_url: string + readonly avatar_url: string + readonly contributions: number +} + +export type { IGitHubRepositoryContributor } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryOwner.ts b/src/models/services/github/interfaces/IGitHubRepositoryOwner.ts new file mode 100644 index 0000000..2e0f2ee --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryOwner.ts @@ -0,0 +1,7 @@ +interface IGitHubRepositoryOwner { + readonly login: string + readonly avatar_url: string + readonly html_url: string +} + +export type { IGitHubRepositoryOwner } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryQuery.ts b/src/models/services/github/interfaces/IGitHubRepositoryQuery.ts new file mode 100644 index 0000000..ec4995e --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryQuery.ts @@ -0,0 +1,9 @@ +import type { IGitHubRepository } from "./IGitHubRepository" + +interface IGitHubRepositoryQuery { + readonly total_count: number + readonly incomplete_results: boolean + readonly items: IGitHubRepository[] +} + +export type { IGitHubRepositoryQuery } diff --git a/src/models/services/github/interfaces/IGitHubRepositoryRelease.ts b/src/models/services/github/interfaces/IGitHubRepositoryRelease.ts new file mode 100644 index 0000000..f70a44f --- /dev/null +++ b/src/models/services/github/interfaces/IGitHubRepositoryRelease.ts @@ -0,0 +1,13 @@ +import type { IGitHubReleaseAsset } from "./IGitHubReleaseAsset" + +interface IGitHubRepositoryRelease { + readonly name: string | null + readonly tag_name: string + readonly html_url: string + readonly body: string | null + readonly prerelease: boolean + readonly published_at: string + readonly assets: IGitHubReleaseAsset[] +} + +export type { IGitHubRepositoryRelease } diff --git a/src/models/services/github/interfaces/IPluginCommit.ts b/src/models/services/github/interfaces/IPluginCommit.ts new file mode 100644 index 0000000..04b8bd2 --- /dev/null +++ b/src/models/services/github/interfaces/IPluginCommit.ts @@ -0,0 +1,9 @@ +interface IPluginCommit { + readonly sha: string + readonly html_url: string + readonly message: string + readonly date: string + readonly author: string +} + +export type { IPluginCommit } diff --git a/src/models/services/github/interfaces/IPluginContributor.ts b/src/models/services/github/interfaces/IPluginContributor.ts new file mode 100644 index 0000000..12f9e10 --- /dev/null +++ b/src/models/services/github/interfaces/IPluginContributor.ts @@ -0,0 +1,8 @@ +interface IPluginContributor { + readonly username: string + readonly profile_url: string + readonly avatar_url: string + readonly contributions: number +} + +export type { IPluginContributor } diff --git a/src/models/services/github/interfaces/IPluginPackageJSON.ts b/src/models/services/github/interfaces/IPluginPackageJSON.ts new file mode 100644 index 0000000..e30f669 --- /dev/null +++ b/src/models/services/github/interfaces/IPluginPackageJSON.ts @@ -0,0 +1,6 @@ +interface IPluginPackageJSON { + readonly version?: string + readonly keywords?: string[] +} + +export type { IPluginPackageJSON } diff --git a/src/models/services/github/interfaces/IPluginRelease.ts b/src/models/services/github/interfaces/IPluginRelease.ts new file mode 100644 index 0000000..8aca224 --- /dev/null +++ b/src/models/services/github/interfaces/IPluginRelease.ts @@ -0,0 +1,13 @@ +import type { IPluginReleaseAsset } from "./IPluginReleaseAsset" + +interface IPluginRelease { + readonly name: string + readonly tag: string + readonly url: string + readonly description: string + readonly prerelease: boolean + readonly date: string + readonly assets: IPluginReleaseAsset[] +} + +export type { IPluginRelease } diff --git a/src/models/services/github/interfaces/IPluginReleaseAsset.ts b/src/models/services/github/interfaces/IPluginReleaseAsset.ts new file mode 100644 index 0000000..d4c893c --- /dev/null +++ b/src/models/services/github/interfaces/IPluginReleaseAsset.ts @@ -0,0 +1,8 @@ +interface IPluginReleaseAsset { + readonly name: string + readonly size: number + readonly download_url: string + readonly download_count: number +} + +export type { IPluginReleaseAsset } diff --git a/src/models/services/github/interfaces/index.ts b/src/models/services/github/interfaces/index.ts new file mode 100644 index 0000000..24e5c74 --- /dev/null +++ b/src/models/services/github/interfaces/index.ts @@ -0,0 +1,16 @@ +export type * from "./IEnrichedPlugin" +export type * from "./IGitHubReleaseAsset" +export type * from "./IGitHubRepository" +export type * from "./IGitHubRepositoryCommit" +export type * from "./IGitHubRepositoryCommitAuthor" +export type * from "./IGitHubRepositoryCommitAuthorUser" +export type * from "./IGitHubRepositoryCommitDetails" +export type * from "./IGitHubRepositoryContributor" +export type * from "./IGitHubRepositoryOwner" +export type * from "./IGitHubRepositoryQuery" +export type * from "./IGitHubRepositoryRelease" +export type * from "./IPluginCommit" +export type * from "./IPluginContributor" +export type * from "./IPluginPackageJSON" +export type * from "./IPluginRelease" +export type * from "./IPluginReleaseAsset" diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..4ddbe90 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,8 @@ +import type { Express } from "express" +import { pluginsRouter } from "./plugins" + +const loadRoutes = (application: Express): void => { + application.use("/plugins", pluginsRouter) +} + +export { loadRoutes } diff --git a/src/routes/plugins.ts b/src/routes/plugins.ts new file mode 100644 index 0000000..bfa0fd6 --- /dev/null +++ b/src/routes/plugins.ts @@ -0,0 +1,22 @@ +import { Router } from "express" +import { PluginsController } from "../controllers/plugins/PluginsController" + +/** + * @summary Router for plugins. + * @description Router for plugins. + */ +const pluginsRouter: Router = Router() + +/** + * @summary Get plugins pagination. + * @returns The plugins pagination. + */ +pluginsRouter.get("/", PluginsController.getPluginsPagination) + +/** + * @summary Get all plugins. + * @returns The all plugins. + */ +pluginsRouter.get("/all", PluginsController.getAllPlugins) + +export { pluginsRouter } diff --git a/src/services/base/BaseDatabaseService.ts b/src/services/base/BaseDatabaseService.ts index f1c1940..155db07 100644 --- a/src/services/base/BaseDatabaseService.ts +++ b/src/services/base/BaseDatabaseService.ts @@ -22,13 +22,13 @@ abstract class BaseDatabaseService { * @description Default page size. Do not modify this value, it's only in case when you edit PAGE_SIZE. * @see `this.PAGE_SIZE` If you want to change the page size, you should edit this value. */ - protected static readonly DEFAULT_PAGE_SIZE: number = 50 + public static readonly DEFAULT_PAGE_SIZE: number = 50 /** * @summary Page size. * @description A property used with pagination. It's set to the default page size by default. */ - protected static readonly PAGE_SIZE: number = this.DEFAULT_PAGE_SIZE + public static readonly PAGE_SIZE: number = this.DEFAULT_PAGE_SIZE } export { BaseDatabaseService } diff --git a/src/services/base/BaseGitHubService.ts b/src/services/base/BaseGitHubService.ts new file mode 100644 index 0000000..2155924 --- /dev/null +++ b/src/services/base/BaseGitHubService.ts @@ -0,0 +1,42 @@ +import type { AxiosRequestConfig } from "axios" +import { EnvironmentVariables } from "../../globals/EnvironmentVariables" + +/** + * @summary Base class for GitHub services. + * @description Provides shared constants and request helpers for GitHub API integrations. + */ +abstract class BaseGitHubService { + /** + * @summary Protected constructor. + * @description Protected constructor to prevent direct instantiation while allowing inheritance. + */ + protected constructor() {} + + protected static readonly GITHUB_API_BASE_URL: string = "https://api.github.com" + protected static readonly GITHUB_RAW_BASE_URL: string = "https://raw.githubusercontent.com" + protected static readonly DEFAULT_REPOSITORY_SEARCH_QUERY: string = "topic:serenityjs-plugin" + + protected static buildRequestConfig(extraConfig?: AxiosRequestConfig): AxiosRequestConfig { + const token: string | undefined = EnvironmentVariables.GITHUB_TOKEN + return { + ...extraConfig, + headers: { + Accept: "application/vnd.github.v3+json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(extraConfig?.headers ?? {}), + }, + } + } + + protected static buildRepositoryApiUrl(owner: string, name: string, suffix: string = ""): string { + const normalizedSuffix: string = suffix.startsWith("/") || suffix.length === 0 ? suffix : `/${suffix}` + return `${this.GITHUB_API_BASE_URL}/repos/${owner}/${name}${normalizedSuffix}` + } + + protected static buildRawRepositoryFileUrl(owner: string, name: string, branch: string, filePath: string): string { + const normalizedPath: string = filePath.startsWith("/") ? filePath.slice(1) : filePath + return `${this.GITHUB_RAW_BASE_URL}/${owner}/${name}/${branch}/${normalizedPath}` + } +} + +export { BaseGitHubService } diff --git a/src/services/databases/plugins/PluginsService.ts b/src/services/databases/plugins/PluginsService.ts index 75e519c..de64545 100644 --- a/src/services/databases/plugins/PluginsService.ts +++ b/src/services/databases/plugins/PluginsService.ts @@ -1,4 +1,4 @@ -import { count, eq } from "drizzle-orm" +import { and, count, eq } from "drizzle-orm" import { DatabaseClient } from "../../../globals/clients/DatabaseClient" import { RedisClient } from "../../../globals/clients/RedisClient" import { BaseDatabaseService } from "../../base/BaseDatabaseService" @@ -26,6 +26,8 @@ class PluginsService extends BaseDatabaseService { * @description Plugins count caching key. */ private static readonly PLUGINS_COUNT_CACHING_KEY: string = "pluginsCount" + private static readonly ALL_PLUGINS_CACHING_KEY: string = "allPlugins" + private static readonly APPROVED_PLUGINS_CACHING_KEY: string = "approvedPlugins" /** * @summary Get the number of plugins. @@ -68,6 +70,58 @@ class PluginsService extends BaseDatabaseService { return queriedValues } + /** + * @summary Get all plugins. + * @description Get all plugins from cache or the database. + * @returns All plugins. + */ + public static async getAllPlugins(): Promise { + const cachedValue: IPlugin[] | null = await RedisClient.getValue(this.ALL_PLUGINS_CACHING_KEY) + if (cachedValue !== null) return cachedValue + + const queriedValues: IPlugin[] = await DatabaseClient.drizzleInstance.select().from(pluginsTable).execute() + await RedisClient.setValue(this.ALL_PLUGINS_CACHING_KEY, queriedValues) + return queriedValues + } + + /** + * @summary Get approved plugins. + * @description Get approved plugins from cache or the database. + * @returns Approved plugins. + */ + public static async getApprovedPlugins(): Promise { + const cachedValue: IPlugin[] | null = await RedisClient.getValue(this.APPROVED_PLUGINS_CACHING_KEY) + if (cachedValue !== null) return cachedValue + + const queriedValues: IPlugin[] = await DatabaseClient.drizzleInstance + .select() + .from(pluginsTable) + .where(eq(pluginsTable.approved, true)) + .execute() + + await RedisClient.setValue(this.APPROVED_PLUGINS_CACHING_KEY, queriedValues) + return queriedValues + } + + /** + * @summary Get a plugin by owner and name. + * @description Get a plugin by owner and name from the database. + * @param owner The plugin owner. + * @param name The plugin name. + * @returns The plugin if found. + */ + public static async getPluginByOwnerAndName(owner: string, name: string): Promise { + const queriedValues: IPlugin[] = await DatabaseClient.drizzleInstance + .select() + .from(pluginsTable) + .where(and(eq(pluginsTable.owner, owner), eq(pluginsTable.name, name))) + .limit(1) + .execute() + + if (queriedValues.length === 0) return null + return queriedValues[0] + } + /** * @summary Get the plugin by ID. * @description Get the plugin by ID. @@ -111,6 +165,8 @@ class PluginsService extends BaseDatabaseService { await RedisClient.deleteValues( this.PLUGINS_COUNT_CACHING_KEY, + this.ALL_PLUGINS_CACHING_KEY, + this.APPROVED_PLUGINS_CACHING_KEY, `pluginsByPage:*`, `pluginById:${addedPlugin[0].id}`, ) @@ -136,7 +192,12 @@ class PluginsService extends BaseDatabaseService { .returning() .execute() - await RedisClient.deleteValues(`pluginsByPage:*`, `pluginById:${id}`) + await RedisClient.deleteValues( + this.ALL_PLUGINS_CACHING_KEY, + this.APPROVED_PLUGINS_CACHING_KEY, + `pluginsByPage:*`, + `pluginById:${id}`, + ) return updatedPlugin[0] } catch { return null @@ -146,6 +207,13 @@ class PluginsService extends BaseDatabaseService { public static async deletePlugin(id: number): Promise { try { await DatabaseClient.drizzleInstance.delete(pluginsTable).where(eq(pluginsTable.id, id)).execute() + await RedisClient.deleteValues( + this.PLUGINS_COUNT_CACHING_KEY, + this.ALL_PLUGINS_CACHING_KEY, + this.APPROVED_PLUGINS_CACHING_KEY, + `pluginsByPage:*`, + `pluginById:${id}`, + ) return true } catch { return false diff --git a/src/services/github/GitHubClientService.ts b/src/services/github/GitHubClientService.ts new file mode 100644 index 0000000..2c91321 --- /dev/null +++ b/src/services/github/GitHubClientService.ts @@ -0,0 +1,126 @@ +import axios from "axios" +import type { + IGitHubRepository, + IGitHubRepositoryCommit, + IGitHubRepositoryContributor, + IGitHubRepositoryQuery, + IGitHubRepositoryRelease, + IPluginPackageJSON, +} from "../../models/services/github/interfaces" +import { BaseGitHubService } from "../base/BaseGitHubService" + +/** + * @summary Service for GitHub API calls. + * @description Encapsulates all HTTP interactions with the GitHub and raw GitHub endpoints. + */ +class GitHubClientService extends BaseGitHubService { + private constructor() { + super() + } + + public static async searchPluginRepositories(): Promise { + try { + const response = await axios.get( + "https://api.github.com/search/repositories", + this.buildRequestConfig({ + params: { + q: this.DEFAULT_REPOSITORY_SEARCH_QUERY, + per_page: 100, + }, + }), + ) + return response.data.items + } catch { + return [] + } + } + + public static async getRepository(owner: string, name: string): Promise { + try { + const response = await axios.get( + this.buildRepositoryApiUrl(owner, name), + this.buildRequestConfig(), + ) + return response.data + } catch { + return null + } + } + + public static async getReleases(owner: string, name: string): Promise { + try { + const response = await axios.get( + this.buildRepositoryApiUrl(owner, name, "/releases"), + this.buildRequestConfig(), + ) + return response.data + } catch { + return [] + } + } + + public static async getContributors(owner: string, name: string): Promise { + try { + const response = await axios.get( + this.buildRepositoryApiUrl(owner, name, "/contributors"), + this.buildRequestConfig(), + ) + return response.data + } catch { + return [] + } + } + + public static async getCommits(owner: string, name: string): Promise { + try { + const response = await axios.get( + this.buildRepositoryApiUrl(owner, name, "/commits"), + this.buildRequestConfig({ + params: { per_page: 100 }, + }), + ) + return response.data + } catch { + return [] + } + } + + public static async getReadme(owner: string, name: string, branch: string): Promise { + try { + const response = await axios.get( + this.buildRawRepositoryFileUrl(owner, name, branch, "README.md"), + this.buildRequestConfig(), + ) + return response.data + } catch { + return null + } + } + + public static async getPackageJSON( + owner: string, + name: string, + branch: string, + ): Promise { + try { + const response = await axios.get( + this.buildRawRepositoryFileUrl(owner, name, branch, "package.json"), + this.buildRequestConfig(), + ) + return response.data + } catch { + return null + } + } + + public static async doesRawFileExist(rawFileUrl: string): Promise { + try { + const response = await axios.head(rawFileUrl, this.buildRequestConfig()) + return response.status === 200 + } catch { + return false + } + } +} + +export { GitHubClientService } diff --git a/src/services/github/GitHubPluginEnrichmentService.ts b/src/services/github/GitHubPluginEnrichmentService.ts new file mode 100644 index 0000000..843f4c5 --- /dev/null +++ b/src/services/github/GitHubPluginEnrichmentService.ts @@ -0,0 +1,139 @@ +import { DiscordClientConfiguration } from "../../globals/configuration/discord/DiscordClientConfiguration" +import type { IPlugin } from "../../models/services/databases/plugins/base/interfaces/IPlugin" +import type { + IEnrichedPlugin, + IPluginCommit, + IPluginContributor, + IPluginRelease, +} from "../../models/services/github/interfaces" +import { BaseGitHubService } from "../base/BaseGitHubService" +import { GitHubClientService } from "./GitHubClientService" + +/** + * @summary Service for plugin enrichment. + * @description Builds rich plugin payloads by collecting metadata from GitHub. + */ +class GitHubPluginEnrichmentService extends BaseGitHubService { + private constructor() { + super() + } + + public static async enrich(plugin: IPlugin): Promise { + const repository = await GitHubClientService.getRepository(plugin.owner, plugin.name) + if (repository === null) return null + + const [releases, contributors, commits, readme, packageJSON, logo, banner, gallery] = await Promise.all([ + GitHubClientService.getReleases(plugin.owner, plugin.name), + GitHubClientService.getContributors(plugin.owner, plugin.name), + GitHubClientService.getCommits(plugin.owner, plugin.name), + GitHubClientService.getReadme(plugin.owner, plugin.name, plugin.branch), + GitHubClientService.getPackageJSON(plugin.owner, plugin.name, plugin.branch), + this.getLogo(plugin), + this.getBanner(plugin), + this.getGallery(plugin), + ]) + + const mappedReleases: IPluginRelease[] = releases.map((release) => ({ + name: release.name ?? release.tag_name, + tag: release.tag_name, + url: release.html_url, + description: release.body ?? "", + prerelease: release.prerelease, + date: release.published_at, + assets: release.assets.map((asset) => ({ + name: asset.name, + size: asset.size, + download_url: asset.browser_download_url, + download_count: asset.download_count, + })), + })) + const mappedContributors: IPluginContributor[] = contributors.map((contributor) => ({ + username: contributor.login, + profile_url: contributor.html_url, + avatar_url: contributor.avatar_url, + contributions: contributor.contributions, + })) + const mappedCommits: IPluginCommit[] = commits.map((commit) => ({ + sha: commit.sha, + html_url: commit.html_url, + message: commit.commit.message, + date: commit.commit.author.date, + author: commit.author?.login ?? commit.commit.author.name, + })) + + const downloads: number = mappedReleases.reduce( + (totalDownloads, release) => + totalDownloads + + release.assets.reduce((releaseDownloads, asset) => releaseDownloads + asset.download_count, 0), + 0, + ) + + return { + id: plugin.id, + approved: plugin.approved, + name: plugin.name, + owner: plugin.owner, + url: plugin.url, + branch: plugin.branch, + description: repository.description, + version: packageJSON?.version ?? null, + stars: repository.stargazers_count, + downloads, + forks: repository.forks_count, + issues: repository.open_issues_count, + keywords: packageJSON?.keywords ?? [], + logo, + banner, + published: repository.created_at, + updated: repository.updated_at, + readme, + gallery, + contributors: mappedContributors, + commits: mappedCommits, + releases: mappedReleases, + } + } + + private static async getLogo(plugin: IPlugin): Promise { + const logoUrl: string = this.buildRawRepositoryFileUrl( + plugin.owner, + plugin.name, + plugin.branch, + "public/logo.png", + ) + const logoExists: boolean = await GitHubClientService.doesRawFileExist(logoUrl) + if (!logoExists) return DiscordClientConfiguration.DEFAULT_LOGO + return logoUrl + } + + private static async getBanner(plugin: IPlugin): Promise { + const bannerUrl: string = this.buildRawRepositoryFileUrl( + plugin.owner, + plugin.name, + plugin.branch, + "public/banner.png", + ) + const bannerExists: boolean = await GitHubClientService.doesRawFileExist(bannerUrl) + if (!bannerExists) return null + return bannerUrl + } + + private static async getGallery(plugin: IPlugin): Promise { + const galleryUrls: string[] = [] + for (let i = 1; i <= 10; i++) { + const imageUrl: string = this.buildRawRepositoryFileUrl( + plugin.owner, + plugin.name, + plugin.branch, + `public/gallery/image${i}.png`, + ) + const exists: boolean = await GitHubClientService.doesRawFileExist(imageUrl) + if (!exists) break + galleryUrls.push(imageUrl) + } + + return galleryUrls + } +} + +export { GitHubPluginEnrichmentService } diff --git a/src/services/github/GitHubSyncService.ts b/src/services/github/GitHubSyncService.ts new file mode 100644 index 0000000..b5a9940 --- /dev/null +++ b/src/services/github/GitHubSyncService.ts @@ -0,0 +1,116 @@ +import { DiscordClient } from "../../globals/clients/DiscordClient" +import { RedisClient } from "../../globals/clients/RedisClient" +import type { IPlugin } from "../../models/services/databases/plugins/base/interfaces/IPlugin" +import type { IEnrichedPlugin } from "../../models/services/github/interfaces" +import { PluginsService } from "../databases/plugins/PluginsService" +import { GitHubClientService } from "./GitHubClientService" +import { GitHubPluginEnrichmentService } from "./GitHubPluginEnrichmentService" + +/** + * @summary Service for GitHub synchronization. + * @description Synchronizes plugin repositories from GitHub into the database and keeps enriched cache fresh. + */ +class GitHubSyncService { + private constructor() {} + + private static readonly ENRICHED_PLUGINS_CACHE_KEY: string = "github:enrichedPlugins" + private static readonly REFRESH_TIME_IN_MILLISECONDS: number = 5 * 60 * 1000 + private static readonly CACHE_REBUILD_TIME_IN_MILLISECONDS: number = 60 * 60 * 1000 + + private static hasStarted: boolean = false + private static isRefreshing: boolean = false + private static enrichedPlugins: IEnrichedPlugin[] = [] + + public static start(): void { + if (this.hasStarted) return + this.hasStarted = true + + void this.refreshAndRebuildCache() + setInterval(() => { + void this.refreshDiscoveredPlugins() + }, this.REFRESH_TIME_IN_MILLISECONDS) + setInterval(() => { + void this.refreshAndRebuildCache() + }, this.CACHE_REBUILD_TIME_IN_MILLISECONDS) + } + + public static async getAllPlugins(): Promise { + if (this.enrichedPlugins.length > 0) return this.enrichedPlugins + + const cachedPlugins: IEnrichedPlugin[] | null = await RedisClient.getValue(this.ENRICHED_PLUGINS_CACHE_KEY) + if (cachedPlugins !== null) { + this.enrichedPlugins = cachedPlugins + return this.enrichedPlugins + } + + await this.rebuildApprovedPluginsCache() + return this.enrichedPlugins + } + + public static async getPluginsByPage(page: number, pageSize: number): Promise { + const plugins: IEnrichedPlugin[] = await this.getAllPlugins() + const offset: number = (page - 1) * pageSize + return plugins.slice(offset, offset + pageSize) + } + + public static async getPluginsCount(): Promise { + const plugins: IEnrichedPlugin[] = await this.getAllPlugins() + return plugins.length + } + + private static async refreshAndRebuildCache(): Promise { + await this.refreshDiscoveredPlugins() + await this.rebuildApprovedPluginsCache() + } + + private static async refreshDiscoveredPlugins(): Promise { + if (this.isRefreshing) return + this.isRefreshing = true + + try { + const repositories = await GitHubClientService.searchPluginRepositories() + for (const repository of repositories) { + const owner: string = repository.owner.login + const name: string = repository.name + + const existingPlugin: IPlugin | null = await PluginsService.getPluginByOwnerAndName(owner, name) + if (existingPlugin !== null) continue + + const releases = await GitHubClientService.getReleases(owner, name) + if (releases.length === 0) continue + + const createdPlugin: IPlugin | null = await PluginsService.createPlugin({ + approved: false, + name, + owner, + url: repository.html_url, + branch: repository.default_branch, + }) + + if (createdPlugin !== null) { + await DiscordClient.sendPluginApprovalRequest(createdPlugin) + } + } + } catch (error: unknown) { + console.error("GitHubSyncService.refreshDiscoveredPlugins failed:", error) + } finally { + this.isRefreshing = false + } + } + + private static async rebuildApprovedPluginsCache(): Promise { + const approvedPlugins: IPlugin[] = await PluginsService.getApprovedPlugins() + const enrichedPluginsSettled: Array = await Promise.all( + approvedPlugins.map((plugin) => GitHubPluginEnrichmentService.enrich(plugin)), + ) + + this.enrichedPlugins = enrichedPluginsSettled.filter((plugin): plugin is IEnrichedPlugin => plugin !== null) + await RedisClient.setValue( + this.ENRICHED_PLUGINS_CACHE_KEY, + this.enrichedPlugins, + Math.floor(this.CACHE_REBUILD_TIME_IN_MILLISECONDS / 1000), + ) + } +} + +export { GitHubSyncService } diff --git a/src/services/github/index.ts b/src/services/github/index.ts new file mode 100644 index 0000000..bad0b49 --- /dev/null +++ b/src/services/github/index.ts @@ -0,0 +1,3 @@ +export { GitHubClientService } from "./GitHubClientService" +export { GitHubPluginEnrichmentService } from "./GitHubPluginEnrichmentService" +export { GitHubSyncService } from "./GitHubSyncService" From 32d07ab090f0d6f09c7712cdf4c5a5e2db135194 Mon Sep 17 00:00:00 2001 From: Bonanoo <61703768+AnyBananaGAME@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:08:10 +0300 Subject: [PATCH 32/35] Fix typo in pluginsTable name --- src/globals/databases/plugins/PluginsSchemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globals/databases/plugins/PluginsSchemas.ts b/src/globals/databases/plugins/PluginsSchemas.ts index 8411eb5..ad283b9 100644 --- a/src/globals/databases/plugins/PluginsSchemas.ts +++ b/src/globals/databases/plugins/PluginsSchemas.ts @@ -4,7 +4,7 @@ import { boolean, pgTable, serial, varchar } from "drizzle-orm/pg-core" * @summary Plugins table. * @description Plugins table. */ -const pluginsTable = pgTable("plguins", { +const pluginsTable = pgTable("plugins", { id: serial("id").notNull().primaryKey(), approved: boolean("approved").notNull().default(false), name: varchar("name", { length: 255 }).notNull(), From 6fb5599eaafeb243bc219a2077067f0f491829a3 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Wed, 29 Apr 2026 14:18:11 +0200 Subject: [PATCH 33/35] chore(gitignore): removed temp directory --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index da68b8d..c2c988e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,3 @@ # Operating system files. /**/.DS_Store /**/*.ini - -# I'll delete it later. -/old_src/ From af6aac5326861a18f8a3c71bc67f9201e3585b82 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Wed, 29 Apr 2026 14:23:10 +0200 Subject: [PATCH 34/35] fix(ci): fixed CI test step --- .github/workflows/CI.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 46535d8..4654677 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -45,7 +45,7 @@ jobs: run: bun run format:check - name: Run tests. - run: bun run test + run: bun test - name: Build a production-ready application. run: bun run build From 344ea3b760eec0a09c085cbccffa5be808f333d2 Mon Sep 17 00:00:00 2001 From: Oliwier Date: Wed, 29 Apr 2026 15:11:02 +0200 Subject: [PATCH 35/35] feat(tests): extended an example test Co-Authored-By: Bonanoo <61703768+AnyBananaGAME@users.noreply.github.com> --- tests/ExampleTest.test.ts | 169 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 161 insertions(+), 8 deletions(-) diff --git a/tests/ExampleTest.test.ts b/tests/ExampleTest.test.ts index 511bab1..3a00cbb 100644 --- a/tests/ExampleTest.test.ts +++ b/tests/ExampleTest.test.ts @@ -1,9 +1,162 @@ -import { test, expect } from "bun:test" - -/** - * @summary Example test. - * @description Delete this entire file when you start writing your own tests. - */ -test("Example test.", (): void => { - expect(true).toBe(true) +import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test" +import type { Request, Response } from "express" +import { PluginsController } from "../src/controllers/plugins/PluginsController" +import { EnvironmentVariables } from "../src/globals/EnvironmentVariables" +import { GitHubSyncService } from "../src/services/github" + +describe("EnvironmentVariables.APP_PORT", () => { + const OriginalEnvironment = { ...process.env } + + beforeEach(() => { + process.env = { ...OriginalEnvironment } + }) + + afterEach(() => { + process.env = { ...OriginalEnvironment } + }) + + test("returns a parsed port number when APP_PORT is valid", () => { + process.env.APP_PORT = "3000" + expect(EnvironmentVariables.APP_PORT).toBe(3000) + }) + + test("throws when APP_PORT is missing", () => { + delete process.env.APP_PORT + expect(() => EnvironmentVariables.APP_PORT).toThrow("APP_PORT is not set.") + }) +}) + +describe("EnvironmentVariables.CACHE_ENGINE_ENABLED", () => { + const OriginalEnvironment = { ...process.env } + + beforeEach(() => { + process.env = { ...OriginalEnvironment } + }) + + afterEach(() => { + process.env = { ...OriginalEnvironment } + }) + + test("returns true for a true value", () => { + process.env.CACHE_ENGINE_ENABLED = "true" + expect(EnvironmentVariables.CACHE_ENGINE_ENABLED).toBe(true) + }) + + test("throws for invalid boolean values", () => { + process.env.CACHE_ENGINE_ENABLED = "enabled" + expect(() => EnvironmentVariables.CACHE_ENGINE_ENABLED).toThrow("CACHE_ENGINE_ENABLED is not a boolean.") + }) +}) + +describe("EnvironmentVariables.GITHUB_TOKEN", () => { + const OriginalEnvironment = { ...process.env } + + beforeEach(() => { + process.env = { ...OriginalEnvironment } + }) + + afterEach(() => { + process.env = { ...OriginalEnvironment } + }) + + test("returns undefined for whitespace-only token", () => { + process.env.GITHUB_TOKEN = " " + expect(EnvironmentVariables.GITHUB_TOKEN).toBeUndefined() + }) + + test("returns trimmed token value", () => { + process.env.GITHUB_TOKEN = " token-value " + expect(EnvironmentVariables.GITHUB_TOKEN).toBe("token-value") + }) +}) + +describe("PluginsController.getPluginsPagination", () => { + test("returns 400 when page query is invalid", async () => { + const RequestObject = { query: { page: "0" } } as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const CountSpy = spyOn(GitHubSyncService, "getPluginsCount") + const PageSpy = spyOn(GitHubSyncService, "getPluginsByPage") + + await PluginsController.getPluginsPagination(RequestObject, ResponseObject) + + expect(StatusSpy).toHaveBeenCalledWith(400) + expect(SendSpy).toHaveBeenCalledWith({ + message: "Query parameter `page` must be a positive integer.", + }) + expect(CountSpy).not.toHaveBeenCalled() + expect(PageSpy).not.toHaveBeenCalled() + CountSpy.mockRestore() + PageSpy.mockRestore() + }) + + test("returns count and paged items for a valid page", async () => { + const RequestObject = { query: { page: "2" } } as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const CountSpy = spyOn(GitHubSyncService, "getPluginsCount").mockResolvedValue(51) + const Items = [{ owner: "serenityjs", name: "sample-plugin" }] + const PageSpy = spyOn(GitHubSyncService, "getPluginsByPage").mockResolvedValue(Items as never) + + await PluginsController.getPluginsPagination(RequestObject, ResponseObject) + + expect(CountSpy).toHaveBeenCalledTimes(1) + expect(PageSpy).toHaveBeenCalledWith(2, 25) + expect(StatusSpy).toHaveBeenCalledWith(200) + expect(SendSpy).toHaveBeenCalledWith({ + count: 51, + items: Items, + }) + CountSpy.mockRestore() + PageSpy.mockRestore() + }) + + test("returns 500 when services fail for paginated plugins", async () => { + const RequestObject = { query: { page: "1" } } as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const CountSpy = spyOn(GitHubSyncService, "getPluginsCount").mockRejectedValue(new Error("database down")) + const PageSpy = spyOn(GitHubSyncService, "getPluginsByPage") + + await PluginsController.getPluginsPagination(RequestObject, ResponseObject) + + expect(StatusSpy).toHaveBeenCalledWith(500) + expect(SendSpy).toHaveBeenCalledWith({ message: "Failed to fetch paginated plugins." }) + CountSpy.mockRestore() + PageSpy.mockRestore() + }) +}) + +describe("PluginsController.getAllPlugins", () => { + test("returns all plugins on success", async () => { + const RequestObject = {} as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const Items = [{ owner: "serenityjs", name: "plugin-a" }] + const AllSpy = spyOn(GitHubSyncService, "getAllPlugins").mockResolvedValue(Items as never) + + await PluginsController.getAllPlugins(RequestObject, ResponseObject) + + expect(StatusSpy).toHaveBeenCalledWith(200) + expect(SendSpy).toHaveBeenCalledWith(Items) + AllSpy.mockRestore() + }) + + test("returns 500 when listing all plugins fails", async () => { + const RequestObject = {} as unknown as Request + const StatusSpy = mock(() => ResponseObject) + const SendSpy = mock(() => ResponseObject) + const ResponseObject = { status: StatusSpy, send: SendSpy } as unknown as Response + const AllSpy = spyOn(GitHubSyncService, "getAllPlugins").mockRejectedValue(new Error("network error")) + + await PluginsController.getAllPlugins(RequestObject, ResponseObject) + + expect(StatusSpy).toHaveBeenCalledWith(500) + expect(SendSpy).toHaveBeenCalledWith({ message: "Failed to fetch all plugins." }) + AllSpy.mockRestore() + }) })