From cab46075fd840ff8020cab01f0d0d78f4c781d7e Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Thu, 30 Oct 2025 20:46:27 +0100 Subject: [PATCH 01/21] chore: add plan for database --- ...abase-implementation-plan-5ca2c206.plan.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .cursor/plans/database-implementation-plan-5ca2c206.plan.md diff --git a/.cursor/plans/database-implementation-plan-5ca2c206.plan.md b/.cursor/plans/database-implementation-plan-5ca2c206.plan.md new file mode 100644 index 0000000..4ad23a9 --- /dev/null +++ b/.cursor/plans/database-implementation-plan-5ca2c206.plan.md @@ -0,0 +1,65 @@ + +# SQLite Database Implementation for Discord Moderation Tool + +## Overview + +This plan implements a comprehensive SQLite database system to track moderation actions, user history, and enable appeals and analytics functionality. + +## Key Files & Structure + +Create the following file structure: + +``` +src/ +├── database/ +│ ├── index.ts # Database connection & initialization +│ ├── models/ # TypeScript models +│ ├── migrations/ # Database migration files +│ └── operations/ # CRUD operations +└── types/ + └── database.ts # Database type definitions +``` + +## Database Schema + +**Three main tables:** + +- `users` - Store user info (Discord ID, username, etc.) +- `actions` - Store moderation actions with metadata +- `action_types` - Define available action types (warning, mute, ban, etc.) + +## Implementation Approach + +Work through the following phases in order: + +1. **Database Setup**: Install `better-sqlite3` and create database connection module +2. **Schema Creation**: Implement migrations for users, actions, and action_types tables +3. **TypeScript Integration**: Create type definitions and model classes +4. **CRUD Operations**: Implement database operations for users and actions +5. **Command Integration**: Update existing moderation commands to log actions +6. **Advanced Features**: Add appeals system and analytics +7. **Testing**: Write tests and optimize performance + +## Key Integration Points + +- Modify existing commands in `src/commands/` to log actions +- Update `env.ts` for database configuration +- Add database initialization to bot startup + +## Benefits + +- Complete audit trail of moderation actions +- Track repeat offenders and patterns +- Enable appeals system +- Generate moderation analytics +- Maintain compliance records + +### To-dos + +- [ ] Install better-sqlite3 dependencies and create database connection module +- [ ] Create migration files for users, actions, and action_types tables +- [ ] Define TypeScript interfaces and models for database entities +- [ ] Implement CRUD operations for users and actions +- [ ] Update existing moderation commands to log actions to database +- [ ] Implement appeals system and analytics queries +- [ ] Write tests and optimize database performance \ No newline at end of file From a5bc3d84d0140fb77a514718d7c77a14bd7aed89 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 1 Nov 2025 17:06:16 +0100 Subject: [PATCH 02/21] feat: introduce prisma --- .gitignore | 3 +- package.json | 3 + pnpm-lock.yaml | 564 ++++++++++++++++++ prisma.config.ts | 12 + .../20251031193446_init/migration.sql | 8 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 17 + script.ts | 25 + 8 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/20251031193446_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 script.ts diff --git a/.gitignore b/.gitignore index 4ac265d..60795e1 100644 --- a/.gitignore +++ b/.gitignore @@ -167,4 +167,5 @@ tmp/ .env.production.local # esbuild metafile -meta.json \ No newline at end of file +meta.json +/generated/prisma diff --git a/package.json b/package.json index 37a8b15..9aa0550 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "license": "MIT", "packageManager": "pnpm@10.17.1", "dependencies": { + "@prisma/adapter-better-sqlite3": "^6.18.0", + "@prisma/client": "^6.18.0", "discord.js": "^14.22.1" }, "devDependencies": { @@ -27,6 +29,7 @@ "@types/node": "^24.5.2", "husky": "^9.1.7", "lint-staged": "^16.2.1", + "prisma": "^6.18.0", "tsx": "^4.20.6", "typescript": "^5.9.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a621689..2ca1e7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@prisma/adapter-better-sqlite3': + specifier: ^6.18.0 + version: 6.18.0 + '@prisma/client': + specifier: ^6.18.0 + version: 6.18.0(prisma@6.18.0(typescript@5.9.2))(typescript@5.9.2) discord.js: specifier: ^14.22.1 version: 14.22.1 @@ -24,6 +30,9 @@ importers: lint-staged: specifier: ^16.2.1 version: 16.2.1 + prisma: + specifier: ^6.18.0 + version: 6.18.0(typescript@5.9.2) tsx: specifier: ^4.20.6 version: 4.20.6 @@ -270,6 +279,42 @@ packages: cpu: [x64] os: [win32] + '@prisma/adapter-better-sqlite3@6.18.0': + resolution: {integrity: sha512-YBok05ezVdJ0j62bkvVEb7Pf1jkglOXpWgBzU+lolZg+131cD3gSwTWoiNRmA+74HI6dHhOjzpsod50DiMoI2w==} + + '@prisma/client@6.18.0': + resolution: {integrity: sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.18.0': + resolution: {integrity: sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==} + + '@prisma/debug@6.18.0': + resolution: {integrity: sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==} + + '@prisma/driver-adapter-utils@6.18.0': + resolution: {integrity: sha512-9wgSriEKs4j1ePxlv1/RNfJV9Gu5rzG37Neshg+DfrCcUY3amroERvTjyR04w5J1THdGdOTgGL9VdJcVaKRMmQ==} + + '@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f': + resolution: {integrity: sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==} + + '@prisma/engines@6.18.0': + resolution: {integrity: sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==} + + '@prisma/fetch-engine@6.18.0': + resolution: {integrity: sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==} + + '@prisma/get-platform@6.18.0': + resolution: {integrity: sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==} + '@sapphire/async-queue@1.5.5': resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -282,6 +327,9 @@ packages: resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@types/node@24.5.2': resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} @@ -304,10 +352,43 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@11.10.0: + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -323,6 +404,35 @@ packages: resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} engines: {node: '>=20'} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + discord-api-types@0.38.26: resolution: {integrity: sha512-xpmPviHjIJ6dFu1eNwNDIGQ3N6qmPUUYFVAx/YZ64h7ZgPkTcKjnciD8bZe8Vbeji7yS5uYljyciunpq0J5NSw==} @@ -330,9 +440,23 @@ packages: resolution: {integrity: sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w==} engines: {node: '>=18'} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + effect@3.18.4: + resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} + emoji-regex@10.5.0: resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -345,13 +469,30 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -364,11 +505,27 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} @@ -377,6 +534,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + lint-staged@16.2.1: resolution: {integrity: sha512-KMeYmH9wKvHsXdUp+z6w7HN3fHKHXwT1pSTQTYxB9kI6ekK1rlL3kLZEoXZCppRPXFK9PFW/wfQctV7XUqMrPQ==} engines: {node: '>=20.17'} @@ -407,14 +568,51 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + nano-spawn@1.0.3: resolution: {integrity: sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==} engines: {node: '>=20.17'} + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.80.0: + resolution: {integrity: sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==} + engines: {node: '>=10'} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + nypm@0.6.2: + resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -424,6 +622,45 @@ packages: engines: {node: '>=0.10'} hasBin: true + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + prisma@6.18.0: + resolution: {integrity: sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -434,10 +671,24 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} @@ -454,10 +705,27 @@ packages: resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} engines: {node: '>=20'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@7.1.2: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -473,6 +741,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -485,10 +756,16 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -668,6 +945,50 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true + '@prisma/adapter-better-sqlite3@6.18.0': + dependencies: + '@prisma/driver-adapter-utils': 6.18.0 + better-sqlite3: 11.10.0 + + '@prisma/client@6.18.0(prisma@6.18.0(typescript@5.9.2))(typescript@5.9.2)': + optionalDependencies: + prisma: 6.18.0(typescript@5.9.2) + typescript: 5.9.2 + + '@prisma/config@6.18.0': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.18.4 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@6.18.0': {} + + '@prisma/driver-adapter-utils@6.18.0': + dependencies: + '@prisma/debug': 6.18.0 + + '@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f': {} + + '@prisma/engines@6.18.0': + dependencies: + '@prisma/debug': 6.18.0 + '@prisma/engines-version': 6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f + '@prisma/fetch-engine': 6.18.0 + '@prisma/get-platform': 6.18.0 + + '@prisma/fetch-engine@6.18.0': + dependencies: + '@prisma/debug': 6.18.0 + '@prisma/engines-version': 6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f + '@prisma/get-platform': 6.18.0 + + '@prisma/get-platform@6.18.0': + dependencies: + '@prisma/debug': 6.18.0 + '@sapphire/async-queue@1.5.5': {} '@sapphire/shapeshift@4.0.0': @@ -677,6 +998,8 @@ snapshots: '@sapphire/snowflake@3.5.3': {} + '@standard-schema/spec@1.0.0': {} + '@types/node@24.5.2': dependencies: undici-types: 7.12.0 @@ -695,10 +1018,57 @@ snapshots: ansi-styles@6.2.3: {} + base64-js@1.5.1: {} + + better-sqlite3@11.10.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@1.1.4: {} + + citty@0.1.6: + dependencies: + consola: 3.4.2 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -712,6 +1082,24 @@ snapshots: commander@14.0.1: {} + confbox@0.2.2: {} + + consola@3.4.2: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + deepmerge-ts@7.1.5: {} + + defu@6.1.4: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + discord-api-types@0.38.26: {} discord.js@14.22.1: @@ -733,8 +1121,21 @@ snapshots: - bufferutil - utf-8-validate + dotenv@16.6.1: {} + + effect@3.18.4: + dependencies: + '@standard-schema/spec': 1.0.0 + fast-check: 3.23.2 + emoji-regex@10.5.0: {} + empathic@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + environment@1.1.0: {} esbuild@0.25.10: @@ -768,12 +1169,24 @@ snapshots: eventemitter3@5.0.1: {} + expand-template@2.0.3: {} + + exsolve@1.0.7: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + fs-constants@1.0.0: {} + fsevents@2.3.3: optional: true @@ -783,14 +1196,33 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.2 + pathe: 2.0.3 + + github-from-package@0.0.0: {} + husky@9.1.7: {} + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + is-fullwidth-code-point@5.1.0: dependencies: get-east-asian-width: 1.4.0 is-number@7.0.0: {} + jiti@2.6.1: {} + lint-staged@16.2.1: dependencies: commander: 14.0.1 @@ -831,16 +1263,105 @@ snapshots: mimic-function@5.0.1: {} + mimic-response@3.1.0: {} + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + nano-spawn@1.0.3: {} + napi-build-utils@2.0.0: {} + + node-abi@3.80.0: + dependencies: + semver: 7.7.3 + + node-fetch-native@1.6.7: {} + + nypm@0.6.2: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + pathe: 2.0.3 + pkg-types: 2.3.0 + tinyexec: 1.0.1 + + ohash@2.0.11: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + picomatch@2.3.1: {} pidtree@0.6.0: {} + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.80.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + prisma@6.18.0(typescript@5.9.2): + dependencies: + '@prisma/config': 6.18.0 + '@prisma/engines': 6.18.0 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - magicast + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pure-rand@6.1.0: {} + + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + resolve-pkg-maps@1.0.0: {} restore-cursor@5.1.0: @@ -850,8 +1371,20 @@ snapshots: rfdc@1.4.1: {} + safe-buffer@5.2.1: {} + + semver@7.7.3: {} + signal-exit@4.1.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -870,10 +1403,33 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@7.1.2: dependencies: ansi-regex: 6.2.2 + strip-json-comments@2.0.1: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tinyexec@1.0.1: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -889,18 +1445,26 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + typescript@5.9.2: {} undici-types@7.12.0: {} undici@6.21.3: {} + util-deprecate@1.0.2: {} + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 string-width: 7.2.0 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + ws@8.18.3: {} yaml@2.8.1: {} diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..e05563c --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + engine: "classic", + datasource: { + url: "file:./dev.db", + }, +}); diff --git a/prisma/migrations/20251031193446_init/migration.sql b/prisma/migrations/20251031193446_init/migration.sql new file mode 100644 index 0000000..6a776a6 --- /dev/null +++ b/prisma/migrations/20251031193446_init/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "discordId" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_discordId_key" ON "User"("discordId"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..951f8a5 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,17 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client" + output = "../generated/prisma" +} + +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model User { + id Int @id @default(autoincrement()) + discordId String @unique +} \ No newline at end of file diff --git a/script.ts b/script.ts new file mode 100644 index 0000000..733394d --- /dev/null +++ b/script.ts @@ -0,0 +1,25 @@ +import { PrismaClient } from "./generated/prisma/client.js"; + +const prisma = new PrismaClient(); + +async function main() { + const user = await prisma.user.create({ + data: { + discordId: "883932482734", + }, + }); + console.log(user); + + const users = await prisma.user.findMany(); + console.log(users); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); From c67daa1026c2e639403d63646be3c5edf0e2d732 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 1 Nov 2025 17:56:12 +0100 Subject: [PATCH 03/21] docs: implement SQLite database with Prisma for moderation actions tracking --- ...abase-implementation-plan-5ca2c206.plan.md | 65 --- ...abase-implementation-plan-b7b83c34.plan.md | 479 ++++++++++++++++++ 2 files changed, 479 insertions(+), 65 deletions(-) delete mode 100644 .cursor/plans/database-implementation-plan-5ca2c206.plan.md create mode 100644 .cursor/plans/database-implementation-plan-b7b83c34.plan.md diff --git a/.cursor/plans/database-implementation-plan-5ca2c206.plan.md b/.cursor/plans/database-implementation-plan-5ca2c206.plan.md deleted file mode 100644 index 4ad23a9..0000000 --- a/.cursor/plans/database-implementation-plan-5ca2c206.plan.md +++ /dev/null @@ -1,65 +0,0 @@ - -# SQLite Database Implementation for Discord Moderation Tool - -## Overview - -This plan implements a comprehensive SQLite database system to track moderation actions, user history, and enable appeals and analytics functionality. - -## Key Files & Structure - -Create the following file structure: - -``` -src/ -├── database/ -│ ├── index.ts # Database connection & initialization -│ ├── models/ # TypeScript models -│ ├── migrations/ # Database migration files -│ └── operations/ # CRUD operations -└── types/ - └── database.ts # Database type definitions -``` - -## Database Schema - -**Three main tables:** - -- `users` - Store user info (Discord ID, username, etc.) -- `actions` - Store moderation actions with metadata -- `action_types` - Define available action types (warning, mute, ban, etc.) - -## Implementation Approach - -Work through the following phases in order: - -1. **Database Setup**: Install `better-sqlite3` and create database connection module -2. **Schema Creation**: Implement migrations for users, actions, and action_types tables -3. **TypeScript Integration**: Create type definitions and model classes -4. **CRUD Operations**: Implement database operations for users and actions -5. **Command Integration**: Update existing moderation commands to log actions -6. **Advanced Features**: Add appeals system and analytics -7. **Testing**: Write tests and optimize performance - -## Key Integration Points - -- Modify existing commands in `src/commands/` to log actions -- Update `env.ts` for database configuration -- Add database initialization to bot startup - -## Benefits - -- Complete audit trail of moderation actions -- Track repeat offenders and patterns -- Enable appeals system -- Generate moderation analytics -- Maintain compliance records - -### To-dos - -- [ ] Install better-sqlite3 dependencies and create database connection module -- [ ] Create migration files for users, actions, and action_types tables -- [ ] Define TypeScript interfaces and models for database entities -- [ ] Implement CRUD operations for users and actions -- [ ] Update existing moderation commands to log actions to database -- [ ] Implement appeals system and analytics queries -- [ ] Write tests and optimize database performance \ No newline at end of file diff --git a/.cursor/plans/database-implementation-plan-b7b83c34.plan.md b/.cursor/plans/database-implementation-plan-b7b83c34.plan.md new file mode 100644 index 0000000..8a56635 --- /dev/null +++ b/.cursor/plans/database-implementation-plan-b7b83c34.plan.md @@ -0,0 +1,479 @@ + +# Database Implementation for Discord Moderation Tool + +## Overview + +Implement SQLite database with Prisma to track all moderation actions (warn, mute, unmute, kick, ban, unban, timeout, remove_timeout, repel) with full audit history and error correction capabilities. This plan focuses solely on database setup and operations - Discord bot command integration will be handled separately. + +## Current State + +- ✅ Prisma installed (`@prisma/client`, `@prisma/adapter-better-sqlite3`) +- ✅ Basic User model in `prisma/schema.prisma` +- ✅ Docker setup complete with profiles (dev/prod) +- ✅ Volume persistence configured (`./data:/app/data`) + +## Exact Database Schema + +Replace `prisma/schema.prisma` with: + +```prisma +generator client { + provider = "prisma-client-js" + output = "../generated/prisma" +} + +datasource db { + provider = "sqlite" + url = "file:../data/dev.db" +} + +model User { + id Int @id @default(autoincrement()) + discordId String @unique + + // Relations + actionsReceived ModerationAction[] @relation("ActionTarget") + actionsPerformed ModerationAction[] @relation("ActionModerator") + + @@index([discordId]) +} + +enum ActionType { + WARN + MUTE + UNMUTE + KICK + BAN + UNBAN + TIMEOUT + REMOVE_TIMEOUT + REPEL +} + +enum ActionStatus { + ACTIVE + EXPIRED + REMOVED_BY_ERROR + REVERSED +} + +model ModerationAction { + id Int @id @default(autoincrement()) + type ActionType + status ActionStatus @default(ACTIVE) + reason String? + duration Int? // Duration in seconds for timeouts/mutes + + // User relationships + targetId Int + target User @relation("ActionTarget", fields: [targetId], references: [id]) + moderatorId Int + moderator User @relation("ActionModerator", fields: [moderatorId], references: [id]) + + // Timestamps + createdAt DateTime @default(now()) + expiresAt DateTime? + + // For corrections/reversals + parentActionId Int? + parentAction ModerationAction? @relation("ActionCorrections", fields: [parentActionId], references: [id]) + corrections ModerationAction[] @relation("ActionCorrections") + + @@index([targetId]) + @@index([moderatorId]) + @@index([status]) + @@index([createdAt]) +} +``` + +## Database Operations Module + +Create `src/database/operations.ts`: + +```typescript +import { PrismaClient, ActionType, ActionStatus } from "../../generated/prisma/index.js"; + +export const prisma = new PrismaClient(); + +// Connect to database and verify connection +export async function connectDatabase() { + try { + await prisma.$connect(); + // Test query to verify connection + await prisma.user.findFirst(); + console.log("✅ Database connected successfully"); + } catch (error) { + console.error("❌ Database connection failed:", error); + throw error; + } +} + +// Disconnect from database +export async function disconnectDatabase() { + await prisma.$disconnect(); + console.log("Database disconnected"); +} + +// User operations - only Discord ID needed +export async function upsertUser(discordId: string) { + return await prisma.user.upsert({ + where: { discordId }, + update: {}, + create: { discordId }, + }); +} + +export async function getUserByDiscordId(discordId: string) { + return await prisma.user.findUnique({ + where: { discordId }, + }); +} + +// Moderation action operations +type CreateActionParams = { + type: ActionType; + targetDiscordId: string; + moderatorDiscordId: string; + reason?: string; + duration?: number; +}; + +export async function createModerationAction(params: CreateActionParams) { + // Ensure both users exist + const target = await upsertUser(params.targetDiscordId); + const moderator = await upsertUser(params.moderatorDiscordId); + + // Calculate expiry for timed actions + const expiresAt = params.duration + ? new Date(Date.now() + params.duration * 1000) + : null; + + return await prisma.moderationAction.create({ + data: { + type: params.type, + reason: params.reason, + duration: params.duration, + targetId: target.id, + moderatorId: moderator.id, + expiresAt, + }, + include: { + target: true, + moderator: true, + }, + }); +} + +export async function removeActionByError( + actionId: number, + removedByDiscordId: string, + reason: string +) { + const removedBy = await upsertUser(removedByDiscordId); + + // Mark original action as removed + const originalAction = await prisma.moderationAction.update({ + where: { id: actionId }, + data: { status: ActionStatus.REMOVED_BY_ERROR }, + }); + + // Create correction record + return await prisma.moderationAction.create({ + data: { + type: originalAction.type, + status: ActionStatus.REMOVED_BY_ERROR, + reason: `Correction: ${reason}`, + targetId: originalAction.targetId, + moderatorId: removedBy.id, + parentActionId: originalAction.id, + }, + include: { + parentAction: true, + }, + }); +} + +export async function getUserActions(discordId: string, limit = 50) { + const user = await getUserByDiscordId(discordId); + if (!user) return []; + + return await prisma.moderationAction.findMany({ + where: { targetId: user.id }, + include: { + moderator: true, + }, + orderBy: { createdAt: 'desc' }, + take: limit, + }); +} + +export async function getActiveActions(discordId: string) { + const user = await getUserByDiscordId(discordId); + if (!user) return []; + + return await prisma.moderationAction.findMany({ + where: { + targetId: user.id, + status: ActionStatus.ACTIVE, + }, + include: { + moderator: true, + }, + }); +} + +export async function getActionById(actionId: number) { + return await prisma.moderationAction.findUnique({ + where: { id: actionId }, + include: { + target: true, + moderator: true, + parentAction: true, + }, + }); +} + +export async function updateActionStatus(actionId: number, status: ActionStatus) { + return await prisma.moderationAction.update({ + where: { id: actionId }, + data: { status }, + }); +} +``` + +## Analytics Module + +Create `src/database/analytics.ts`: + +```typescript +import { prisma } from "./operations.js"; +import { ActionType, ActionStatus } from "../../generated/prisma/index.js"; + +export async function getUserActionStats(discordId: string) { + const user = await prisma.user.findUnique({ + where: { discordId }, + include: { + actionsReceived: { + where: { + status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] } + } + } + } + }); + + if (!user) return null; + + const actionsByType = user.actionsReceived.reduce((acc, action) => { + acc[action.type] = (acc[action.type] || 0) + 1; + return acc; + }, {} as Record); + + return { + discordId: user.discordId, + totalActions: user.actionsReceived.length, + actionsByType, + }; +} + +export async function getModeratorStats(discordId: string, days = 30) { + const user = await prisma.user.findUnique({ + where: { discordId }, + }); + + if (!user) return null; + + const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000); + + const actions = await prisma.moderationAction.findMany({ + where: { + moderatorId: user.id, + createdAt: { gte: since }, + status: { not: ActionStatus.REMOVED_BY_ERROR } + } + }); + + const actionsByType = actions.reduce((acc, action) => { + acc[action.type] = (acc[action.type] || 0) + 1; + return acc; + }, {} as Record); + + return { + discordId: user.discordId, + totalActions: actions.length, + actionsByType, + period: `${days} days`, + }; +} + +export async function getRecentActions(limit = 20) { + return await prisma.moderationAction.findMany({ + include: { + target: true, + moderator: true, + }, + orderBy: { createdAt: 'desc' }, + take: limit, + }); +} + +export async function findRepeatOffenders(minActions = 3) { + const actions = await prisma.moderationAction.findMany({ + where: { + status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] } + }, + include: { + target: true, + } + }); + + const userActionCounts = actions.reduce((acc, action) => { + const discordId = action.target.discordId; + if (!acc[discordId]) { + acc[discordId] = { discordId, count: 0, actions: [] }; + } + acc[discordId].count++; + acc[discordId].actions.push(action); + return acc; + }, {} as Record); + + return Object.values(userActionCounts) + .filter(item => item.count >= minActions) + .sort((a, b) => b.count - a.count); +} + +export async function getActionsByType(actionType: ActionType, limit = 50) { + return await prisma.moderationAction.findMany({ + where: { type: actionType }, + include: { + target: true, + moderator: true, + }, + orderBy: { createdAt: 'desc' }, + take: limit, + }); +} + +export async function getTotalActionCount() { + return await prisma.moderationAction.count({ + where: { + status: { not: ActionStatus.REMOVED_BY_ERROR } + } + }); +} +``` + +## Initialize Database in Main + +Update `index.ts` to initialize Prisma with explicit connection: + +```typescript +import { connectDatabase, disconnectDatabase } from "./src/database/operations.js"; + +// ... existing imports and code ... + +// Connect to database before starting bot +await connectDatabase(); + +client.once(Events.ClientReady, (readyClient) => { + console.log(`Ready! Logged in as ${readyClient.user.tag}`); +}); + +// Graceful shutdown +process.on('SIGINT', async () => { + console.log("Shutting down..."); + await disconnectDatabase(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log("Shutting down..."); + await disconnectDatabase(); + process.exit(0); +}); +``` + +## Implementation Steps + +1. **Update Schema**: Replace `prisma/schema.prisma` with new schema +2. **Run Migration**: `pnpm prisma migrate dev --name add_moderation_tables` +3. **Generate Client**: `pnpm prisma generate` +4. **Create Operations**: Add `src/database/operations.ts` with all CRUD functions +5. **Create Analytics**: Add `src/database/analytics.ts` with analytics queries +6. **Initialize Database**: Update `index.ts` with connectDatabase/disconnectDatabase calls +7. **Test Connection**: Run bot and verify database connection logs + +## Volume Management + +Database location: `./data/dev.db` + +```bash +# Run development +docker-compose --profile dev up + +# Run production +docker-compose --profile prod up -d + +# Backup database +cp ./data/dev.db ./backup_$(date +%Y%m%d).db + +# Restore database +cp ./backup_20250101.db ./data/dev.db + +# View database with sqlite3 +sqlite3 ./data/dev.db + +# View all tables +sqlite3 ./data/dev.db ".tables" + +# View schema +sqlite3 ./data/dev.db ".schema" +``` + +## Files to Create + +- `src/database/operations.ts` - Database CRUD operations (Discord ID only) +- `src/database/analytics.ts` - Analytics and reporting queries + +## Files to Modify + +- `prisma/schema.prisma` - Complete schema definition +- `index.ts` - Initialize database connection with explicit connect/disconnect + +## Usage Examples + +After implementation, Discord commands can use the database operations: + +```typescript +// Create a ban action +await createModerationAction({ + type: ActionType.BAN, + targetDiscordId: "123456789", + moderatorDiscordId: "987654321", + reason: "Spam", +}); + +// Get user's action history +const actions = await getUserActions("123456789"); + +// Get user statistics +const stats = await getUserActionStats("123456789"); + +// Find repeat offenders +const offenders = await findRepeatOffenders(3); +``` + +### To-dos + +- [ ] Update prisma/schema.prisma with User, ActionType, ActionStatus, and ModerationAction models +- [ ] Create and run Prisma migration: pnpm prisma migrate dev --name add_moderation_tables +- [ ] Create src/database/operations.ts with CRUD functions for users and moderation actions +- [ ] Create src/database/analytics.ts with analytics query helpers +- [ ] Update index.ts to initialize Prisma client and handle graceful shutdown +- [ ] Create src/commands/ban/index.ts command with database logging +- [ ] Create src/commands/kick/index.ts command with database logging +- [ ] Create src/commands/warn/index.ts command with database logging +- [ ] Create src/commands/timeout/index.ts command with database logging +- [ ] Create src/commands/repel/index.ts command with database logging +- [ ] Create src/commands/remove-action/index.ts for correcting errors +- [ ] Create src/commands/history/index.ts to view user moderation history +- [ ] Update src/commands/index.ts to register all new moderation commands \ No newline at end of file From e3ce6a0380c863e8d6feb038e3ea80c7c2895aeb Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 1 Nov 2025 19:47:54 +0100 Subject: [PATCH 04/21] feat: add moderation tables and update schema for user actions tracking --- .../migration.sql | 31 ++++++++++ prisma/schema.prisma | 61 +++++++++++++++++-- 2 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 prisma/migrations/20251101170231_add_moderation_tables/migration.sql diff --git a/prisma/migrations/20251101170231_add_moderation_tables/migration.sql b/prisma/migrations/20251101170231_add_moderation_tables/migration.sql new file mode 100644 index 0000000..90971f1 --- /dev/null +++ b/prisma/migrations/20251101170231_add_moderation_tables/migration.sql @@ -0,0 +1,31 @@ +-- CreateTable +CREATE TABLE "ModerationAction" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "type" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "reason" TEXT, + "duration" INTEGER, + "targetId" INTEGER NOT NULL, + "moderatorId" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" DATETIME, + "parentActionId" INTEGER, + CONSTRAINT "ModerationAction_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ModerationAction_moderatorId_fkey" FOREIGN KEY ("moderatorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ModerationAction_parentActionId_fkey" FOREIGN KEY ("parentActionId") REFERENCES "ModerationAction" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateIndex +CREATE INDEX "ModerationAction_targetId_idx" ON "ModerationAction"("targetId"); + +-- CreateIndex +CREATE INDEX "ModerationAction_moderatorId_idx" ON "ModerationAction"("moderatorId"); + +-- CreateIndex +CREATE INDEX "ModerationAction_status_idx" ON "ModerationAction"("status"); + +-- CreateIndex +CREATE INDEX "ModerationAction_createdAt_idx" ON "ModerationAction"("createdAt"); + +-- CreateIndex +CREATE INDEX "User_discordId_idx" ON "User"("discordId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 951f8a5..d7bf2ee 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2,16 +2,69 @@ // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client" + provider = "prisma-client-js" output = "../generated/prisma" } datasource db { provider = "sqlite" - url = "file:./dev.db" + url = "file:../data/moderation.db" } model User { - id Int @id @default(autoincrement()) - discordId String @unique + id Int @id @default(autoincrement()) + discordId String @unique + + // Relations + actionsReceived ModerationAction[] @relation("ActionTarget") + actionsPerformed ModerationAction[] @relation("ActionModerator") + + @@index([discordId]) +} + +enum ActionType { + WARN + MUTE + UNMUTE + KICK + BAN + UNBAN + TIMEOUT + REMOVE_TIMEOUT + REPEL +} + +enum ActionStatus { + ACTIVE + EXPIRED + REMOVED_BY_ERROR + REVERSED +} + +model ModerationAction { + id Int @id @default(autoincrement()) + type ActionType + status ActionStatus @default(ACTIVE) + reason String? + duration Int? // Duration in seconds for timeouts/mutes + + // User relationships + targetId Int + target User @relation("ActionTarget", fields: [targetId], references: [id]) + moderatorId Int + moderator User @relation("ActionModerator", fields: [moderatorId], references: [id]) + + // Timestamps + createdAt DateTime @default(now()) + expiresAt DateTime? + + // For corrections/reversals + parentActionId Int? + parentAction ModerationAction? @relation("ActionCorrections", fields: [parentActionId], references: [id]) + corrections ModerationAction[] @relation("ActionCorrections") + + @@index([targetId]) + @@index([moderatorId]) + @@index([status]) + @@index([createdAt]) } \ No newline at end of file From f9fd99b3e9247768e3dd263b11b2a1ec8adc6408 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 1 Nov 2025 19:48:02 +0100 Subject: [PATCH 05/21] feat: implement database connection and graceful shutdown in bot --- src/index.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/index.ts b/src/index.ts index 0867be7..eb590a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,29 @@ import { Client, GatewayIntentBits } from "discord.js"; +import { connectDatabase, disconnectDatabase } from "./database/operations.js"; import { config } from "./env.js"; import { loadCommands, registerCommands } from "./utils/commands.js"; import { loadEvents } from "./utils/events.js"; const client = new Client({ intents: [GatewayIntentBits.Guilds] }); +// Connect to database before starting bot +await connectDatabase(); + loadCommands(client); loadEvents(client); registerCommands(); +// Graceful shutdown +process.on("SIGINT", async () => { + console.log("Shutting down..."); + await disconnectDatabase(); + process.exit(0); +}); + +process.on("SIGTERM", async () => { + console.log("Shutting down..."); + await disconnectDatabase(); + process.exit(0); +}); + client.login(config.discord.token); From bea8234133b113e6d07966ccbc6a0aa96b81d4f7 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 1 Nov 2025 19:50:16 +0100 Subject: [PATCH 06/21] feat: add analytics and operations modules for user and moderation action management --- src/database/analytics.ts | 131 ++++++++++++++++++++++++++++++++ src/database/operations.ts | 150 +++++++++++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 src/database/analytics.ts create mode 100644 src/database/operations.ts diff --git a/src/database/analytics.ts b/src/database/analytics.ts new file mode 100644 index 0000000..13530e1 --- /dev/null +++ b/src/database/analytics.ts @@ -0,0 +1,131 @@ +import { + ActionStatus, + type ActionType, + type ModerationAction, +} from "../../generated/prisma/index.js"; +import { prisma } from "./operations.js"; + +export async function getUserActionStats(discordId: string) { + const user = await prisma.user.findUnique({ + where: { discordId }, + include: { + actionsReceived: { + where: { + status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] }, + }, + }, + }, + }); + + if (!user) { + return null; + } + + const actionsByType = user.actionsReceived.reduce( + (acc, action) => { + acc[action.type] = (acc[action.type] || 0) + 1; + return acc; + }, + {} as Record + ); + + return { + discordId: user.discordId, + totalActions: user.actionsReceived.length, + actionsByType, + }; +} + +export async function getModeratorStats(discordId: string, days = 30) { + const user = await prisma.user.findUnique({ + where: { discordId }, + }); + + if (!user) { + return null; + } + + const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000); + + const actions = await prisma.moderationAction.findMany({ + where: { + moderatorId: user.id, + createdAt: { gte: since }, + status: { not: ActionStatus.REMOVED_BY_ERROR }, + }, + }); + + const actionsByType = actions.reduce( + (acc, action) => { + acc[action.type] = (acc[action.type] || 0) + 1; + return acc; + }, + {} as Record + ); + + return { + discordId: user.discordId, + totalActions: actions.length, + actionsByType, + period: `${days} days`, + }; +} + +export async function getRecentActions(limit = 20) { + return await prisma.moderationAction.findMany({ + include: { + target: true, + moderator: true, + }, + orderBy: { createdAt: "desc" }, + take: limit, + }); +} + +export async function findRepeatOffenders(minActions = 3) { + const actions = await prisma.moderationAction.findMany({ + where: { + status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] }, + }, + include: { + target: true, + }, + }); + + const userActionCounts = actions.reduce( + (acc, action) => { + const discordId = action.target.discordId; + if (!acc[discordId]) { + acc[discordId] = { discordId, count: 0, actions: [] }; + } + acc[discordId].count++; + acc[discordId].actions.push(action); + return acc; + }, + {} as Record + ); + + return Object.values(userActionCounts) + .filter((item) => item.count >= minActions) + .sort((a, b) => b.count - a.count); +} + +export async function getActionsByType(actionType: ActionType, limit = 50) { + return await prisma.moderationAction.findMany({ + where: { type: actionType }, + include: { + target: true, + moderator: true, + }, + orderBy: { createdAt: "desc" }, + take: limit, + }); +} + +export async function getTotalActionCount() { + return await prisma.moderationAction.count({ + where: { + status: { not: ActionStatus.REMOVED_BY_ERROR }, + }, + }); +} diff --git a/src/database/operations.ts b/src/database/operations.ts new file mode 100644 index 0000000..11e63b9 --- /dev/null +++ b/src/database/operations.ts @@ -0,0 +1,150 @@ +import { ActionStatus, type ActionType, PrismaClient } from "../../generated/prisma/index.js"; + +export const prisma = new PrismaClient(); + +// Connect to database and verify connection +export async function connectDatabase() { + try { + await prisma.$connect(); + // Test query to verify connection + await prisma.user.findFirst(); + console.log("✅ Database connected successfully"); + } catch (error) { + console.error("❌ Database connection failed:", error); + throw error; + } +} + +// Disconnect from database +export async function disconnectDatabase() { + await prisma.$disconnect(); + console.log("Database disconnected"); +} + +// User operations - only Discord ID needed +export async function upsertUser(discordId: string) { + return await prisma.user.upsert({ + where: { discordId }, + update: {}, + create: { discordId }, + }); +} + +export async function getUserByDiscordId(discordId: string) { + return await prisma.user.findUnique({ + where: { discordId }, + }); +} + +// Moderation action operations +type CreateActionParams = { + type: ActionType; + targetDiscordId: string; + moderatorDiscordId: string; + reason?: string; + duration?: number; +}; + +export async function createModerationAction(params: CreateActionParams) { + // Ensure both users exist + const target = await upsertUser(params.targetDiscordId); + const moderator = await upsertUser(params.moderatorDiscordId); + + // Calculate expiry for timed actions + const expiresAt = params.duration ? new Date(Date.now() + params.duration * 1000) : null; + + return await prisma.moderationAction.create({ + data: { + type: params.type, + reason: params.reason, + duration: params.duration, + targetId: target.id, + moderatorId: moderator.id, + expiresAt, + }, + include: { + target: true, + moderator: true, + }, + }); +} + +export async function removeActionByError( + actionId: number, + removedByDiscordId: string, + reason: string +) { + const removedBy = await upsertUser(removedByDiscordId); + + // Mark original action as removed + const originalAction = await prisma.moderationAction.update({ + where: { id: actionId }, + data: { status: ActionStatus.REMOVED_BY_ERROR }, + }); + + // Create correction record + return await prisma.moderationAction.create({ + data: { + type: originalAction.type, + status: ActionStatus.REMOVED_BY_ERROR, + reason: `Correction: ${reason}`, + targetId: originalAction.targetId, + moderatorId: removedBy.id, + parentActionId: originalAction.id, + }, + include: { + parentAction: true, + }, + }); +} + +export async function getUserActions(discordId: string, limit = 50) { + const user = await getUserByDiscordId(discordId); + if (!user) { + return []; + } + + return await prisma.moderationAction.findMany({ + where: { targetId: user.id }, + include: { + moderator: true, + }, + orderBy: { createdAt: "desc" }, + take: limit, + }); +} + +export async function getActiveActions(discordId: string) { + const user = await getUserByDiscordId(discordId); + if (!user) { + return []; + } + + return await prisma.moderationAction.findMany({ + where: { + targetId: user.id, + status: ActionStatus.ACTIVE, + }, + include: { + moderator: true, + }, + }); +} + +export async function getActionById(actionId: number) { + return await prisma.moderationAction.findUnique({ + where: { id: actionId }, + include: { + target: true, + moderator: true, + parentAction: true, + }, + }); +} + +export async function updateActionStatus(actionId: number, status: ActionStatus) { + return await prisma.moderationAction.update({ + where: { id: actionId }, + data: { status }, + }); +} From 3eb2a0e8bddfda158200bcc46b7afbeb8afb49d8 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 1 Nov 2025 20:25:28 +0100 Subject: [PATCH 07/21] test: add comprehensive database tests --- src/database/operations.ts | 7 +- tests/database.test.ts | 513 +++++++++++++++++++++++++++++++++++++ 2 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 tests/database.test.ts diff --git a/src/database/operations.ts b/src/database/operations.ts index 11e63b9..3c66d2f 100644 --- a/src/database/operations.ts +++ b/src/database/operations.ts @@ -1,6 +1,11 @@ import { ActionStatus, type ActionType, PrismaClient } from "../../generated/prisma/index.js"; -export const prisma = new PrismaClient(); +export let prisma = new PrismaClient(); + +// Allow tests to override the Prisma client +export function setPrismaClient(client: PrismaClient) { + prisma = client; +} // Connect to database and verify connection export async function connectDatabase() { diff --git a/tests/database.test.ts b/tests/database.test.ts new file mode 100644 index 0000000..5272fed --- /dev/null +++ b/tests/database.test.ts @@ -0,0 +1,513 @@ +import assert from "node:assert/strict"; +import { existsSync, mkdirSync, unlinkSync } from "node:fs"; +import path from "node:path"; +import { after, before, describe, it } from "node:test"; +import { ActionStatus, ActionType, PrismaClient } from "../generated/prisma/index.js"; +import { + findRepeatOffenders, + getActionsByType, + getModeratorStats, + getRecentActions, + getTotalActionCount, + getUserActionStats, +} from "../src/database/analytics.js"; +import { + createModerationAction, + getActionById, + getActiveActions, + getUserActions, + getUserByDiscordId, + removeActionByError, + setPrismaClient, + updateActionStatus, + upsertUser, +} from "../src/database/operations.js"; + +// Use a test database with absolute path +const __dirname = path.dirname(new URL(import.meta.url).pathname); +const TEST_DB_PATH = path.join(__dirname, "..", "data", "test.db"); +const testPrisma = new PrismaClient({ + datasources: { + db: { + url: `file:${TEST_DB_PATH}`, + }, + }, +}); + +describe("Database Operations", () => { + before(async () => { + // Ensure data directory exists + const dataDir = path.join(__dirname, "..", "data"); + if (!existsSync(dataDir)) { + mkdirSync(dataDir, { recursive: true }); + } + + // Clean up any existing test database + if (existsSync(TEST_DB_PATH)) { + unlinkSync(TEST_DB_PATH); + } + + // Connect to database first + await testPrisma.$connect(); + + // Configure operations to use test database + setPrismaClient(testPrisma); + + // Create tables manually + await testPrisma.$executeRawUnsafe(` + CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "discordId" TEXT NOT NULL + ); + `); + + await testPrisma.$executeRawUnsafe(` + CREATE UNIQUE INDEX "User_discordId_key" ON "User"("discordId"); + `); + + await testPrisma.$executeRawUnsafe(` + CREATE INDEX "User_discordId_idx" ON "User"("discordId"); + `); + + await testPrisma.$executeRawUnsafe(` + CREATE TABLE "ModerationAction" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "type" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "reason" TEXT, + "duration" INTEGER, + "targetId" INTEGER NOT NULL, + "moderatorId" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" DATETIME, + "parentActionId" INTEGER, + CONSTRAINT "ModerationAction_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ModerationAction_moderatorId_fkey" FOREIGN KEY ("moderatorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "ModerationAction_parentActionId_fkey" FOREIGN KEY ("parentActionId") REFERENCES "ModerationAction" ("id") ON DELETE SET NULL ON UPDATE CASCADE + ); + `); + + await testPrisma.$executeRawUnsafe(` + CREATE INDEX "ModerationAction_targetId_idx" ON "ModerationAction"("targetId"); + `); + + await testPrisma.$executeRawUnsafe(` + CREATE INDEX "ModerationAction_moderatorId_idx" ON "ModerationAction"("moderatorId"); + `); + + await testPrisma.$executeRawUnsafe(` + CREATE INDEX "ModerationAction_status_idx" ON "ModerationAction"("status"); + `); + + await testPrisma.$executeRawUnsafe(` + CREATE INDEX "ModerationAction_createdAt_idx" ON "ModerationAction"("createdAt"); + `); + }); + + after(async () => { + await testPrisma.$disconnect(); + // Clean up test database + if (existsSync(TEST_DB_PATH)) { + unlinkSync(TEST_DB_PATH); + } + }); + + describe("User Operations", () => { + it("should create a new user", async () => { + const user = await upsertUser("user001"); + + assert.ok(user.id); + assert.strictEqual(user.discordId, "user001"); + }); + + it("should upsert user (create if not exists)", async () => { + const user1 = await upsertUser("user002"); + + assert.strictEqual(user1.discordId, "user002"); + + // Second call should return same user + const user2 = await upsertUser("user002"); + + assert.strictEqual(user1.id, user2.id); + }); + + it("should find user by Discord ID", async () => { + await upsertUser("user003"); + + const user = await getUserByDiscordId("user003"); + + assert.ok(user); + assert.strictEqual(user.discordId, "user003"); + }); + + it("should return null for non-existent user", async () => { + const user = await getUserByDiscordId("nonexistent"); + + assert.strictEqual(user, null); + }); + }); + + describe("Moderation Actions", () => { + it("should create a ban action", async () => { + const action = await createModerationAction({ + type: ActionType.BAN, + targetDiscordId: "target001", + moderatorDiscordId: "mod001", + reason: "Test ban", + }); + + assert.ok(action.id); + assert.strictEqual(action.type, ActionType.BAN); + assert.strictEqual(action.status, ActionStatus.ACTIVE); + assert.strictEqual(action.reason, "Test ban"); + assert.strictEqual(action.target.discordId, "target001"); + assert.strictEqual(action.moderator.discordId, "mod001"); + }); + + it("should create a timeout action with duration", async () => { + const duration = 3600; // 1 hour + + const action = await createModerationAction({ + type: ActionType.TIMEOUT, + targetDiscordId: "target002", + moderatorDiscordId: "mod002", + reason: "Test timeout", + duration, + }); + + assert.strictEqual(action.type, ActionType.TIMEOUT); + assert.strictEqual(action.duration, duration); + assert.ok(action.expiresAt); + }); + + it("should create multiple action types", async () => { + const actionTypes = [ActionType.WARN, ActionType.MUTE, ActionType.KICK, ActionType.REPEL]; + + for (const type of actionTypes) { + const action = await createModerationAction({ + type, + targetDiscordId: "target003", + moderatorDiscordId: "mod003", + reason: `Test ${type}`, + }); + + assert.strictEqual(action.type, type); + } + + const allActions = await getUserActions("target003"); + + assert.strictEqual(allActions.length, actionTypes.length); + }); + + it("should query actions by target user", async () => { + // Create 3 actions + for (let i = 0; i < 3; i++) { + await createModerationAction({ + type: ActionType.WARN, + targetDiscordId: "target004", + moderatorDiscordId: "mod004", + reason: `Warning ${i + 1}`, + }); + } + + const actions = await getUserActions("target004"); + + assert.strictEqual(actions.length, 3); + assert.strictEqual(actions[0].moderator.discordId, "mod004"); + }); + + it("should update action status", async () => { + const action = await createModerationAction({ + type: ActionType.TIMEOUT, + targetDiscordId: "target005", + moderatorDiscordId: "mod005", + }); + + const updated = await updateActionStatus(action.id, ActionStatus.EXPIRED); + + assert.strictEqual(updated.status, ActionStatus.EXPIRED); + }); + + it("should get action by id", async () => { + const created = await createModerationAction({ + type: ActionType.KICK, + targetDiscordId: "target006", + moderatorDiscordId: "mod006", + reason: "Test kick", + }); + + const fetched = await getActionById(created.id); + + assert.ok(fetched); + assert.strictEqual(fetched.id, created.id); + assert.strictEqual(fetched.type, ActionType.KICK); + assert.strictEqual(fetched.target.discordId, "target006"); + }); + + it("should get active actions for user", async () => { + // Create active and expired actions + const action1 = await createModerationAction({ + type: ActionType.WARN, + targetDiscordId: "target007", + moderatorDiscordId: "mod007", + }); + + const action2 = await createModerationAction({ + type: ActionType.MUTE, + targetDiscordId: "target007", + moderatorDiscordId: "mod007", + }); + + // Mark one as expired + await updateActionStatus(action2.id, ActionStatus.EXPIRED); + + const activeActions = await getActiveActions("target007"); + + assert.strictEqual(activeActions.length, 1); + assert.strictEqual(activeActions[0].id, action1.id); + }); + }); + + describe("Action Corrections", () => { + it("should mark action as removed by error and create correction", async () => { + const originalAction = await createModerationAction({ + type: ActionType.BAN, + targetDiscordId: "target008", + moderatorDiscordId: "mod008", + reason: "Mistake", + }); + + // Remove action by error + const correction = await removeActionByError( + originalAction.id, + "corrector001", + "This was a mistake" + ); + + assert.ok(correction.parentActionId); + assert.strictEqual(correction.parentActionId, originalAction.id); + assert.strictEqual(correction.status, ActionStatus.REMOVED_BY_ERROR); + assert.ok(correction.reason?.includes("Correction: This was a mistake")); + + // Verify original action is marked as removed + const updated = await getActionById(originalAction.id); + assert.ok(updated); + assert.strictEqual(updated.status, ActionStatus.REMOVED_BY_ERROR); + }); + + it("should create correction record with parent reference", async () => { + const originalAction = await createModerationAction({ + type: ActionType.KICK, + targetDiscordId: "target009", + moderatorDiscordId: "mod009", + reason: "Wrong user", + }); + + // Create correction + const correction = await removeActionByError( + originalAction.id, + "corrector002", + "Wrong person was kicked" + ); + + assert.ok(correction.parentActionId); + assert.strictEqual(correction.parentActionId, originalAction.id); + + // Verify parent relationship + const withParent = await getActionById(correction.id); + + assert.ok(withParent?.parentAction); + assert.strictEqual(withParent.parentAction.id, originalAction.id); + assert.strictEqual(withParent.parentAction.reason, "Wrong user"); + }); + }); + + describe("Analytics", () => { + it("should get user action stats with counts by type", async () => { + // Create 2 warnings and 1 ban + await createModerationAction({ + type: ActionType.WARN, + targetDiscordId: "target010", + moderatorDiscordId: "mod010", + }); + + await createModerationAction({ + type: ActionType.WARN, + targetDiscordId: "target010", + moderatorDiscordId: "mod010", + }); + + await createModerationAction({ + type: ActionType.BAN, + targetDiscordId: "target010", + moderatorDiscordId: "mod010", + }); + + const stats = await getUserActionStats("target010"); + + assert.ok(stats); + assert.strictEqual(stats.discordId, "target010"); + assert.strictEqual(stats.totalActions, 3); + assert.strictEqual(stats.actionsByType[ActionType.WARN], 2); + assert.strictEqual(stats.actionsByType[ActionType.BAN], 1); + }); + + it("should get moderator stats", async () => { + // Create actions by the same moderator + await createModerationAction({ + type: ActionType.WARN, + targetDiscordId: "target011", + moderatorDiscordId: "mod011", + }); + + await createModerationAction({ + type: ActionType.KICK, + targetDiscordId: "target012", + moderatorDiscordId: "mod011", + }); + + const stats = await getModeratorStats("mod011"); + + assert.ok(stats); + assert.strictEqual(stats.discordId, "mod011"); + assert.strictEqual(stats.totalActions, 2); + assert.strictEqual(stats.actionsByType[ActionType.WARN], 1); + assert.strictEqual(stats.actionsByType[ActionType.KICK], 1); + }); + + it("should get recent actions ordered by date", async () => { + // Create actions with slight delay + for (let i = 0; i < 3; i++) { + await createModerationAction({ + type: ActionType.WARN, + targetDiscordId: "target013", + moderatorDiscordId: "mod013", + reason: `Action ${i}`, + }); + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + const recentActions = await getRecentActions(2); + + // Should get the 2 most recent actions + assert.ok(recentActions.length >= 2); + // Most recent should come first + assert.ok(recentActions[0].createdAt >= recentActions[1].createdAt); + }); + + it("should find repeat offenders", async () => { + // Create multiple actions for one user + for (let i = 0; i < 4; i++) { + await createModerationAction({ + type: ActionType.WARN, + targetDiscordId: "repeat_offender", + moderatorDiscordId: "mod014", + }); + } + + // Create fewer actions for another user + await createModerationAction({ + type: ActionType.WARN, + targetDiscordId: "minor_offender", + moderatorDiscordId: "mod014", + }); + + const offenders = await findRepeatOffenders(3); + + // Should find the user with 4+ actions + const repeatOffender = offenders.find((o) => o.discordId === "repeat_offender"); + assert.ok(repeatOffender); + assert.ok(repeatOffender.count >= 4); + + // Minor offender shouldn't be in the list + const minorOffender = offenders.find((o) => o.discordId === "minor_offender"); + assert.strictEqual(minorOffender, undefined); + }); + + it("should get actions by type", async () => { + await createModerationAction({ + type: ActionType.KICK, + targetDiscordId: "target015", + moderatorDiscordId: "mod015", + }); + + await createModerationAction({ + type: ActionType.KICK, + targetDiscordId: "target016", + moderatorDiscordId: "mod015", + }); + + const kickActions = await getActionsByType(ActionType.KICK); + + // Should have at least the 2 we just created + const ourKicks = kickActions.filter( + (a) => a.target.discordId === "target015" || a.target.discordId === "target016" + ); + assert.strictEqual(ourKicks.length, 2); + }); + + it("should count total actions excluding errors", async () => { + const action1 = await createModerationAction({ + type: ActionType.WARN, + targetDiscordId: "target017", + moderatorDiscordId: "mod017", + }); + + await createModerationAction({ + type: ActionType.BAN, + targetDiscordId: "target017", + moderatorDiscordId: "mod017", + }); + + // Mark one as error + await removeActionByError(action1.id, "corrector003", "Error"); + + // Count should exclude the removed one + const total = await getTotalActionCount(); + + // Hard to test exact number since other tests create actions too + // Just verify it's a positive number + assert.ok(total > 0); + }); + }); + + describe("Database Structure", () => { + it("should have all required tables", async () => { + const tables = await testPrisma.$queryRaw>` + SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_prisma%' + `; + + const tableNames = tables.map((t) => t.name); + assert.ok(tableNames.includes("User")); + assert.ok(tableNames.includes("ModerationAction")); + }); + + it("should have proper indexes", async () => { + const indexes = await testPrisma.$queryRaw>` + SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' + `; + + const indexNames = indexes.map((i) => i.name); + assert.ok(indexNames.some((name) => name.includes("discordId"))); + assert.ok(indexNames.some((name) => name.includes("targetId"))); + assert.ok(indexNames.some((name) => name.includes("moderatorId"))); + }); + + it("should enforce unique constraint on discordId", async () => { + await testPrisma.user.create({ + data: { discordId: "unique001" }, + }); + + await assert.rejects( + async () => { + await testPrisma.user.create({ + data: { discordId: "unique001" }, + }); + }, + { + name: "PrismaClientKnownRequestError", + } + ); + }); + }); +}); From c74920f65086eaedf3c8bc192d722d3df38374fb Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 1 Nov 2025 20:30:02 +0100 Subject: [PATCH 08/21] refactor: improve variable naming for clarity in analytics and database tests --- src/database/analytics.ts | 2 +- tests/database.test.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/database/analytics.ts b/src/database/analytics.ts index 13530e1..05334c8 100644 --- a/src/database/analytics.ts +++ b/src/database/analytics.ts @@ -107,7 +107,7 @@ export async function findRepeatOffenders(minActions = 3) { return Object.values(userActionCounts) .filter((item) => item.count >= minActions) - .sort((a, b) => b.count - a.count); + .sort((offenderA, offenderB) => offenderB.count - offenderA.count); } export async function getActionsByType(actionType: ActionType, limit = 50) { diff --git a/tests/database.test.ts b/tests/database.test.ts index 5272fed..93c1c92 100644 --- a/tests/database.test.ts +++ b/tests/database.test.ts @@ -415,12 +415,12 @@ describe("Database Operations", () => { const offenders = await findRepeatOffenders(3); // Should find the user with 4+ actions - const repeatOffender = offenders.find((o) => o.discordId === "repeat_offender"); + const repeatOffender = offenders.find((offender) => offender.discordId === "repeat_offender"); assert.ok(repeatOffender); assert.ok(repeatOffender.count >= 4); // Minor offender shouldn't be in the list - const minorOffender = offenders.find((o) => o.discordId === "minor_offender"); + const minorOffender = offenders.find((offender) => offender.discordId === "minor_offender"); assert.strictEqual(minorOffender, undefined); }); @@ -441,7 +441,8 @@ describe("Database Operations", () => { // Should have at least the 2 we just created const ourKicks = kickActions.filter( - (a) => a.target.discordId === "target015" || a.target.discordId === "target016" + (action) => + action.target.discordId === "target015" || action.target.discordId === "target016" ); assert.strictEqual(ourKicks.length, 2); }); @@ -477,7 +478,7 @@ describe("Database Operations", () => { SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_prisma%' `; - const tableNames = tables.map((t) => t.name); + const tableNames = tables.map((table) => table.name); assert.ok(tableNames.includes("User")); assert.ok(tableNames.includes("ModerationAction")); }); @@ -487,7 +488,7 @@ describe("Database Operations", () => { SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' `; - const indexNames = indexes.map((i) => i.name); + const indexNames = indexes.map((index) => index.name); assert.ok(indexNames.some((name) => name.includes("discordId"))); assert.ok(indexNames.some((name) => name.includes("targetId"))); assert.ok(indexNames.some((name) => name.includes("moderatorId"))); From c6fb8e20255b5ba6019fb404bf13cb63d862e76d Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sat, 1 Nov 2025 20:36:09 +0100 Subject: [PATCH 09/21] test: enhance database action counting tests to verify accurate totals after action creation and error removal --- tests/database.test.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/database.test.ts b/tests/database.test.ts index 93c1c92..0367a2b 100644 --- a/tests/database.test.ts +++ b/tests/database.test.ts @@ -448,6 +448,10 @@ describe("Database Operations", () => { }); it("should count total actions excluding errors", async () => { + // Get baseline count + const countBefore = await getTotalActionCount(); + + // Create 3 new actions const action1 = await createModerationAction({ type: ActionType.WARN, targetDiscordId: "target017", @@ -460,15 +464,23 @@ describe("Database Operations", () => { moderatorDiscordId: "mod017", }); + await createModerationAction({ + type: ActionType.KICK, + targetDiscordId: "target017", + moderatorDiscordId: "mod017", + }); + + // Count should have increased by 3 + const countAfterCreate = await getTotalActionCount(); + assert.strictEqual(countAfterCreate, countBefore + 3); + // Mark one as error await removeActionByError(action1.id, "corrector003", "Error"); - // Count should exclude the removed one - const total = await getTotalActionCount(); - - // Hard to test exact number since other tests create actions too - // Just verify it's a positive number - assert.ok(total > 0); + // Count should decrease by 1 (original action marked as REMOVED_BY_ERROR) + // Note: The correction record is created with REMOVED_BY_ERROR status, so it's never counted + const countAfterRemoval = await getTotalActionCount(); + assert.strictEqual(countAfterRemoval, countBefore + 2); }); }); From 304fabcbdb4d21a372c2ad65f5d3e850b063e79c Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 9 Nov 2025 14:58:26 +0100 Subject: [PATCH 10/21] refactor: update Prisma schema to introduce new roles and action types, and streamline action status definitions --- prisma/schema.prisma | 72 +++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d7bf2ee..4e53cc4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,60 +11,44 @@ datasource db { url = "file:../data/moderation.db" } -model User { - id Int @id @default(autoincrement()) - discordId String @unique - - // Relations - actionsReceived ModerationAction[] @relation("ActionTarget") - actionsPerformed ModerationAction[] @relation("ActionModerator") - - @@index([discordId]) +enum Role { + MEMBER + ASSISTANT + MOD_I + MOD_II + MOD_III + MANAGER + ADMIN } enum ActionType { - WARN + REPEL + TIMEOUT MUTE - UNMUTE + TEMP_MUTE + WARN KICK + BAN - UNBAN - TIMEOUT - REMOVE_TIMEOUT - REPEL + TEMP_BAN + + REVERT } enum ActionStatus { ACTIVE - EXPIRED - REMOVED_BY_ERROR - REVERSED + STALE + REVERTED } -model ModerationAction { - id Int @id @default(autoincrement()) - type ActionType - status ActionStatus @default(ACTIVE) - reason String? - duration Int? // Duration in seconds for timeouts/mutes - - // User relationships - targetId Int - target User @relation("ActionTarget", fields: [targetId], references: [id]) - moderatorId Int - moderator User @relation("ActionModerator", fields: [moderatorId], references: [id]) - - // Timestamps - createdAt DateTime @default(now()) - expiresAt DateTime? - - // For corrections/reversals - parentActionId Int? - parentAction ModerationAction? @relation("ActionCorrections", fields: [parentActionId], references: [id]) - corrections ModerationAction[] @relation("ActionCorrections") - - @@index([targetId]) - @@index([moderatorId]) - @@index([status]) - @@index([createdAt]) +enum ActionReason { + SPAM + SCAM + DISRUPTION + NSFW + HATE_SPEECH + SELF_PROMOTION + JOB_POSTING + FOR_HIRE + OTHER } \ No newline at end of file From 32c1a3647630f182329f7c358bebfb492c666de3 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 9 Nov 2025 15:11:51 +0100 Subject: [PATCH 11/21] feat: add User and Action models to Prisma schema, replacing ModerationAction and updating relationships --- .../migration.sql | 62 +++++++++++++++++++ .../migration.sql | 30 +++++++++ prisma/schema.prisma | 39 +++++++++++- 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20251109140048_add_user_and_action_models/migration.sql create mode 100644 prisma/migrations/20251109140934_remove_duration_field/migration.sql diff --git a/prisma/migrations/20251109140048_add_user_and_action_models/migration.sql b/prisma/migrations/20251109140048_add_user_and_action_models/migration.sql new file mode 100644 index 0000000..e486aed --- /dev/null +++ b/prisma/migrations/20251109140048_add_user_and_action_models/migration.sql @@ -0,0 +1,62 @@ +/* + Warnings: + + - You are about to drop the `ModerationAction` table. If the table is not empty, all the data it contains will be lost. + - The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `discordId` on the `User` table. All the data in the column will be lost. + - You are about to drop the column `id` on the `User` table. All the data in the column will be lost. + - Added the required column `discordUserId` to the `User` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "ModerationAction_createdAt_idx"; + +-- DropIndex +DROP INDEX "ModerationAction_status_idx"; + +-- DropIndex +DROP INDEX "ModerationAction_moderatorId_idx"; + +-- DropIndex +DROP INDEX "ModerationAction_targetId_idx"; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "ModerationAction"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "Action" ( + "actionId" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "userId" INTEGER NOT NULL, + "type" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "duration" INTEGER, + "moderatorUserId" INTEGER NOT NULL, + "reason" TEXT NOT NULL, + "note" TEXT, + "createdAt" INTEGER NOT NULL, + "expiresAt" INTEGER, + "revertingActionId" INTEGER, + CONSTRAINT "Action_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Action_moderatorUserId_fkey" FOREIGN KEY ("moderatorUserId") REFERENCES "User" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Action_revertingActionId_fkey" FOREIGN KEY ("revertingActionId") REFERENCES "Action" ("actionId") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_User" ( + "userId" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "discordUserId" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'MEMBER' +); +DROP TABLE "User"; +ALTER TABLE "new_User" RENAME TO "User"; +CREATE UNIQUE INDEX "User_discordUserId_key" ON "User"("discordUserId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; + +-- CreateIndex +CREATE UNIQUE INDEX "Action_revertingActionId_key" ON "Action"("revertingActionId"); diff --git a/prisma/migrations/20251109140934_remove_duration_field/migration.sql b/prisma/migrations/20251109140934_remove_duration_field/migration.sql new file mode 100644 index 0000000..3070140 --- /dev/null +++ b/prisma/migrations/20251109140934_remove_duration_field/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - You are about to drop the column `duration` on the `Action` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Action" ( + "actionId" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "userId" INTEGER NOT NULL, + "type" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "moderatorUserId" INTEGER NOT NULL, + "reason" TEXT NOT NULL, + "note" TEXT, + "createdAt" INTEGER NOT NULL, + "expiresAt" INTEGER, + "revertingActionId" INTEGER, + CONSTRAINT "Action_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Action_moderatorUserId_fkey" FOREIGN KEY ("moderatorUserId") REFERENCES "User" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Action_revertingActionId_fkey" FOREIGN KEY ("revertingActionId") REFERENCES "Action" ("actionId") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Action" ("actionId", "createdAt", "expiresAt", "moderatorUserId", "note", "reason", "revertingActionId", "status", "type", "userId") SELECT "actionId", "createdAt", "expiresAt", "moderatorUserId", "note", "reason", "revertingActionId", "status", "type", "userId" FROM "Action"; +DROP TABLE "Action"; +ALTER TABLE "new_Action" RENAME TO "Action"; +CREATE UNIQUE INDEX "Action_revertingActionId_key" ON "Action"("revertingActionId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4e53cc4..6fb5781 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -44,11 +44,48 @@ enum ActionStatus { enum ActionReason { SPAM SCAM - DISRUPTION + DISRESPCTING NSFW HATE_SPEECH SELF_PROMOTION JOB_POSTING FOR_HIRE OTHER +} + +model User { + userId Int @id @default(autoincrement()) + discordUserId String @unique + role Role @default(MEMBER) + + // A user can have many actions performed on them + actions Action[] @relation("UserActions") + + // A user (as moderator) can perform many moderation actions + moderatedActions Action[] @relation("ModeratorActions") +} + +model Action { + actionId Int @id @default(autoincrement()) + userId Int + type ActionType + status ActionStatus @default(ACTIVE) + moderatorUserId Int + reason String + note String? + createdAt Int + expiresAt Int? + revertingActionId Int? @unique + + // Relationship to the user this action was performed on + user User @relation("UserActions", fields: [userId], references: [userId]) + + // Relationship to the moderator who performed this action + moderator User @relation("ModeratorActions", fields: [moderatorUserId], references: [userId]) + + // Self-referential relationship: this action reverts another action + revertingAction Action? @relation("ActionCorrections", fields: [revertingActionId], references: [actionId]) + + // Self-referential relationship: actions that revert this action + revertedByActions Action[] @relation("ActionCorrections") } \ No newline at end of file From 6effde87a2b927a52feff09b327c376e91c0d2d6 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 9 Nov 2025 15:27:23 +0100 Subject: [PATCH 12/21] chore: remove unused analytics and operations modules along with associated tests --- src/database/analytics.ts | 131 --------- src/database/operations.ts | 155 ----------- tests/database.test.ts | 526 ------------------------------------- 3 files changed, 812 deletions(-) delete mode 100644 src/database/analytics.ts delete mode 100644 src/database/operations.ts delete mode 100644 tests/database.test.ts diff --git a/src/database/analytics.ts b/src/database/analytics.ts deleted file mode 100644 index 05334c8..0000000 --- a/src/database/analytics.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - ActionStatus, - type ActionType, - type ModerationAction, -} from "../../generated/prisma/index.js"; -import { prisma } from "./operations.js"; - -export async function getUserActionStats(discordId: string) { - const user = await prisma.user.findUnique({ - where: { discordId }, - include: { - actionsReceived: { - where: { - status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] }, - }, - }, - }, - }); - - if (!user) { - return null; - } - - const actionsByType = user.actionsReceived.reduce( - (acc, action) => { - acc[action.type] = (acc[action.type] || 0) + 1; - return acc; - }, - {} as Record - ); - - return { - discordId: user.discordId, - totalActions: user.actionsReceived.length, - actionsByType, - }; -} - -export async function getModeratorStats(discordId: string, days = 30) { - const user = await prisma.user.findUnique({ - where: { discordId }, - }); - - if (!user) { - return null; - } - - const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000); - - const actions = await prisma.moderationAction.findMany({ - where: { - moderatorId: user.id, - createdAt: { gte: since }, - status: { not: ActionStatus.REMOVED_BY_ERROR }, - }, - }); - - const actionsByType = actions.reduce( - (acc, action) => { - acc[action.type] = (acc[action.type] || 0) + 1; - return acc; - }, - {} as Record - ); - - return { - discordId: user.discordId, - totalActions: actions.length, - actionsByType, - period: `${days} days`, - }; -} - -export async function getRecentActions(limit = 20) { - return await prisma.moderationAction.findMany({ - include: { - target: true, - moderator: true, - }, - orderBy: { createdAt: "desc" }, - take: limit, - }); -} - -export async function findRepeatOffenders(minActions = 3) { - const actions = await prisma.moderationAction.findMany({ - where: { - status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] }, - }, - include: { - target: true, - }, - }); - - const userActionCounts = actions.reduce( - (acc, action) => { - const discordId = action.target.discordId; - if (!acc[discordId]) { - acc[discordId] = { discordId, count: 0, actions: [] }; - } - acc[discordId].count++; - acc[discordId].actions.push(action); - return acc; - }, - {} as Record - ); - - return Object.values(userActionCounts) - .filter((item) => item.count >= minActions) - .sort((offenderA, offenderB) => offenderB.count - offenderA.count); -} - -export async function getActionsByType(actionType: ActionType, limit = 50) { - return await prisma.moderationAction.findMany({ - where: { type: actionType }, - include: { - target: true, - moderator: true, - }, - orderBy: { createdAt: "desc" }, - take: limit, - }); -} - -export async function getTotalActionCount() { - return await prisma.moderationAction.count({ - where: { - status: { not: ActionStatus.REMOVED_BY_ERROR }, - }, - }); -} diff --git a/src/database/operations.ts b/src/database/operations.ts deleted file mode 100644 index 3c66d2f..0000000 --- a/src/database/operations.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { ActionStatus, type ActionType, PrismaClient } from "../../generated/prisma/index.js"; - -export let prisma = new PrismaClient(); - -// Allow tests to override the Prisma client -export function setPrismaClient(client: PrismaClient) { - prisma = client; -} - -// Connect to database and verify connection -export async function connectDatabase() { - try { - await prisma.$connect(); - // Test query to verify connection - await prisma.user.findFirst(); - console.log("✅ Database connected successfully"); - } catch (error) { - console.error("❌ Database connection failed:", error); - throw error; - } -} - -// Disconnect from database -export async function disconnectDatabase() { - await prisma.$disconnect(); - console.log("Database disconnected"); -} - -// User operations - only Discord ID needed -export async function upsertUser(discordId: string) { - return await prisma.user.upsert({ - where: { discordId }, - update: {}, - create: { discordId }, - }); -} - -export async function getUserByDiscordId(discordId: string) { - return await prisma.user.findUnique({ - where: { discordId }, - }); -} - -// Moderation action operations -type CreateActionParams = { - type: ActionType; - targetDiscordId: string; - moderatorDiscordId: string; - reason?: string; - duration?: number; -}; - -export async function createModerationAction(params: CreateActionParams) { - // Ensure both users exist - const target = await upsertUser(params.targetDiscordId); - const moderator = await upsertUser(params.moderatorDiscordId); - - // Calculate expiry for timed actions - const expiresAt = params.duration ? new Date(Date.now() + params.duration * 1000) : null; - - return await prisma.moderationAction.create({ - data: { - type: params.type, - reason: params.reason, - duration: params.duration, - targetId: target.id, - moderatorId: moderator.id, - expiresAt, - }, - include: { - target: true, - moderator: true, - }, - }); -} - -export async function removeActionByError( - actionId: number, - removedByDiscordId: string, - reason: string -) { - const removedBy = await upsertUser(removedByDiscordId); - - // Mark original action as removed - const originalAction = await prisma.moderationAction.update({ - where: { id: actionId }, - data: { status: ActionStatus.REMOVED_BY_ERROR }, - }); - - // Create correction record - return await prisma.moderationAction.create({ - data: { - type: originalAction.type, - status: ActionStatus.REMOVED_BY_ERROR, - reason: `Correction: ${reason}`, - targetId: originalAction.targetId, - moderatorId: removedBy.id, - parentActionId: originalAction.id, - }, - include: { - parentAction: true, - }, - }); -} - -export async function getUserActions(discordId: string, limit = 50) { - const user = await getUserByDiscordId(discordId); - if (!user) { - return []; - } - - return await prisma.moderationAction.findMany({ - where: { targetId: user.id }, - include: { - moderator: true, - }, - orderBy: { createdAt: "desc" }, - take: limit, - }); -} - -export async function getActiveActions(discordId: string) { - const user = await getUserByDiscordId(discordId); - if (!user) { - return []; - } - - return await prisma.moderationAction.findMany({ - where: { - targetId: user.id, - status: ActionStatus.ACTIVE, - }, - include: { - moderator: true, - }, - }); -} - -export async function getActionById(actionId: number) { - return await prisma.moderationAction.findUnique({ - where: { id: actionId }, - include: { - target: true, - moderator: true, - parentAction: true, - }, - }); -} - -export async function updateActionStatus(actionId: number, status: ActionStatus) { - return await prisma.moderationAction.update({ - where: { id: actionId }, - data: { status }, - }); -} diff --git a/tests/database.test.ts b/tests/database.test.ts deleted file mode 100644 index 0367a2b..0000000 --- a/tests/database.test.ts +++ /dev/null @@ -1,526 +0,0 @@ -import assert from "node:assert/strict"; -import { existsSync, mkdirSync, unlinkSync } from "node:fs"; -import path from "node:path"; -import { after, before, describe, it } from "node:test"; -import { ActionStatus, ActionType, PrismaClient } from "../generated/prisma/index.js"; -import { - findRepeatOffenders, - getActionsByType, - getModeratorStats, - getRecentActions, - getTotalActionCount, - getUserActionStats, -} from "../src/database/analytics.js"; -import { - createModerationAction, - getActionById, - getActiveActions, - getUserActions, - getUserByDiscordId, - removeActionByError, - setPrismaClient, - updateActionStatus, - upsertUser, -} from "../src/database/operations.js"; - -// Use a test database with absolute path -const __dirname = path.dirname(new URL(import.meta.url).pathname); -const TEST_DB_PATH = path.join(__dirname, "..", "data", "test.db"); -const testPrisma = new PrismaClient({ - datasources: { - db: { - url: `file:${TEST_DB_PATH}`, - }, - }, -}); - -describe("Database Operations", () => { - before(async () => { - // Ensure data directory exists - const dataDir = path.join(__dirname, "..", "data"); - if (!existsSync(dataDir)) { - mkdirSync(dataDir, { recursive: true }); - } - - // Clean up any existing test database - if (existsSync(TEST_DB_PATH)) { - unlinkSync(TEST_DB_PATH); - } - - // Connect to database first - await testPrisma.$connect(); - - // Configure operations to use test database - setPrismaClient(testPrisma); - - // Create tables manually - await testPrisma.$executeRawUnsafe(` - CREATE TABLE "User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "discordId" TEXT NOT NULL - ); - `); - - await testPrisma.$executeRawUnsafe(` - CREATE UNIQUE INDEX "User_discordId_key" ON "User"("discordId"); - `); - - await testPrisma.$executeRawUnsafe(` - CREATE INDEX "User_discordId_idx" ON "User"("discordId"); - `); - - await testPrisma.$executeRawUnsafe(` - CREATE TABLE "ModerationAction" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "type" TEXT NOT NULL, - "status" TEXT NOT NULL DEFAULT 'ACTIVE', - "reason" TEXT, - "duration" INTEGER, - "targetId" INTEGER NOT NULL, - "moderatorId" INTEGER NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "expiresAt" DATETIME, - "parentActionId" INTEGER, - CONSTRAINT "ModerationAction_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "ModerationAction_moderatorId_fkey" FOREIGN KEY ("moderatorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "ModerationAction_parentActionId_fkey" FOREIGN KEY ("parentActionId") REFERENCES "ModerationAction" ("id") ON DELETE SET NULL ON UPDATE CASCADE - ); - `); - - await testPrisma.$executeRawUnsafe(` - CREATE INDEX "ModerationAction_targetId_idx" ON "ModerationAction"("targetId"); - `); - - await testPrisma.$executeRawUnsafe(` - CREATE INDEX "ModerationAction_moderatorId_idx" ON "ModerationAction"("moderatorId"); - `); - - await testPrisma.$executeRawUnsafe(` - CREATE INDEX "ModerationAction_status_idx" ON "ModerationAction"("status"); - `); - - await testPrisma.$executeRawUnsafe(` - CREATE INDEX "ModerationAction_createdAt_idx" ON "ModerationAction"("createdAt"); - `); - }); - - after(async () => { - await testPrisma.$disconnect(); - // Clean up test database - if (existsSync(TEST_DB_PATH)) { - unlinkSync(TEST_DB_PATH); - } - }); - - describe("User Operations", () => { - it("should create a new user", async () => { - const user = await upsertUser("user001"); - - assert.ok(user.id); - assert.strictEqual(user.discordId, "user001"); - }); - - it("should upsert user (create if not exists)", async () => { - const user1 = await upsertUser("user002"); - - assert.strictEqual(user1.discordId, "user002"); - - // Second call should return same user - const user2 = await upsertUser("user002"); - - assert.strictEqual(user1.id, user2.id); - }); - - it("should find user by Discord ID", async () => { - await upsertUser("user003"); - - const user = await getUserByDiscordId("user003"); - - assert.ok(user); - assert.strictEqual(user.discordId, "user003"); - }); - - it("should return null for non-existent user", async () => { - const user = await getUserByDiscordId("nonexistent"); - - assert.strictEqual(user, null); - }); - }); - - describe("Moderation Actions", () => { - it("should create a ban action", async () => { - const action = await createModerationAction({ - type: ActionType.BAN, - targetDiscordId: "target001", - moderatorDiscordId: "mod001", - reason: "Test ban", - }); - - assert.ok(action.id); - assert.strictEqual(action.type, ActionType.BAN); - assert.strictEqual(action.status, ActionStatus.ACTIVE); - assert.strictEqual(action.reason, "Test ban"); - assert.strictEqual(action.target.discordId, "target001"); - assert.strictEqual(action.moderator.discordId, "mod001"); - }); - - it("should create a timeout action with duration", async () => { - const duration = 3600; // 1 hour - - const action = await createModerationAction({ - type: ActionType.TIMEOUT, - targetDiscordId: "target002", - moderatorDiscordId: "mod002", - reason: "Test timeout", - duration, - }); - - assert.strictEqual(action.type, ActionType.TIMEOUT); - assert.strictEqual(action.duration, duration); - assert.ok(action.expiresAt); - }); - - it("should create multiple action types", async () => { - const actionTypes = [ActionType.WARN, ActionType.MUTE, ActionType.KICK, ActionType.REPEL]; - - for (const type of actionTypes) { - const action = await createModerationAction({ - type, - targetDiscordId: "target003", - moderatorDiscordId: "mod003", - reason: `Test ${type}`, - }); - - assert.strictEqual(action.type, type); - } - - const allActions = await getUserActions("target003"); - - assert.strictEqual(allActions.length, actionTypes.length); - }); - - it("should query actions by target user", async () => { - // Create 3 actions - for (let i = 0; i < 3; i++) { - await createModerationAction({ - type: ActionType.WARN, - targetDiscordId: "target004", - moderatorDiscordId: "mod004", - reason: `Warning ${i + 1}`, - }); - } - - const actions = await getUserActions("target004"); - - assert.strictEqual(actions.length, 3); - assert.strictEqual(actions[0].moderator.discordId, "mod004"); - }); - - it("should update action status", async () => { - const action = await createModerationAction({ - type: ActionType.TIMEOUT, - targetDiscordId: "target005", - moderatorDiscordId: "mod005", - }); - - const updated = await updateActionStatus(action.id, ActionStatus.EXPIRED); - - assert.strictEqual(updated.status, ActionStatus.EXPIRED); - }); - - it("should get action by id", async () => { - const created = await createModerationAction({ - type: ActionType.KICK, - targetDiscordId: "target006", - moderatorDiscordId: "mod006", - reason: "Test kick", - }); - - const fetched = await getActionById(created.id); - - assert.ok(fetched); - assert.strictEqual(fetched.id, created.id); - assert.strictEqual(fetched.type, ActionType.KICK); - assert.strictEqual(fetched.target.discordId, "target006"); - }); - - it("should get active actions for user", async () => { - // Create active and expired actions - const action1 = await createModerationAction({ - type: ActionType.WARN, - targetDiscordId: "target007", - moderatorDiscordId: "mod007", - }); - - const action2 = await createModerationAction({ - type: ActionType.MUTE, - targetDiscordId: "target007", - moderatorDiscordId: "mod007", - }); - - // Mark one as expired - await updateActionStatus(action2.id, ActionStatus.EXPIRED); - - const activeActions = await getActiveActions("target007"); - - assert.strictEqual(activeActions.length, 1); - assert.strictEqual(activeActions[0].id, action1.id); - }); - }); - - describe("Action Corrections", () => { - it("should mark action as removed by error and create correction", async () => { - const originalAction = await createModerationAction({ - type: ActionType.BAN, - targetDiscordId: "target008", - moderatorDiscordId: "mod008", - reason: "Mistake", - }); - - // Remove action by error - const correction = await removeActionByError( - originalAction.id, - "corrector001", - "This was a mistake" - ); - - assert.ok(correction.parentActionId); - assert.strictEqual(correction.parentActionId, originalAction.id); - assert.strictEqual(correction.status, ActionStatus.REMOVED_BY_ERROR); - assert.ok(correction.reason?.includes("Correction: This was a mistake")); - - // Verify original action is marked as removed - const updated = await getActionById(originalAction.id); - assert.ok(updated); - assert.strictEqual(updated.status, ActionStatus.REMOVED_BY_ERROR); - }); - - it("should create correction record with parent reference", async () => { - const originalAction = await createModerationAction({ - type: ActionType.KICK, - targetDiscordId: "target009", - moderatorDiscordId: "mod009", - reason: "Wrong user", - }); - - // Create correction - const correction = await removeActionByError( - originalAction.id, - "corrector002", - "Wrong person was kicked" - ); - - assert.ok(correction.parentActionId); - assert.strictEqual(correction.parentActionId, originalAction.id); - - // Verify parent relationship - const withParent = await getActionById(correction.id); - - assert.ok(withParent?.parentAction); - assert.strictEqual(withParent.parentAction.id, originalAction.id); - assert.strictEqual(withParent.parentAction.reason, "Wrong user"); - }); - }); - - describe("Analytics", () => { - it("should get user action stats with counts by type", async () => { - // Create 2 warnings and 1 ban - await createModerationAction({ - type: ActionType.WARN, - targetDiscordId: "target010", - moderatorDiscordId: "mod010", - }); - - await createModerationAction({ - type: ActionType.WARN, - targetDiscordId: "target010", - moderatorDiscordId: "mod010", - }); - - await createModerationAction({ - type: ActionType.BAN, - targetDiscordId: "target010", - moderatorDiscordId: "mod010", - }); - - const stats = await getUserActionStats("target010"); - - assert.ok(stats); - assert.strictEqual(stats.discordId, "target010"); - assert.strictEqual(stats.totalActions, 3); - assert.strictEqual(stats.actionsByType[ActionType.WARN], 2); - assert.strictEqual(stats.actionsByType[ActionType.BAN], 1); - }); - - it("should get moderator stats", async () => { - // Create actions by the same moderator - await createModerationAction({ - type: ActionType.WARN, - targetDiscordId: "target011", - moderatorDiscordId: "mod011", - }); - - await createModerationAction({ - type: ActionType.KICK, - targetDiscordId: "target012", - moderatorDiscordId: "mod011", - }); - - const stats = await getModeratorStats("mod011"); - - assert.ok(stats); - assert.strictEqual(stats.discordId, "mod011"); - assert.strictEqual(stats.totalActions, 2); - assert.strictEqual(stats.actionsByType[ActionType.WARN], 1); - assert.strictEqual(stats.actionsByType[ActionType.KICK], 1); - }); - - it("should get recent actions ordered by date", async () => { - // Create actions with slight delay - for (let i = 0; i < 3; i++) { - await createModerationAction({ - type: ActionType.WARN, - targetDiscordId: "target013", - moderatorDiscordId: "mod013", - reason: `Action ${i}`, - }); - await new Promise((resolve) => setTimeout(resolve, 10)); - } - - const recentActions = await getRecentActions(2); - - // Should get the 2 most recent actions - assert.ok(recentActions.length >= 2); - // Most recent should come first - assert.ok(recentActions[0].createdAt >= recentActions[1].createdAt); - }); - - it("should find repeat offenders", async () => { - // Create multiple actions for one user - for (let i = 0; i < 4; i++) { - await createModerationAction({ - type: ActionType.WARN, - targetDiscordId: "repeat_offender", - moderatorDiscordId: "mod014", - }); - } - - // Create fewer actions for another user - await createModerationAction({ - type: ActionType.WARN, - targetDiscordId: "minor_offender", - moderatorDiscordId: "mod014", - }); - - const offenders = await findRepeatOffenders(3); - - // Should find the user with 4+ actions - const repeatOffender = offenders.find((offender) => offender.discordId === "repeat_offender"); - assert.ok(repeatOffender); - assert.ok(repeatOffender.count >= 4); - - // Minor offender shouldn't be in the list - const minorOffender = offenders.find((offender) => offender.discordId === "minor_offender"); - assert.strictEqual(minorOffender, undefined); - }); - - it("should get actions by type", async () => { - await createModerationAction({ - type: ActionType.KICK, - targetDiscordId: "target015", - moderatorDiscordId: "mod015", - }); - - await createModerationAction({ - type: ActionType.KICK, - targetDiscordId: "target016", - moderatorDiscordId: "mod015", - }); - - const kickActions = await getActionsByType(ActionType.KICK); - - // Should have at least the 2 we just created - const ourKicks = kickActions.filter( - (action) => - action.target.discordId === "target015" || action.target.discordId === "target016" - ); - assert.strictEqual(ourKicks.length, 2); - }); - - it("should count total actions excluding errors", async () => { - // Get baseline count - const countBefore = await getTotalActionCount(); - - // Create 3 new actions - const action1 = await createModerationAction({ - type: ActionType.WARN, - targetDiscordId: "target017", - moderatorDiscordId: "mod017", - }); - - await createModerationAction({ - type: ActionType.BAN, - targetDiscordId: "target017", - moderatorDiscordId: "mod017", - }); - - await createModerationAction({ - type: ActionType.KICK, - targetDiscordId: "target017", - moderatorDiscordId: "mod017", - }); - - // Count should have increased by 3 - const countAfterCreate = await getTotalActionCount(); - assert.strictEqual(countAfterCreate, countBefore + 3); - - // Mark one as error - await removeActionByError(action1.id, "corrector003", "Error"); - - // Count should decrease by 1 (original action marked as REMOVED_BY_ERROR) - // Note: The correction record is created with REMOVED_BY_ERROR status, so it's never counted - const countAfterRemoval = await getTotalActionCount(); - assert.strictEqual(countAfterRemoval, countBefore + 2); - }); - }); - - describe("Database Structure", () => { - it("should have all required tables", async () => { - const tables = await testPrisma.$queryRaw>` - SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_prisma%' - `; - - const tableNames = tables.map((table) => table.name); - assert.ok(tableNames.includes("User")); - assert.ok(tableNames.includes("ModerationAction")); - }); - - it("should have proper indexes", async () => { - const indexes = await testPrisma.$queryRaw>` - SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' - `; - - const indexNames = indexes.map((index) => index.name); - assert.ok(indexNames.some((name) => name.includes("discordId"))); - assert.ok(indexNames.some((name) => name.includes("targetId"))); - assert.ok(indexNames.some((name) => name.includes("moderatorId"))); - }); - - it("should enforce unique constraint on discordId", async () => { - await testPrisma.user.create({ - data: { discordId: "unique001" }, - }); - - await assert.rejects( - async () => { - await testPrisma.user.create({ - data: { discordId: "unique001" }, - }); - }, - { - name: "PrismaClientKnownRequestError", - } - ); - }); - }); -}); From d228f8cb799439603923c2ed93a33a69d8572696 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 9 Nov 2025 15:33:17 +0100 Subject: [PATCH 13/21] fix: correct spelling of 'DISRESPECTFUL' in ActionReason enum in Prisma schema --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6fb5781..329c506 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -44,7 +44,7 @@ enum ActionStatus { enum ActionReason { SPAM SCAM - DISRESPCTING + DISRESPECTFUL NSFW HATE_SPEECH SELF_PROMOTION From ed3e99718b12dfa214dd1aa237c865295b091959 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 9 Nov 2025 15:34:37 +0100 Subject: [PATCH 14/21] chore: remove outdated database implementation plan and related files --- ...abase-implementation-plan-b7b83c34.plan.md | 479 ------------------ 1 file changed, 479 deletions(-) delete mode 100644 .cursor/plans/database-implementation-plan-b7b83c34.plan.md diff --git a/.cursor/plans/database-implementation-plan-b7b83c34.plan.md b/.cursor/plans/database-implementation-plan-b7b83c34.plan.md deleted file mode 100644 index 8a56635..0000000 --- a/.cursor/plans/database-implementation-plan-b7b83c34.plan.md +++ /dev/null @@ -1,479 +0,0 @@ - -# Database Implementation for Discord Moderation Tool - -## Overview - -Implement SQLite database with Prisma to track all moderation actions (warn, mute, unmute, kick, ban, unban, timeout, remove_timeout, repel) with full audit history and error correction capabilities. This plan focuses solely on database setup and operations - Discord bot command integration will be handled separately. - -## Current State - -- ✅ Prisma installed (`@prisma/client`, `@prisma/adapter-better-sqlite3`) -- ✅ Basic User model in `prisma/schema.prisma` -- ✅ Docker setup complete with profiles (dev/prod) -- ✅ Volume persistence configured (`./data:/app/data`) - -## Exact Database Schema - -Replace `prisma/schema.prisma` with: - -```prisma -generator client { - provider = "prisma-client-js" - output = "../generated/prisma" -} - -datasource db { - provider = "sqlite" - url = "file:../data/dev.db" -} - -model User { - id Int @id @default(autoincrement()) - discordId String @unique - - // Relations - actionsReceived ModerationAction[] @relation("ActionTarget") - actionsPerformed ModerationAction[] @relation("ActionModerator") - - @@index([discordId]) -} - -enum ActionType { - WARN - MUTE - UNMUTE - KICK - BAN - UNBAN - TIMEOUT - REMOVE_TIMEOUT - REPEL -} - -enum ActionStatus { - ACTIVE - EXPIRED - REMOVED_BY_ERROR - REVERSED -} - -model ModerationAction { - id Int @id @default(autoincrement()) - type ActionType - status ActionStatus @default(ACTIVE) - reason String? - duration Int? // Duration in seconds for timeouts/mutes - - // User relationships - targetId Int - target User @relation("ActionTarget", fields: [targetId], references: [id]) - moderatorId Int - moderator User @relation("ActionModerator", fields: [moderatorId], references: [id]) - - // Timestamps - createdAt DateTime @default(now()) - expiresAt DateTime? - - // For corrections/reversals - parentActionId Int? - parentAction ModerationAction? @relation("ActionCorrections", fields: [parentActionId], references: [id]) - corrections ModerationAction[] @relation("ActionCorrections") - - @@index([targetId]) - @@index([moderatorId]) - @@index([status]) - @@index([createdAt]) -} -``` - -## Database Operations Module - -Create `src/database/operations.ts`: - -```typescript -import { PrismaClient, ActionType, ActionStatus } from "../../generated/prisma/index.js"; - -export const prisma = new PrismaClient(); - -// Connect to database and verify connection -export async function connectDatabase() { - try { - await prisma.$connect(); - // Test query to verify connection - await prisma.user.findFirst(); - console.log("✅ Database connected successfully"); - } catch (error) { - console.error("❌ Database connection failed:", error); - throw error; - } -} - -// Disconnect from database -export async function disconnectDatabase() { - await prisma.$disconnect(); - console.log("Database disconnected"); -} - -// User operations - only Discord ID needed -export async function upsertUser(discordId: string) { - return await prisma.user.upsert({ - where: { discordId }, - update: {}, - create: { discordId }, - }); -} - -export async function getUserByDiscordId(discordId: string) { - return await prisma.user.findUnique({ - where: { discordId }, - }); -} - -// Moderation action operations -type CreateActionParams = { - type: ActionType; - targetDiscordId: string; - moderatorDiscordId: string; - reason?: string; - duration?: number; -}; - -export async function createModerationAction(params: CreateActionParams) { - // Ensure both users exist - const target = await upsertUser(params.targetDiscordId); - const moderator = await upsertUser(params.moderatorDiscordId); - - // Calculate expiry for timed actions - const expiresAt = params.duration - ? new Date(Date.now() + params.duration * 1000) - : null; - - return await prisma.moderationAction.create({ - data: { - type: params.type, - reason: params.reason, - duration: params.duration, - targetId: target.id, - moderatorId: moderator.id, - expiresAt, - }, - include: { - target: true, - moderator: true, - }, - }); -} - -export async function removeActionByError( - actionId: number, - removedByDiscordId: string, - reason: string -) { - const removedBy = await upsertUser(removedByDiscordId); - - // Mark original action as removed - const originalAction = await prisma.moderationAction.update({ - where: { id: actionId }, - data: { status: ActionStatus.REMOVED_BY_ERROR }, - }); - - // Create correction record - return await prisma.moderationAction.create({ - data: { - type: originalAction.type, - status: ActionStatus.REMOVED_BY_ERROR, - reason: `Correction: ${reason}`, - targetId: originalAction.targetId, - moderatorId: removedBy.id, - parentActionId: originalAction.id, - }, - include: { - parentAction: true, - }, - }); -} - -export async function getUserActions(discordId: string, limit = 50) { - const user = await getUserByDiscordId(discordId); - if (!user) return []; - - return await prisma.moderationAction.findMany({ - where: { targetId: user.id }, - include: { - moderator: true, - }, - orderBy: { createdAt: 'desc' }, - take: limit, - }); -} - -export async function getActiveActions(discordId: string) { - const user = await getUserByDiscordId(discordId); - if (!user) return []; - - return await prisma.moderationAction.findMany({ - where: { - targetId: user.id, - status: ActionStatus.ACTIVE, - }, - include: { - moderator: true, - }, - }); -} - -export async function getActionById(actionId: number) { - return await prisma.moderationAction.findUnique({ - where: { id: actionId }, - include: { - target: true, - moderator: true, - parentAction: true, - }, - }); -} - -export async function updateActionStatus(actionId: number, status: ActionStatus) { - return await prisma.moderationAction.update({ - where: { id: actionId }, - data: { status }, - }); -} -``` - -## Analytics Module - -Create `src/database/analytics.ts`: - -```typescript -import { prisma } from "./operations.js"; -import { ActionType, ActionStatus } from "../../generated/prisma/index.js"; - -export async function getUserActionStats(discordId: string) { - const user = await prisma.user.findUnique({ - where: { discordId }, - include: { - actionsReceived: { - where: { - status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] } - } - } - } - }); - - if (!user) return null; - - const actionsByType = user.actionsReceived.reduce((acc, action) => { - acc[action.type] = (acc[action.type] || 0) + 1; - return acc; - }, {} as Record); - - return { - discordId: user.discordId, - totalActions: user.actionsReceived.length, - actionsByType, - }; -} - -export async function getModeratorStats(discordId: string, days = 30) { - const user = await prisma.user.findUnique({ - where: { discordId }, - }); - - if (!user) return null; - - const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000); - - const actions = await prisma.moderationAction.findMany({ - where: { - moderatorId: user.id, - createdAt: { gte: since }, - status: { not: ActionStatus.REMOVED_BY_ERROR } - } - }); - - const actionsByType = actions.reduce((acc, action) => { - acc[action.type] = (acc[action.type] || 0) + 1; - return acc; - }, {} as Record); - - return { - discordId: user.discordId, - totalActions: actions.length, - actionsByType, - period: `${days} days`, - }; -} - -export async function getRecentActions(limit = 20) { - return await prisma.moderationAction.findMany({ - include: { - target: true, - moderator: true, - }, - orderBy: { createdAt: 'desc' }, - take: limit, - }); -} - -export async function findRepeatOffenders(minActions = 3) { - const actions = await prisma.moderationAction.findMany({ - where: { - status: { in: [ActionStatus.ACTIVE, ActionStatus.EXPIRED] } - }, - include: { - target: true, - } - }); - - const userActionCounts = actions.reduce((acc, action) => { - const discordId = action.target.discordId; - if (!acc[discordId]) { - acc[discordId] = { discordId, count: 0, actions: [] }; - } - acc[discordId].count++; - acc[discordId].actions.push(action); - return acc; - }, {} as Record); - - return Object.values(userActionCounts) - .filter(item => item.count >= minActions) - .sort((a, b) => b.count - a.count); -} - -export async function getActionsByType(actionType: ActionType, limit = 50) { - return await prisma.moderationAction.findMany({ - where: { type: actionType }, - include: { - target: true, - moderator: true, - }, - orderBy: { createdAt: 'desc' }, - take: limit, - }); -} - -export async function getTotalActionCount() { - return await prisma.moderationAction.count({ - where: { - status: { not: ActionStatus.REMOVED_BY_ERROR } - } - }); -} -``` - -## Initialize Database in Main - -Update `index.ts` to initialize Prisma with explicit connection: - -```typescript -import { connectDatabase, disconnectDatabase } from "./src/database/operations.js"; - -// ... existing imports and code ... - -// Connect to database before starting bot -await connectDatabase(); - -client.once(Events.ClientReady, (readyClient) => { - console.log(`Ready! Logged in as ${readyClient.user.tag}`); -}); - -// Graceful shutdown -process.on('SIGINT', async () => { - console.log("Shutting down..."); - await disconnectDatabase(); - process.exit(0); -}); - -process.on('SIGTERM', async () => { - console.log("Shutting down..."); - await disconnectDatabase(); - process.exit(0); -}); -``` - -## Implementation Steps - -1. **Update Schema**: Replace `prisma/schema.prisma` with new schema -2. **Run Migration**: `pnpm prisma migrate dev --name add_moderation_tables` -3. **Generate Client**: `pnpm prisma generate` -4. **Create Operations**: Add `src/database/operations.ts` with all CRUD functions -5. **Create Analytics**: Add `src/database/analytics.ts` with analytics queries -6. **Initialize Database**: Update `index.ts` with connectDatabase/disconnectDatabase calls -7. **Test Connection**: Run bot and verify database connection logs - -## Volume Management - -Database location: `./data/dev.db` - -```bash -# Run development -docker-compose --profile dev up - -# Run production -docker-compose --profile prod up -d - -# Backup database -cp ./data/dev.db ./backup_$(date +%Y%m%d).db - -# Restore database -cp ./backup_20250101.db ./data/dev.db - -# View database with sqlite3 -sqlite3 ./data/dev.db - -# View all tables -sqlite3 ./data/dev.db ".tables" - -# View schema -sqlite3 ./data/dev.db ".schema" -``` - -## Files to Create - -- `src/database/operations.ts` - Database CRUD operations (Discord ID only) -- `src/database/analytics.ts` - Analytics and reporting queries - -## Files to Modify - -- `prisma/schema.prisma` - Complete schema definition -- `index.ts` - Initialize database connection with explicit connect/disconnect - -## Usage Examples - -After implementation, Discord commands can use the database operations: - -```typescript -// Create a ban action -await createModerationAction({ - type: ActionType.BAN, - targetDiscordId: "123456789", - moderatorDiscordId: "987654321", - reason: "Spam", -}); - -// Get user's action history -const actions = await getUserActions("123456789"); - -// Get user statistics -const stats = await getUserActionStats("123456789"); - -// Find repeat offenders -const offenders = await findRepeatOffenders(3); -``` - -### To-dos - -- [ ] Update prisma/schema.prisma with User, ActionType, ActionStatus, and ModerationAction models -- [ ] Create and run Prisma migration: pnpm prisma migrate dev --name add_moderation_tables -- [ ] Create src/database/operations.ts with CRUD functions for users and moderation actions -- [ ] Create src/database/analytics.ts with analytics query helpers -- [ ] Update index.ts to initialize Prisma client and handle graceful shutdown -- [ ] Create src/commands/ban/index.ts command with database logging -- [ ] Create src/commands/kick/index.ts command with database logging -- [ ] Create src/commands/warn/index.ts command with database logging -- [ ] Create src/commands/timeout/index.ts command with database logging -- [ ] Create src/commands/repel/index.ts command with database logging -- [ ] Create src/commands/remove-action/index.ts for correcting errors -- [ ] Create src/commands/history/index.ts to view user moderation history -- [ ] Update src/commands/index.ts to register all new moderation commands \ No newline at end of file From fa1e5551a37754758bdea1e6c6b00281a19e4f3b Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 9 Nov 2025 16:02:21 +0100 Subject: [PATCH 15/21] chore: update docker-compose and Dockerfile to use named volumes and add migration service --- Dockerfile | 9 +++++++++ docker-compose.yml | 27 ++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index a063c17..ed48a78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,9 @@ FROM deps-dev AS build COPY . . +# Generate Prisma client +RUN npx prisma generate + RUN pnpm run build:ci # Production stage - Minimal runtime image @@ -39,7 +42,10 @@ COPY --from=deps /app/node_modules ./node_modules # Copy built application COPY --from=build /app/dist ./dist +COPY --from=build /app/generated ./generated +COPY --from=build /app/prisma ./prisma COPY package.json ./ +COPY prisma.config.ts ./ # Create data directory and set permissions for node user RUN mkdir -p /app/data && chown -R node:node /app/data @@ -56,6 +62,9 @@ ENV NODE_ENV=development COPY . . +# Generate Prisma client +RUN npx prisma generate + # Create data directory and set permissions for node user RUN mkdir -p /app/data && chown -R node:node /app/data diff --git a/docker-compose.yml b/docker-compose.yml index e3f90ae..f55bf56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,8 +14,8 @@ services: environment: - NODE_ENV=production volumes: - - ./logs:/app/logs - - ./data:/app/data + - prod_logs:/app/logs + - prod_data:/app/data # Named volume - persists outside repo discord-bot-dev: build: @@ -34,4 +34,25 @@ services: - ./logs:/app/logs - ./data:/app/data ports: - - "9229:9229" # For debugging \ No newline at end of file + - "9229:9229" # For debugging + + # Run database migrations manually when ready + migrate: + build: + context: . + dockerfile: Dockerfile + target: production + container_name: moderation-tool-migrate + profiles: ["tools"] + env_file: + - .env.local + volumes: + - prod_data:/app/data # Use same named volume as production + command: npx prisma migrate deploy + +# Named volumes - persist outside the repo directory +volumes: + prod_data: + name: discord-moderation-data + prod_logs: + name: discord-moderation-logs \ No newline at end of file From f9a3861f3e3e0aa96879542c0d6699ee9d891a85 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 9 Nov 2025 16:05:29 +0100 Subject: [PATCH 16/21] chore: update datasource URL in Prisma configuration to point to moderation database --- prisma.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma.config.ts b/prisma.config.ts index e05563c..e33aad4 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -7,6 +7,6 @@ export default defineConfig({ }, engine: "classic", datasource: { - url: "file:./dev.db", + url: "file:../data/moderation.db", // Match schema.prisma }, }); From 12cacc9a5e1ebd57e15080038766299c57dc3efa Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Sun, 9 Nov 2025 16:07:43 +0100 Subject: [PATCH 17/21] chore: remove unused script.ts file and related database connection logic --- script.ts | 25 ------------------------- src/index.ts | 6 ------ 2 files changed, 31 deletions(-) delete mode 100644 script.ts diff --git a/script.ts b/script.ts deleted file mode 100644 index 733394d..0000000 --- a/script.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { PrismaClient } from "./generated/prisma/client.js"; - -const prisma = new PrismaClient(); - -async function main() { - const user = await prisma.user.create({ - data: { - discordId: "883932482734", - }, - }); - console.log(user); - - const users = await prisma.user.findMany(); - console.log(users); -} - -main() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (e) => { - console.error(e); - await prisma.$disconnect(); - process.exit(1); - }); diff --git a/src/index.ts b/src/index.ts index eb590a1..dfd5e55 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,10 @@ import { Client, GatewayIntentBits } from "discord.js"; -import { connectDatabase, disconnectDatabase } from "./database/operations.js"; import { config } from "./env.js"; import { loadCommands, registerCommands } from "./utils/commands.js"; import { loadEvents } from "./utils/events.js"; const client = new Client({ intents: [GatewayIntentBits.Guilds] }); -// Connect to database before starting bot -await connectDatabase(); - loadCommands(client); loadEvents(client); registerCommands(); @@ -16,13 +12,11 @@ registerCommands(); // Graceful shutdown process.on("SIGINT", async () => { console.log("Shutting down..."); - await disconnectDatabase(); process.exit(0); }); process.on("SIGTERM", async () => { console.log("Shutting down..."); - await disconnectDatabase(); process.exit(0); }); From b6f5e462008c02f01f0905dd0a95b1edf94f73ab Mon Sep 17 00:00:00 2001 From: Wiktoria van Harneveldt <62669899+wiktoriavh@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:34:09 +0100 Subject: [PATCH 18/21] Update prisma/schema.prisma Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 329c506..0c4d34e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -71,7 +71,7 @@ model Action { type ActionType status ActionStatus @default(ACTIVE) moderatorUserId Int - reason String + reason ActionReason note String? createdAt Int expiresAt Int? From d4ec19298c175c0fc1ca3c2b8b5241a3721fe696 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Fri, 21 Nov 2025 20:52:49 +0100 Subject: [PATCH 19/21] chore: add new Prisma scripts for database migration, validation, and formatting; update dependencies --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 9c7eea3..2a047cf 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "format": "biome format --write .", "check": "biome check .", "check:fix": "biome check --write .", + "db:migrate": "prisma migrate dev", + "db:validate": "prisma validate", + "db:format": "prisma format", "prepare": "husky", "pre-commit": "lint-staged", "typecheck": "tsc --noEmit" From 07db5779f79e4096a3090ecde3cfe6773b5bba1c Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Fri, 21 Nov 2025 20:54:39 +0100 Subject: [PATCH 20/21] chore: upgrade Prisma dependencies to version 7.0.0 and update related lockfile entries --- package.json | 6 +- pnpm-lock.yaml | 563 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 500 insertions(+), 69 deletions(-) diff --git a/package.json b/package.json index 2a047cf..f44adfb 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "license": "MIT", "packageManager": "pnpm@10.17.1", "dependencies": { - "@prisma/adapter-better-sqlite3": "^6.18.0", - "@prisma/client": "^6.18.0", + "@prisma/adapter-better-sqlite3": "^7.0.0", + "@prisma/client": "^7.0.0", "discord.js": "^14.22.1" }, "devDependencies": { @@ -33,7 +33,7 @@ "@types/node": "^24.5.2", "husky": "^9.1.7", "lint-staged": "^16.2.1", - "prisma": "^6.18.0", + "prisma": "^7.0.0", "tsx": "^4.20.6", "typescript": "^5.9.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ca1e7e..7d9c33b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: dependencies: '@prisma/adapter-better-sqlite3': - specifier: ^6.18.0 - version: 6.18.0 + specifier: ^7.0.0 + version: 7.0.0 '@prisma/client': - specifier: ^6.18.0 - version: 6.18.0(prisma@6.18.0(typescript@5.9.2))(typescript@5.9.2) + specifier: ^7.0.0 + version: 7.0.0(prisma@7.0.0(@types/react@19.2.6)(better-sqlite3@11.10.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.2))(typescript@5.9.2) discord.js: specifier: ^14.22.1 version: 14.22.1 @@ -31,8 +31,8 @@ importers: specifier: ^16.2.1 version: 16.2.1 prisma: - specifier: ^6.18.0 - version: 6.18.0(typescript@5.9.2) + specifier: ^7.0.0 + version: 7.0.0(@types/react@19.2.6)(better-sqlite3@11.10.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.2) tsx: specifier: ^4.20.6 version: 4.20.6 @@ -95,6 +95,18 @@ packages: cpu: [x64] os: [win32] + '@chevrotain/cst-dts-gen@10.5.0': + resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==} + + '@chevrotain/gast@10.5.0': + resolution: {integrity: sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==} + + '@chevrotain/types@10.5.0': + resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==} + + '@chevrotain/utils@10.5.0': + resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + '@discordjs/builders@1.11.3': resolution: {integrity: sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw==} engines: {node: '>=16.11.0'} @@ -123,6 +135,20 @@ packages: resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} engines: {node: '>=16.11.0'} + '@electric-sql/pglite-socket@0.0.6': + resolution: {integrity: sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.3.2 + + '@electric-sql/pglite-tools@0.2.7': + resolution: {integrity: sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg==} + peerDependencies: + '@electric-sql/pglite': 0.3.2 + + '@electric-sql/pglite@0.3.2': + resolution: {integrity: sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==} + '@esbuild/aix-ppc64@0.25.10': resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} @@ -279,41 +305,73 @@ packages: cpu: [x64] os: [win32] - '@prisma/adapter-better-sqlite3@6.18.0': - resolution: {integrity: sha512-YBok05ezVdJ0j62bkvVEb7Pf1jkglOXpWgBzU+lolZg+131cD3gSwTWoiNRmA+74HI6dHhOjzpsod50DiMoI2w==} + '@hono/node-server@1.14.2': + resolution: {integrity: sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@mrleebo/prisma-ast@0.12.1': + resolution: {integrity: sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w==} + engines: {node: '>=16'} + + '@prisma/adapter-better-sqlite3@7.0.0': + resolution: {integrity: sha512-a0ltJcisHuudnzgD822TwTrvxpQ84ZXUk/3WJLOIGrAM/tysLMb7Q4n0uZeCofvh+UMRoGcT2B6TS3T9/TLMUA==} + + '@prisma/client-runtime-utils@7.0.0': + resolution: {integrity: sha512-PAiFgMBPrLSaakBwUpML5NevipuKSL3rtNr8pZ8CZ3OBXo0BFcdeGcBIKw/CxJP6H4GNa4+l5bzJPrk8Iq6tDw==} - '@prisma/client@6.18.0': - resolution: {integrity: sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==} - engines: {node: '>=18.18'} + '@prisma/client@7.0.0': + resolution: {integrity: sha512-FM1NtJezl0zH3CybLxcbJwShJt7xFGSRg+1tGhy3sCB8goUDnxnBR+RC/P35EAW8gjkzx7kgz7bvb0MerY2VSw==} + engines: {node: ^20.19 || ^22.12 || ^24.0} peerDependencies: prisma: '*' - typescript: '>=5.1.0' + typescript: '>=5.4.0' peerDependenciesMeta: prisma: optional: true typescript: optional: true - '@prisma/config@6.18.0': - resolution: {integrity: sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==} + '@prisma/config@7.0.0': + resolution: {integrity: sha512-TDASB57hyGUwHB0IPCSkoJcXFrJOKA1+R/1o4np4PbS+E0F5MiY5aAyUttO0mSuNQaX7t8VH/GkDemffF1mQzg==} + + '@prisma/debug@6.8.2': + resolution: {integrity: sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==} + + '@prisma/debug@7.0.0': + resolution: {integrity: sha512-SdS3qzfMASHtWimywtkiRcJtrHzacbmMVhElko3DYUZSB0TTLqRYWpddRBJdeGgSLmy1FD55p7uGzIJ+MtfhMg==} + + '@prisma/dev@0.13.0': + resolution: {integrity: sha512-QMmF6zFeUF78yv1HYbHvod83AQnl7u6NtKyDhTRZOJup3h1icWs8R7RUVxBJZvM2tBXNAMpLQYYM/8kPlOPegA==} - '@prisma/debug@6.18.0': - resolution: {integrity: sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==} + '@prisma/driver-adapter-utils@7.0.0': + resolution: {integrity: sha512-ZEvzFaIapnfNKFPgZu/Zy4g6jfO5C0ZmMp+IjO9hNKNDwVKrDlBKw7F3Y9oRK0U0kfb9lKWP4Dz7DgtKs4TTbA==} - '@prisma/driver-adapter-utils@6.18.0': - resolution: {integrity: sha512-9wgSriEKs4j1ePxlv1/RNfJV9Gu5rzG37Neshg+DfrCcUY3amroERvTjyR04w5J1THdGdOTgGL9VdJcVaKRMmQ==} + '@prisma/engines-version@6.20.0-16.next-0c19ccc313cf9911a90d99d2ac2eb0280c76c513': + resolution: {integrity: sha512-7bzyN8Gp9GbDFbTDzVUH9nFcgRWvsWmjrGgBJvIC/zEoAuv/lx62gZXgAKfjn/HoPkxz/dS+TtsnduFx8WA+cw==} - '@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f': - resolution: {integrity: sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==} + '@prisma/engines@7.0.0': + resolution: {integrity: sha512-ojCL3OFLMCz33UbU9XwH32jwaeM+dWb8cysTuY8eK6ZlMKXJdy6ogrdG3MGB3meKLGdQBmOpUUGJ7eLIaxbrcg==} - '@prisma/engines@6.18.0': - resolution: {integrity: sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==} + '@prisma/fetch-engine@7.0.0': + resolution: {integrity: sha512-qcyWTeWDjVDaDQSrVIymZU1xCYlvmwCzjA395lIuFjUESOH3YQCb8i/hpd4vopfq3fUR4v6+MjjtIGvnmErQgw==} - '@prisma/fetch-engine@6.18.0': - resolution: {integrity: sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==} + '@prisma/get-platform@6.8.2': + resolution: {integrity: sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==} - '@prisma/get-platform@6.18.0': - resolution: {integrity: sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==} + '@prisma/get-platform@7.0.0': + resolution: {integrity: sha512-zyhzrAa+y/GfyCzTnuk0D9lfkvDzo7IbsNyuhTqhPu/AN0txm0x26HAR4tJLismla/fHf5fBzYwSivYSzkpakg==} + + '@prisma/query-plan-executor@6.18.0': + resolution: {integrity: sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA==} + + '@prisma/studio-core-licensed@0.8.0': + resolution: {integrity: sha512-SXCcgFvo/SC6/11kEOaQghJgCWNEWZUvPYKn/gpvMB9HLSG/5M8If7dWZtEQHhchvl8bh9A89Hw6mEKpsXFimA==} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 '@sapphire/async-queue@1.5.5': resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} @@ -333,6 +391,9 @@ packages: '@types/node@24.5.2': resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + '@types/react@19.2.6': + resolution: {integrity: sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -352,6 +413,10 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -379,6 +444,9 @@ packages: magicast: optional: true + chevrotain@10.5.0: + resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -411,6 +479,13 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -426,6 +501,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -473,8 +552,8 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - exsolve@1.0.7: - resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} fast-check@3.23.2: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} @@ -490,6 +569,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -498,10 +581,16 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-port-please@3.1.2: + resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} @@ -512,11 +601,28 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grammex@3.1.11: + resolution: {integrity: sha512-HNwLkgRg9SqTAd1N3Uh/MnKwTBTzwBxTOPbXQ8pb0tpwydjk90k4zRE8JUn9fMUiRwKtXFZ1TWFmms3dZHN+Fg==} + + hono@4.7.10: + resolution: {integrity: sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==} + engines: {node: '>=16.9.0'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -534,10 +640,20 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + lint-staged@16.2.1: resolution: {integrity: sha512-KMeYmH9wKvHsXdUp+z6w7HN3fHKHXwT1pSTQTYxB9kI6ekK1rlL3kLZEoXZCppRPXFK9PFW/wfQctV7XUqMrPQ==} engines: {node: '>=20.17'} @@ -557,6 +673,17 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.3: + resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + magic-bytes.js@1.12.1: resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} @@ -578,6 +705,14 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + nano-spawn@1.0.3: resolution: {integrity: sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==} engines: {node: '>=20.17'} @@ -585,8 +720,8 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - node-abi@3.80.0: - resolution: {integrity: sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==} + node-abi@3.85.0: + resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} engines: {node: '>=10'} node-fetch-native@1.6.7: @@ -607,6 +742,10 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -625,21 +764,31 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} hasBin: true - prisma@6.18.0: - resolution: {integrity: sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==} - engines: {node: '>=18.18'} + prisma@7.0.0: + resolution: {integrity: sha512-VZObZ1pQV/OScarYg68RYUx61GpFLH2mJGf9fUX4XxQxTst/6ZK7nkY86CSZ3zBW6U9lKRTsBrZWVz20X5G/KQ==} + engines: {node: ^20.19 || ^22.12 || ^24.0} hasBin: true peerDependencies: - typescript: '>=5.1.0' + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' peerDependenciesMeta: + better-sqlite3: + optional: true typescript: optional: true + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -653,6 +802,15 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -661,6 +819,12 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + regexp-to-ast@0.5.0: + resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} + + remeda@2.21.3: + resolution: {integrity: sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -668,17 +832,41 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -693,6 +881,13 @@ packages: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -723,8 +918,9 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tinyexec@1.0.1: - resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -744,6 +940,10 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -759,6 +959,19 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@1.1.0: + resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -783,6 +996,9 @@ packages: engines: {node: '>= 14.6'} hasBin: true + zeptomatch@2.0.2: + resolution: {integrity: sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g==} + snapshots: '@biomejs/biome@2.2.4': @@ -820,6 +1036,21 @@ snapshots: '@biomejs/cli-win32-x64@2.2.4': optional: true + '@chevrotain/cst-dts-gen@10.5.0': + dependencies: + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + + '@chevrotain/gast@10.5.0': + dependencies: + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + + '@chevrotain/types@10.5.0': {} + + '@chevrotain/utils@10.5.0': {} + '@discordjs/builders@1.11.3': dependencies: '@discordjs/formatters': 0.6.1 @@ -867,6 +1098,16 @@ snapshots: - bufferutil - utf-8-validate + '@electric-sql/pglite-socket@0.0.6(@electric-sql/pglite@0.3.2)': + dependencies: + '@electric-sql/pglite': 0.3.2 + + '@electric-sql/pglite-tools@0.2.7(@electric-sql/pglite@0.3.2)': + dependencies: + '@electric-sql/pglite': 0.3.2 + + '@electric-sql/pglite@0.3.2': {} + '@esbuild/aix-ppc64@0.25.10': optional: true @@ -945,17 +1186,30 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true - '@prisma/adapter-better-sqlite3@6.18.0': + '@hono/node-server@1.14.2(hono@4.7.10)': dependencies: - '@prisma/driver-adapter-utils': 6.18.0 + hono: 4.7.10 + + '@mrleebo/prisma-ast@0.12.1': + dependencies: + chevrotain: 10.5.0 + lilconfig: 2.1.0 + + '@prisma/adapter-better-sqlite3@7.0.0': + dependencies: + '@prisma/driver-adapter-utils': 7.0.0 better-sqlite3: 11.10.0 - '@prisma/client@6.18.0(prisma@6.18.0(typescript@5.9.2))(typescript@5.9.2)': + '@prisma/client-runtime-utils@7.0.0': {} + + '@prisma/client@7.0.0(prisma@7.0.0(@types/react@19.2.6)(better-sqlite3@11.10.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.2))(typescript@5.9.2)': + dependencies: + '@prisma/client-runtime-utils': 7.0.0 optionalDependencies: - prisma: 6.18.0(typescript@5.9.2) + prisma: 7.0.0(@types/react@19.2.6)(better-sqlite3@11.10.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.2) typescript: 5.9.2 - '@prisma/config@6.18.0': + '@prisma/config@7.0.0': dependencies: c12: 3.1.0 deepmerge-ts: 7.1.5 @@ -964,30 +1218,66 @@ snapshots: transitivePeerDependencies: - magicast - '@prisma/debug@6.18.0': {} + '@prisma/debug@6.8.2': {} + + '@prisma/debug@7.0.0': {} + + '@prisma/dev@0.13.0(typescript@5.9.2)': + dependencies: + '@electric-sql/pglite': 0.3.2 + '@electric-sql/pglite-socket': 0.0.6(@electric-sql/pglite@0.3.2) + '@electric-sql/pglite-tools': 0.2.7(@electric-sql/pglite@0.3.2) + '@hono/node-server': 1.14.2(hono@4.7.10) + '@mrleebo/prisma-ast': 0.12.1 + '@prisma/get-platform': 6.8.2 + '@prisma/query-plan-executor': 6.18.0 + foreground-child: 3.3.1 + get-port-please: 3.1.2 + hono: 4.7.10 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.21.3 + std-env: 3.9.0 + valibot: 1.1.0(typescript@5.9.2) + zeptomatch: 2.0.2 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.0.0': + dependencies: + '@prisma/debug': 7.0.0 + + '@prisma/engines-version@6.20.0-16.next-0c19ccc313cf9911a90d99d2ac2eb0280c76c513': {} - '@prisma/driver-adapter-utils@6.18.0': + '@prisma/engines@7.0.0': dependencies: - '@prisma/debug': 6.18.0 + '@prisma/debug': 7.0.0 + '@prisma/engines-version': 6.20.0-16.next-0c19ccc313cf9911a90d99d2ac2eb0280c76c513 + '@prisma/fetch-engine': 7.0.0 + '@prisma/get-platform': 7.0.0 - '@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f': {} + '@prisma/fetch-engine@7.0.0': + dependencies: + '@prisma/debug': 7.0.0 + '@prisma/engines-version': 6.20.0-16.next-0c19ccc313cf9911a90d99d2ac2eb0280c76c513 + '@prisma/get-platform': 7.0.0 - '@prisma/engines@6.18.0': + '@prisma/get-platform@6.8.2': dependencies: - '@prisma/debug': 6.18.0 - '@prisma/engines-version': 6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f - '@prisma/fetch-engine': 6.18.0 - '@prisma/get-platform': 6.18.0 + '@prisma/debug': 6.8.2 - '@prisma/fetch-engine@6.18.0': + '@prisma/get-platform@7.0.0': dependencies: - '@prisma/debug': 6.18.0 - '@prisma/engines-version': 6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f - '@prisma/get-platform': 6.18.0 + '@prisma/debug': 7.0.0 + + '@prisma/query-plan-executor@6.18.0': {} - '@prisma/get-platform@6.18.0': + '@prisma/studio-core-licensed@0.8.0(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@prisma/debug': 6.18.0 + '@types/react': 19.2.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) '@sapphire/async-queue@1.5.5': {} @@ -1004,6 +1294,10 @@ snapshots: dependencies: undici-types: 7.12.0 + '@types/react@19.2.6': + dependencies: + csstype: 3.2.3 + '@types/ws@8.18.1': dependencies: '@types/node': 24.5.2 @@ -1018,6 +1312,8 @@ snapshots: ansi-styles@6.2.3: {} + aws-ssl-profiles@1.1.2: {} + base64-js@1.5.1: {} better-sqlite3@11.10.0: @@ -1050,7 +1346,7 @@ snapshots: confbox: 0.2.2 defu: 6.1.4 dotenv: 16.6.1 - exsolve: 1.0.7 + exsolve: 1.0.8 giget: 2.0.0 jiti: 2.6.1 ohash: 2.0.11 @@ -1059,6 +1355,15 @@ snapshots: pkg-types: 2.3.0 rc9: 2.1.2 + chevrotain@10.5.0: + dependencies: + '@chevrotain/cst-dts-gen': 10.5.0 + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + '@chevrotain/utils': 10.5.0 + lodash: 4.17.21 + regexp-to-ast: 0.5.0 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -1086,6 +1391,14 @@ snapshots: consola@3.4.2: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -1096,6 +1409,8 @@ snapshots: defu@6.1.4: {} + denque@2.1.0: {} + destr@2.0.5: {} detect-libc@2.1.2: {} @@ -1171,7 +1486,7 @@ snapshots: expand-template@2.0.3: {} - exsolve@1.0.7: {} + exsolve@1.0.8: {} fast-check@3.23.2: dependencies: @@ -1185,13 +1500,24 @@ snapshots: dependencies: to-regex-range: 5.0.1 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fs-constants@1.0.0: {} fsevents@2.3.3: optional: true + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + get-east-asian-width@1.4.0: {} + get-port-please@3.1.2: {} + get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -1207,8 +1533,20 @@ snapshots: github-from-package@0.0.0: {} + graceful-fs@4.2.11: {} + + grammex@3.1.11: {} + + hono@4.7.10: {} + + http-status-codes@2.3.0: {} + husky@9.1.7: {} + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} inherits@2.0.4: {} @@ -1221,8 +1559,14 @@ snapshots: is-number@7.0.0: {} + is-property@1.0.2: {} + + isexe@2.0.0: {} + jiti@2.6.1: {} + lilconfig@2.1.0: {} + lint-staged@16.2.1: dependencies: commander: 14.0.1 @@ -1254,6 +1598,12 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.2 + long@5.3.2: {} + + lru-cache@7.18.3: {} + + lru.min@1.1.3: {} + magic-bytes.js@1.12.1: {} micromatch@4.0.8: @@ -1269,11 +1619,27 @@ snapshots: mkdirp-classic@0.5.3: {} + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.0 + long: 5.3.2 + lru.min: 1.1.3 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + nano-spawn@1.0.3: {} napi-build-utils@2.0.0: {} - node-abi@3.80.0: + node-abi@3.85.0: dependencies: semver: 7.7.3 @@ -1285,7 +1651,7 @@ snapshots: consola: 3.4.2 pathe: 2.0.3 pkg-types: 2.3.0 - tinyexec: 1.0.1 + tinyexec: 1.0.2 ohash@2.0.11: {} @@ -1297,6 +1663,8 @@ snapshots: dependencies: mimic-function: 5.0.1 + path-key@3.1.1: {} + pathe@2.0.3: {} perfect-debounce@1.0.0: {} @@ -1308,9 +1676,11 @@ snapshots: pkg-types@2.3.0: dependencies: confbox: 0.2.2 - exsolve: 1.0.7 + exsolve: 1.0.8 pathe: 2.0.3 + postgres@3.4.7: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -1319,21 +1689,35 @@ snapshots: minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 2.0.0 - node-abi: 3.80.0 + node-abi: 3.85.0 pump: 3.0.3 rc: 1.2.8 simple-get: 4.0.1 tar-fs: 2.1.4 tunnel-agent: 0.6.0 - prisma@6.18.0(typescript@5.9.2): + prisma@7.0.0(@types/react@19.2.6)(better-sqlite3@11.10.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.2): dependencies: - '@prisma/config': 6.18.0 - '@prisma/engines': 6.18.0 + '@prisma/config': 7.0.0 + '@prisma/dev': 0.13.0(typescript@5.9.2) + '@prisma/engines': 7.0.0 + '@prisma/studio-core-licensed': 0.8.0(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + mysql2: 3.15.3 + postgres: 3.4.7 optionalDependencies: + better-sqlite3: 11.10.0 typescript: 5.9.2 transitivePeerDependencies: + - '@types/react' - magicast + - react + - react-dom + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 pump@3.0.3: dependencies: @@ -1354,6 +1738,13 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-dom@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + + react@19.2.0: {} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -1362,6 +1753,12 @@ snapshots: readdirp@4.1.2: {} + regexp-to-ast@0.5.0: {} + + remeda@2.21.3: + dependencies: + type-fest: 4.41.0 + resolve-pkg-maps@1.0.0: {} restore-cursor@5.1.0: @@ -1369,12 +1766,28 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + rfdc@1.4.1: {} safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + semver@7.7.3: {} + seq-queue@0.0.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} simple-concat@1.0.1: {} @@ -1390,6 +1803,10 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + sqlstring@2.3.3: {} + + std-env@3.9.0: {} + string-argv@0.3.2: {} string-width@7.2.0: @@ -1428,7 +1845,7 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tinyexec@1.0.1: {} + tinyexec@1.0.2: {} to-regex-range@5.0.1: dependencies: @@ -1449,6 +1866,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + type-fest@4.41.0: {} + typescript@5.9.2: {} undici-types@7.12.0: {} @@ -1457,6 +1876,14 @@ snapshots: util-deprecate@1.0.2: {} + valibot@1.1.0(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -1468,3 +1895,7 @@ snapshots: ws@8.18.3: {} yaml@2.8.1: {} + + zeptomatch@2.0.2: + dependencies: + grammex: 3.1.11 From 1a2faee10255cdb813489f77c8e9fba02135b290 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Fri, 21 Nov 2025 20:55:03 +0100 Subject: [PATCH 21/21] chore: update Prisma schema to add VOICE_MODERATOR role, rename DISRESPECTFUL to DISRUPTIVE, and clean up model relationships --- prisma/schema.prisma | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0c4d34e..3ed7570 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2,18 +2,18 @@ // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" output = "../generated/prisma" } datasource db { provider = "sqlite" - url = "file:../data/moderation.db" } enum Role { MEMBER ASSISTANT + VOICE_MODERATOR MOD_I MOD_II MOD_III @@ -44,7 +44,7 @@ enum ActionStatus { enum ActionReason { SPAM SCAM - DISRESPECTFUL + DISRUPTIVE NSFW HATE_SPEECH SELF_PROMOTION @@ -54,13 +54,13 @@ enum ActionReason { } model User { - userId Int @id @default(autoincrement()) - discordUserId String @unique - role Role @default(MEMBER) + userId Int @id @default(autoincrement()) + discordUserId String @unique + role Role @default(MEMBER) // A user can have many actions performed on them - actions Action[] @relation("UserActions") - + actions Action[] @relation("UserActions") + // A user (as moderator) can perform many moderation actions moderatedActions Action[] @relation("ModeratorActions") } @@ -78,14 +78,14 @@ model Action { revertingActionId Int? @unique // Relationship to the user this action was performed on - user User @relation("UserActions", fields: [userId], references: [userId]) - + user User @relation("UserActions", fields: [userId], references: [userId]) + // Relationship to the moderator who performed this action - moderator User @relation("ModeratorActions", fields: [moderatorUserId], references: [userId]) - + moderator User @relation("ModeratorActions", fields: [moderatorUserId], references: [userId]) + // Self-referential relationship: this action reverts another action - revertingAction Action? @relation("ActionCorrections", fields: [revertingActionId], references: [actionId]) - + revertingAction Action? @relation("ActionCorrections", fields: [revertingActionId], references: [actionId]) + // Self-referential relationship: actions that revert this action - revertedByActions Action[] @relation("ActionCorrections") -} \ No newline at end of file + revertedByActions Action[] @relation("ActionCorrections") +}