feat: Charaktere-Modul mit Pathbuilder Import
Backend: - Characters-Modul (CRUD, HP-Tracking, Conditions) - Pathbuilder 2e JSON Import Service - Claude API Integration für automatische Übersetzungen - Translations-Modul mit Datenbank-Caching - Prisma Schema erweitert (Character, Abilities, Skills, Feats, Items, Resources) Frontend: - Kampagnen-Detailseite mit Mitglieder- und Charakterverwaltung - Charakter erstellen Modal - Pathbuilder Import Modal (Datei-Upload + JSON-Paste) - Logo-Integration (Dimension 47 + Zeasy) - Cinzel Font für Branding Weitere Änderungen: - Auth 401 Redirect Fix für Login-Seite - PROGRESS.md mit Projektfortschritt Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
268
server/package-lock.json
generated
268
server/package-lock.json
generated
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.1",
|
||||
"name": "dimension47-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "server",
|
||||
"version": "0.0.1",
|
||||
"name": "dimension47-server",
|
||||
"version": "1.0.0",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.71.2",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
@@ -18,6 +19,7 @@
|
||||
"@nestjs/platform-socket.io": "^11.1.12",
|
||||
"@nestjs/swagger": "^11.2.5",
|
||||
"@nestjs/websockets": "^11.1.12",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
@@ -205,6 +207,26 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz",
|
||||
"integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
|
||||
@@ -236,7 +258,6 @@
|
||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
@@ -667,6 +688,15 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
@@ -809,8 +839,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz",
|
||||
"integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@electric-sql/pglite-socket": {
|
||||
"version": "0.0.6",
|
||||
@@ -2250,7 +2279,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.12.tgz",
|
||||
"integrity": "sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"file-type": "21.3.0",
|
||||
"iterare": "1.2.1",
|
||||
@@ -2310,7 +2338,6 @@
|
||||
"integrity": "sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nuxt/opencollective": "0.4.1",
|
||||
"fast-safe-stringify": "2.1.1",
|
||||
@@ -2394,7 +2421,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.12.tgz",
|
||||
"integrity": "sha512-GYK/vHI0SGz5m8mxr7v3Urx8b9t78Cf/dj5aJMZlGd9/1D9OI1hAl00BaphjEXINUJ/BQLxIlF2zUjrYsd6enQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cors": "2.8.5",
|
||||
"express": "5.2.1",
|
||||
@@ -2416,7 +2442,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.12.tgz",
|
||||
"integrity": "sha512-1itTTYsAZecrq2NbJOkch32y8buLwN7UpcNRdJrhlS+ovJ5GxLx3RyJ3KylwBhbYnO5AeYyL1U/i4W5mg/4qDA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"socket.io": "4.8.3",
|
||||
"tslib": "2.8.1"
|
||||
@@ -2595,7 +2620,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.12.tgz",
|
||||
"integrity": "sha512-ulSOYcgosx1TqY425cRC5oXtAu1R10+OSmVfgyR9ueR25k4luekURt8dzAZxhxSCI0OsDj9WKCFLTkEuAwg0wg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -2677,6 +2701,17 @@
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/adapter-pg": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.2.0.tgz",
|
||||
"integrity": "sha512-euIdQ13cRB2wZ3jPsnDnFhINquo1PYFPCg6yVL8b2rp3EdinQHsX9EDdCtRr489D5uhphcRk463OdQAFlsCr0w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/driver-adapter-utils": "7.2.0",
|
||||
"pg": "^8.16.3",
|
||||
"postgres-array": "3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.2.0.tgz",
|
||||
@@ -2724,7 +2759,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz",
|
||||
"integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/dev": {
|
||||
@@ -2753,6 +2787,15 @@
|
||||
"zeptomatch": "2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/driver-adapter-utils": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz",
|
||||
"integrity": "sha512-gzrUcbI9VmHS24Uf+0+7DNzdIw7keglJsD5m/MHxQOU68OhGVzlphQRobLiDMn8CHNA2XN8uugwKjudVtnfMVQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "7.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.2.0.tgz",
|
||||
@@ -3049,7 +3092,6 @@
|
||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
@@ -3188,7 +3230,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
|
||||
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -3370,7 +3411,6 @@
|
||||
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.53.0",
|
||||
"@typescript-eslint/types": "8.53.0",
|
||||
@@ -4052,7 +4092,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4102,7 +4141,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -4545,7 +4583,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4807,7 +4844,6 @@
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -4865,15 +4901,13 @@
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.14.3",
|
||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz",
|
||||
"integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.15.3",
|
||||
"libphonenumber-js": "^1.11.1",
|
||||
@@ -5227,7 +5261,8 @@
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
@@ -5691,7 +5726,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5752,7 +5786,6 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -5985,7 +6018,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -6723,7 +6755,6 @@
|
||||
"integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -7110,7 +7141,6 @@
|
||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.2.0",
|
||||
"@jest/types": "30.2.0",
|
||||
@@ -7904,6 +7934,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-to-ts": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"ts-algebra": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
@@ -8896,7 +8939,6 @@
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
@@ -9024,6 +9066,104 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz",
|
||||
"integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.10.0",
|
||||
"pg-pool": "^3.11.0",
|
||||
"pg-protocol": "^1.11.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
|
||||
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.0.tgz",
|
||||
"integrity": "sha512-ur/eoPKzDx2IjPaYyXS6Y8NSblxM7X64deV2ObV57vhjsWiwLvUD6meukAzogiOsu60GO8m/3Cb6FdJsWNjwXg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz",
|
||||
"integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz",
|
||||
"integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-types/node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -9159,6 +9299,45 @@
|
||||
"url": "https://github.com/sponsors/porsager"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
|
||||
"integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -9175,7 +9354,6 @@
|
||||
"integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -9234,7 +9412,6 @@
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "7.2.0",
|
||||
"@prisma/dev": "0.17.0",
|
||||
@@ -9445,8 +9622,7 @@
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/regexp-to-ast": {
|
||||
"version": "0.5.0",
|
||||
@@ -9583,7 +9759,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -9619,7 +9794,8 @@
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
@@ -9952,6 +10128,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@@ -10319,7 +10504,6 @@
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -10551,6 +10735,12 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-algebra": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
@@ -10657,7 +10847,6 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -10805,7 +10994,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -11087,7 +11275,6 @@
|
||||
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -11157,7 +11344,6 @@
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.71.2",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
@@ -32,6 +33,7 @@
|
||||
"@nestjs/platform-socket.io": "^11.1.12",
|
||||
"@nestjs/swagger": "^11.2.5",
|
||||
"@nestjs/websockets": "^11.1.12",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
|
||||
543
server/prisma/migrations/20260118162916_init/migration.sql
Normal file
543
server/prisma/migrations/20260118162916_init/migration.sql
Normal file
@@ -0,0 +1,543 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'GM', 'PLAYER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CharacterType" AS ENUM ('PC', 'NPC');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AbilityType" AS ENUM ('STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Proficiency" AS ENUM ('UNTRAINED', 'TRAINED', 'EXPERT', 'MASTER', 'LEGENDARY');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FeatSource" AS ENUM ('CLASS', 'ANCESTRY', 'GENERAL', 'SKILL', 'BONUS', 'ARCHETYPE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SpellTradition" AS ENUM ('ARCANE', 'DIVINE', 'OCCULT', 'PRIMAL');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CombatantType" AS ENUM ('PC', 'NPC', 'MONSTER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ActionType" AS ENUM ('ACTION', 'REACTION', 'FREE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "HighlightColor" AS ENUM ('YELLOW', 'GREEN', 'BLUE', 'PINK');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TranslationType" AS ENUM ('FEAT', 'EQUIPMENT', 'SPELL', 'TRAIT', 'ANCESTRY', 'HERITAGE', 'CLASS', 'BACKGROUND', 'CONDITION', 'ACTION');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TranslationQuality" AS ENUM ('HIGH', 'MEDIUM', 'LOW');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"role" "UserRole" NOT NULL DEFAULT 'PLAYER',
|
||||
"avatarUrl" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Campaign" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"gmId" TEXT NOT NULL,
|
||||
"imageUrl" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Campaign_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CampaignMember" (
|
||||
"campaignId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "CampaignMember_pkey" PRIMARY KEY ("campaignId","userId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Character" (
|
||||
"id" TEXT NOT NULL,
|
||||
"campaignId" TEXT NOT NULL,
|
||||
"ownerId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "CharacterType" NOT NULL DEFAULT 'PC',
|
||||
"level" INTEGER NOT NULL DEFAULT 1,
|
||||
"avatarUrl" TEXT,
|
||||
"hpCurrent" INTEGER NOT NULL,
|
||||
"hpMax" INTEGER NOT NULL,
|
||||
"hpTemp" INTEGER NOT NULL DEFAULT 0,
|
||||
"ancestryId" TEXT,
|
||||
"heritageId" TEXT,
|
||||
"classId" TEXT,
|
||||
"backgroundId" TEXT,
|
||||
"experiencePoints" INTEGER NOT NULL DEFAULT 0,
|
||||
"pathbuilderData" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Character_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CharacterAbility" (
|
||||
"id" TEXT NOT NULL,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"ability" "AbilityType" NOT NULL,
|
||||
"score" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "CharacterAbility_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CharacterFeat" (
|
||||
"id" TEXT NOT NULL,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"featId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameGerman" TEXT,
|
||||
"level" INTEGER NOT NULL,
|
||||
"source" "FeatSource" NOT NULL,
|
||||
|
||||
CONSTRAINT "CharacterFeat_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CharacterSkill" (
|
||||
"id" TEXT NOT NULL,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"skillName" TEXT NOT NULL,
|
||||
"proficiency" "Proficiency" NOT NULL DEFAULT 'UNTRAINED',
|
||||
|
||||
CONSTRAINT "CharacterSkill_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CharacterSpell" (
|
||||
"id" TEXT NOT NULL,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"spellId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameGerman" TEXT,
|
||||
"tradition" "SpellTradition" NOT NULL,
|
||||
"spellLevel" INTEGER NOT NULL,
|
||||
"prepared" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "CharacterSpell_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CharacterItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"equipmentId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameGerman" TEXT,
|
||||
"quantity" INTEGER NOT NULL DEFAULT 1,
|
||||
"bulk" DECIMAL(65,30) NOT NULL DEFAULT 0,
|
||||
"equipped" BOOLEAN NOT NULL DEFAULT false,
|
||||
"invested" BOOLEAN NOT NULL DEFAULT false,
|
||||
"containerId" TEXT,
|
||||
"notes" TEXT,
|
||||
|
||||
CONSTRAINT "CharacterItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CharacterCondition" (
|
||||
"id" TEXT NOT NULL,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameGerman" TEXT,
|
||||
"value" INTEGER,
|
||||
"duration" TEXT,
|
||||
"source" TEXT,
|
||||
|
||||
CONSTRAINT "CharacterCondition_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CharacterResource" (
|
||||
"id" TEXT NOT NULL,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"current" INTEGER NOT NULL,
|
||||
"max" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "CharacterResource_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BattleMap" (
|
||||
"id" TEXT NOT NULL,
|
||||
"campaignId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"imageUrl" TEXT NOT NULL,
|
||||
"gridSizeX" INTEGER NOT NULL DEFAULT 20,
|
||||
"gridSizeY" INTEGER NOT NULL DEFAULT 20,
|
||||
"gridOffsetX" INTEGER NOT NULL DEFAULT 0,
|
||||
"gridOffsetY" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "BattleMap_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Combatant" (
|
||||
"id" TEXT NOT NULL,
|
||||
"campaignId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "CombatantType" NOT NULL,
|
||||
"level" INTEGER NOT NULL,
|
||||
"hpMax" INTEGER NOT NULL,
|
||||
"ac" INTEGER NOT NULL,
|
||||
"fortitude" INTEGER NOT NULL,
|
||||
"reflex" INTEGER NOT NULL,
|
||||
"will" INTEGER NOT NULL,
|
||||
"perception" INTEGER NOT NULL,
|
||||
"speed" INTEGER NOT NULL DEFAULT 25,
|
||||
"avatarUrl" TEXT,
|
||||
"description" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Combatant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CombatantAbility" (
|
||||
"id" TEXT NOT NULL,
|
||||
"combatantId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"actionCost" INTEGER NOT NULL,
|
||||
"actionType" "ActionType" NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"damage" TEXT,
|
||||
"traits" TEXT[],
|
||||
|
||||
CONSTRAINT "CombatantAbility_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BattleSession" (
|
||||
"id" TEXT NOT NULL,
|
||||
"campaignId" TEXT NOT NULL,
|
||||
"mapId" TEXT,
|
||||
"name" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT false,
|
||||
"roundNumber" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "BattleSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BattleToken" (
|
||||
"id" TEXT NOT NULL,
|
||||
"battleSessionId" TEXT NOT NULL,
|
||||
"combatantId" TEXT,
|
||||
"characterId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"positionX" DOUBLE PRECISION NOT NULL,
|
||||
"positionY" DOUBLE PRECISION NOT NULL,
|
||||
"hpCurrent" INTEGER NOT NULL,
|
||||
"hpMax" INTEGER NOT NULL,
|
||||
"initiative" INTEGER,
|
||||
"conditions" TEXT[],
|
||||
"size" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
CONSTRAINT "BattleToken_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Document" (
|
||||
"id" TEXT NOT NULL,
|
||||
"campaignId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"category" TEXT,
|
||||
"tags" TEXT[],
|
||||
"filePath" TEXT NOT NULL,
|
||||
"fileType" TEXT NOT NULL,
|
||||
"uploadedBy" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "DocumentAccess" (
|
||||
"id" TEXT NOT NULL,
|
||||
"documentId" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
"characterId" TEXT,
|
||||
|
||||
CONSTRAINT "DocumentAccess_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Highlight" (
|
||||
"id" TEXT NOT NULL,
|
||||
"documentId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"selectionText" TEXT NOT NULL,
|
||||
"startOffset" INTEGER NOT NULL,
|
||||
"endOffset" INTEGER NOT NULL,
|
||||
"color" "HighlightColor" NOT NULL,
|
||||
"note" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Highlight_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Note" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"campaignId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"isShared" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Note_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "NoteShare" (
|
||||
"noteId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "NoteShare_pkey" PRIMARY KEY ("noteId","userId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Feat" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"traits" TEXT[],
|
||||
"summary" TEXT,
|
||||
"actions" TEXT,
|
||||
"url" TEXT,
|
||||
"level" INTEGER,
|
||||
"sourceBook" TEXT,
|
||||
|
||||
CONSTRAINT "Feat_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Equipment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"traits" TEXT[],
|
||||
"itemCategory" TEXT NOT NULL,
|
||||
"itemSubcategory" TEXT,
|
||||
"bulk" TEXT,
|
||||
"url" TEXT,
|
||||
"summary" TEXT,
|
||||
"activation" TEXT,
|
||||
"hands" TEXT,
|
||||
"damage" TEXT,
|
||||
"range" TEXT,
|
||||
"weaponCategory" TEXT,
|
||||
"price" INTEGER,
|
||||
"level" INTEGER,
|
||||
|
||||
CONSTRAINT "Equipment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Spell" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"level" INTEGER NOT NULL,
|
||||
"actions" TEXT,
|
||||
"traditions" TEXT[],
|
||||
"traits" TEXT[],
|
||||
"range" TEXT,
|
||||
"targets" TEXT,
|
||||
"duration" TEXT,
|
||||
"description" TEXT,
|
||||
"url" TEXT,
|
||||
|
||||
CONSTRAINT "Spell_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Trait" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"url" TEXT,
|
||||
|
||||
CONSTRAINT "Trait_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Translation" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" "TranslationType" NOT NULL,
|
||||
"englishName" TEXT NOT NULL,
|
||||
"germanName" TEXT NOT NULL,
|
||||
"germanSummary" TEXT,
|
||||
"germanDescription" TEXT,
|
||||
"quality" "TranslationQuality" NOT NULL DEFAULT 'MEDIUM',
|
||||
"translatedBy" TEXT NOT NULL DEFAULT 'claude-api',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Translation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CharacterAbility_characterId_ability_key" ON "CharacterAbility"("characterId", "ability");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CharacterSkill_characterId_skillName_key" ON "CharacterSkill"("characterId", "skillName");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CharacterResource_characterId_name_key" ON "CharacterResource"("characterId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "DocumentAccess_documentId_userId_characterId_key" ON "DocumentAccess"("documentId", "userId", "characterId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Feat_name_key" ON "Feat"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Equipment_name_key" ON "Equipment"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Spell_name_key" ON "Spell"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Trait_name_key" ON "Trait"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Translation_type_idx" ON "Translation"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Translation_englishName_idx" ON "Translation"("englishName");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Translation_type_englishName_key" ON "Translation"("type", "englishName");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Campaign" ADD CONSTRAINT "Campaign_gmId_fkey" FOREIGN KEY ("gmId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CampaignMember" ADD CONSTRAINT "CampaignMember_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CampaignMember" ADD CONSTRAINT "CampaignMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Character" ADD CONSTRAINT "Character_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Character" ADD CONSTRAINT "Character_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterAbility" ADD CONSTRAINT "CharacterAbility_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterFeat" ADD CONSTRAINT "CharacterFeat_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterFeat" ADD CONSTRAINT "CharacterFeat_featId_fkey" FOREIGN KEY ("featId") REFERENCES "Feat"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterSkill" ADD CONSTRAINT "CharacterSkill_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterSpell" ADD CONSTRAINT "CharacterSpell_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterSpell" ADD CONSTRAINT "CharacterSpell_spellId_fkey" FOREIGN KEY ("spellId") REFERENCES "Spell"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterItem" ADD CONSTRAINT "CharacterItem_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterItem" ADD CONSTRAINT "CharacterItem_equipmentId_fkey" FOREIGN KEY ("equipmentId") REFERENCES "Equipment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterCondition" ADD CONSTRAINT "CharacterCondition_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CharacterResource" ADD CONSTRAINT "CharacterResource_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BattleMap" ADD CONSTRAINT "BattleMap_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Combatant" ADD CONSTRAINT "Combatant_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CombatantAbility" ADD CONSTRAINT "CombatantAbility_combatantId_fkey" FOREIGN KEY ("combatantId") REFERENCES "Combatant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BattleSession" ADD CONSTRAINT "BattleSession_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BattleSession" ADD CONSTRAINT "BattleSession_mapId_fkey" FOREIGN KEY ("mapId") REFERENCES "BattleMap"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BattleToken" ADD CONSTRAINT "BattleToken_battleSessionId_fkey" FOREIGN KEY ("battleSessionId") REFERENCES "BattleSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BattleToken" ADD CONSTRAINT "BattleToken_combatantId_fkey" FOREIGN KEY ("combatantId") REFERENCES "Combatant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BattleToken" ADD CONSTRAINT "BattleToken_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Document" ADD CONSTRAINT "Document_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Document" ADD CONSTRAINT "Document_uploadedBy_fkey" FOREIGN KEY ("uploadedBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DocumentAccess" ADD CONSTRAINT "DocumentAccess_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DocumentAccess" ADD CONSTRAINT "DocumentAccess_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DocumentAccess" ADD CONSTRAINT "DocumentAccess_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Highlight" ADD CONSTRAINT "Highlight_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Highlight" ADD CONSTRAINT "Highlight_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Note" ADD CONSTRAINT "Note_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Note" ADD CONSTRAINT "Note_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "NoteShare" ADD CONSTRAINT "NoteShare_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "NoteShare" ADD CONSTRAINT "NoteShare_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
3
server/prisma/migrations/migration_lock.toml
Normal file
3
server/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -2,8 +2,9 @@
|
||||
// Prisma Schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../src/generated/prisma"
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
moduleFormat = "cjs"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
||||
28
server/prisma/seed.ts
Normal file
28
server/prisma/seed.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'dotenv/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { PrismaClient } from '../src/generated/prisma/client.js';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
async function main() {
|
||||
const passwordHash = await bcrypt.hash('admin123', 10);
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: 'admin@dimension47.local' },
|
||||
update: {},
|
||||
create: {
|
||||
username: 'admin',
|
||||
email: 'admin@dimension47.local',
|
||||
passwordHash,
|
||||
role: 'ADMIN',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Admin user created:', user.username, user.email);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -4,10 +4,13 @@ import { APP_GUARD } from '@nestjs/core';
|
||||
|
||||
// Core Modules
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { ClaudeModule } from './modules/claude/claude.module';
|
||||
|
||||
// Feature Modules
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { CampaignsModule } from './modules/campaigns/campaigns.module';
|
||||
import { CharactersModule } from './modules/characters/characters.module';
|
||||
import { TranslationsModule } from './modules/translations/translations.module';
|
||||
|
||||
// Guards
|
||||
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
|
||||
@@ -23,10 +26,13 @@ import { RolesGuard } from './modules/auth/guards/roles.guard';
|
||||
|
||||
// Core
|
||||
PrismaModule,
|
||||
ClaudeModule,
|
||||
|
||||
// Features
|
||||
AuthModule,
|
||||
CampaignsModule,
|
||||
CharactersModule,
|
||||
TranslationsModule,
|
||||
],
|
||||
providers: [
|
||||
// Global JWT Auth Guard
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { UserRole } from '../../generated/prisma';
|
||||
import { UserRole } from '../../generated/prisma/client.js';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
|
||||
|
||||
@@ -24,9 +24,25 @@ async function bootstrap() {
|
||||
);
|
||||
|
||||
// CORS
|
||||
const corsOrigins = configService.get<string>('CORS_ORIGINS', 'http://localhost:3000,http://localhost:5173');
|
||||
const nodeEnv = configService.get<string>('NODE_ENV', 'development');
|
||||
app.enableCors({
|
||||
origin: corsOrigins.split(','),
|
||||
origin: (origin, callback) => {
|
||||
// Allow requests with no origin (mobile apps, curl, etc.)
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
// In development, allow all localhost origins
|
||||
if (nodeEnv === 'development' && /^https?:\/\/localhost(:\d+)?$/.test(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Check against configured origins
|
||||
const corsOrigins = configService.get<string>('CORS_ORIGINS', 'http://localhost:3000,http://localhost:5173');
|
||||
if (corsOrigins.split(',').includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
},
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { RegisterDto, LoginDto } from './dto';
|
||||
import { Public } from '../../common/decorators/public.decorator';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { Roles } from '../../common/decorators/roles.decorator';
|
||||
import { UserRole } from '../../generated/prisma';
|
||||
import { UserRole } from '../../generated/prisma/client.js';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@Controller('auth')
|
||||
|
||||
@@ -8,7 +8,7 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { RegisterDto, LoginDto } from './dto';
|
||||
import { UserRole } from '../../generated/prisma';
|
||||
import { UserRole } from '../../generated/prisma/client.js';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { UserRole } from '../../../generated/prisma';
|
||||
import { UserRole } from '../../../generated/prisma/client.js';
|
||||
import { ROLES_KEY } from '../../../common/decorators/roles.decorator';
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { CampaignsService } from './campaigns.service';
|
||||
import { CreateCampaignDto, UpdateCampaignDto, AddMemberDto } from './dto';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { UserRole } from '../../generated/prisma';
|
||||
import { UserRole } from '../../generated/prisma/client.js';
|
||||
|
||||
@ApiTags('Campaigns')
|
||||
@ApiBearerAuth()
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { CreateCampaignDto, UpdateCampaignDto, AddMemberDto } from './dto';
|
||||
import { UserRole } from '../../generated/prisma';
|
||||
import { UserRole } from '../../generated/prisma/client.js';
|
||||
|
||||
@Injectable()
|
||||
export class CampaignsService {
|
||||
|
||||
292
server/src/modules/characters/characters.controller.ts
Normal file
292
server/src/modules/characters/characters.controller.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { CharactersService } from './characters.service';
|
||||
import { PathbuilderImportService } from './pathbuilder-import.service';
|
||||
import {
|
||||
CreateCharacterDto,
|
||||
UpdateCharacterDto,
|
||||
CreateAbilityDto,
|
||||
CreateSkillDto,
|
||||
CreateFeatDto,
|
||||
CreateSpellDto,
|
||||
CreateItemDto,
|
||||
CreateConditionDto,
|
||||
CreateResourceDto,
|
||||
PathbuilderImportDto,
|
||||
} from './dto';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
@ApiTags('Characters')
|
||||
@ApiBearerAuth()
|
||||
@Controller('campaigns/:campaignId/characters')
|
||||
export class CharactersController {
|
||||
constructor(
|
||||
private readonly charactersService: CharactersService,
|
||||
private readonly pathbuilderImportService: PathbuilderImportService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new character' })
|
||||
@ApiResponse({ status: 201, description: 'Character created successfully' })
|
||||
async create(
|
||||
@Param('campaignId') campaignId: string,
|
||||
@Body() dto: CreateCharacterDto,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.create(campaignId, dto, userId);
|
||||
}
|
||||
|
||||
@Post('import')
|
||||
@ApiOperation({ summary: 'Import character from Pathbuilder JSON' })
|
||||
@ApiResponse({ status: 201, description: 'Character imported successfully' })
|
||||
async importFromPathbuilder(
|
||||
@Param('campaignId') campaignId: string,
|
||||
@Body() dto: PathbuilderImportDto,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.pathbuilderImportService.importCharacter(
|
||||
campaignId,
|
||||
userId,
|
||||
dto.pathbuilderJson,
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all characters in a campaign' })
|
||||
@ApiResponse({ status: 200, description: 'List of characters' })
|
||||
async findAll(
|
||||
@Param('campaignId') campaignId: string,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.findAllByCampaign(campaignId, userId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get character by ID' })
|
||||
@ApiResponse({ status: 200, description: 'Character details' })
|
||||
@ApiResponse({ status: 404, description: 'Character not found' })
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.findOne(id, userId);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Update character' })
|
||||
@ApiResponse({ status: 200, description: 'Character updated' })
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateCharacterDto,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.update(id, dto, userId);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete character' })
|
||||
@ApiResponse({ status: 200, description: 'Character deleted' })
|
||||
async remove(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.remove(id, userId);
|
||||
}
|
||||
|
||||
// HP Management
|
||||
@Patch(':id/hp')
|
||||
@ApiOperation({ summary: 'Update character HP' })
|
||||
async updateHp(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { hpCurrent: number; hpTemp?: number },
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.updateHp(id, body.hpCurrent, body.hpTemp, userId);
|
||||
}
|
||||
|
||||
// Abilities
|
||||
@Put(':id/abilities')
|
||||
@ApiOperation({ summary: 'Set character abilities' })
|
||||
async setAbilities(
|
||||
@Param('id') id: string,
|
||||
@Body() abilities: CreateAbilityDto[],
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.setAbilities(id, abilities, userId);
|
||||
}
|
||||
|
||||
// Skills
|
||||
@Put(':id/skills')
|
||||
@ApiOperation({ summary: 'Set character skills' })
|
||||
async setSkills(
|
||||
@Param('id') id: string,
|
||||
@Body() skills: CreateSkillDto[],
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.setSkills(id, skills, userId);
|
||||
}
|
||||
|
||||
@Patch(':id/skills/:skillName')
|
||||
@ApiOperation({ summary: 'Update single skill proficiency' })
|
||||
async updateSkill(
|
||||
@Param('id') id: string,
|
||||
@Param('skillName') skillName: string,
|
||||
@Body() body: { proficiency: string },
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.updateSkill(id, skillName, body.proficiency, userId);
|
||||
}
|
||||
|
||||
// Feats
|
||||
@Post(':id/feats')
|
||||
@ApiOperation({ summary: 'Add feat to character' })
|
||||
async addFeat(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: CreateFeatDto,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.addFeat(id, dto, userId);
|
||||
}
|
||||
|
||||
@Delete(':id/feats/:featId')
|
||||
@ApiOperation({ summary: 'Remove feat from character' })
|
||||
async removeFeat(
|
||||
@Param('id') id: string,
|
||||
@Param('featId') featId: string,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.removeFeat(id, featId, userId);
|
||||
}
|
||||
|
||||
// Spells
|
||||
@Post(':id/spells')
|
||||
@ApiOperation({ summary: 'Add spell to character' })
|
||||
async addSpell(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: CreateSpellDto,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.addSpell(id, dto, userId);
|
||||
}
|
||||
|
||||
@Patch(':id/spells/:spellId')
|
||||
@ApiOperation({ summary: 'Update spell (prepared status)' })
|
||||
async updateSpell(
|
||||
@Param('id') id: string,
|
||||
@Param('spellId') spellId: string,
|
||||
@Body() body: { prepared: boolean },
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.updateSpell(id, spellId, body.prepared, userId);
|
||||
}
|
||||
|
||||
@Delete(':id/spells/:spellId')
|
||||
@ApiOperation({ summary: 'Remove spell from character' })
|
||||
async removeSpell(
|
||||
@Param('id') id: string,
|
||||
@Param('spellId') spellId: string,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.removeSpell(id, spellId, userId);
|
||||
}
|
||||
|
||||
// Items
|
||||
@Post(':id/items')
|
||||
@ApiOperation({ summary: 'Add item to character' })
|
||||
async addItem(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: CreateItemDto,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.addItem(id, dto, userId);
|
||||
}
|
||||
|
||||
@Patch(':id/items/:itemId')
|
||||
@ApiOperation({ summary: 'Update item' })
|
||||
async updateItem(
|
||||
@Param('id') id: string,
|
||||
@Param('itemId') itemId: string,
|
||||
@Body() body: Partial<CreateItemDto>,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.updateItem(id, itemId, body, userId);
|
||||
}
|
||||
|
||||
@Delete(':id/items/:itemId')
|
||||
@ApiOperation({ summary: 'Remove item from character' })
|
||||
async removeItem(
|
||||
@Param('id') id: string,
|
||||
@Param('itemId') itemId: string,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.removeItem(id, itemId, userId);
|
||||
}
|
||||
|
||||
// Conditions
|
||||
@Post(':id/conditions')
|
||||
@ApiOperation({ summary: 'Add condition to character' })
|
||||
async addCondition(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: CreateConditionDto,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.addCondition(id, dto, userId);
|
||||
}
|
||||
|
||||
@Patch(':id/conditions/:conditionId')
|
||||
@ApiOperation({ summary: 'Update condition value' })
|
||||
async updateCondition(
|
||||
@Param('id') id: string,
|
||||
@Param('conditionId') conditionId: string,
|
||||
@Body() body: { value: number },
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.updateCondition(id, conditionId, body.value, userId);
|
||||
}
|
||||
|
||||
@Delete(':id/conditions/:conditionId')
|
||||
@ApiOperation({ summary: 'Remove condition from character' })
|
||||
async removeCondition(
|
||||
@Param('id') id: string,
|
||||
@Param('conditionId') conditionId: string,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.removeCondition(id, conditionId, userId);
|
||||
}
|
||||
|
||||
// Resources
|
||||
@Put(':id/resources')
|
||||
@ApiOperation({ summary: 'Set character resources' })
|
||||
async setResources(
|
||||
@Param('id') id: string,
|
||||
@Body() resources: CreateResourceDto[],
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.setResources(id, resources, userId);
|
||||
}
|
||||
|
||||
@Patch(':id/resources/:resourceName')
|
||||
@ApiOperation({ summary: 'Update resource current value' })
|
||||
async updateResource(
|
||||
@Param('id') id: string,
|
||||
@Param('resourceName') resourceName: string,
|
||||
@Body() body: { current: number },
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.charactersService.updateResource(id, resourceName, body.current, userId);
|
||||
}
|
||||
}
|
||||
13
server/src/modules/characters/characters.module.ts
Normal file
13
server/src/modules/characters/characters.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CharactersController } from './characters.controller';
|
||||
import { CharactersService } from './characters.service';
|
||||
import { PathbuilderImportService } from './pathbuilder-import.service';
|
||||
import { TranslationsModule } from '../translations/translations.module';
|
||||
|
||||
@Module({
|
||||
imports: [TranslationsModule],
|
||||
controllers: [CharactersController],
|
||||
providers: [CharactersService, PathbuilderImportService],
|
||||
exports: [CharactersService, PathbuilderImportService],
|
||||
})
|
||||
export class CharactersModule {}
|
||||
311
server/src/modules/characters/characters.service.ts
Normal file
311
server/src/modules/characters/characters.service.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import {
|
||||
CreateCharacterDto,
|
||||
UpdateCharacterDto,
|
||||
CreateAbilityDto,
|
||||
CreateSkillDto,
|
||||
CreateFeatDto,
|
||||
CreateSpellDto,
|
||||
CreateItemDto,
|
||||
CreateConditionDto,
|
||||
CreateResourceDto,
|
||||
} from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class CharactersService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
// Check if user has access to campaign
|
||||
private async checkCampaignAccess(campaignId: string, userId: string) {
|
||||
const campaign = await this.prisma.campaign.findUnique({
|
||||
where: { id: campaignId },
|
||||
include: { members: true },
|
||||
});
|
||||
|
||||
if (!campaign) {
|
||||
throw new NotFoundException('Campaign not found');
|
||||
}
|
||||
|
||||
const hasAccess =
|
||||
campaign.gmId === userId ||
|
||||
campaign.members.some((m) => m.userId === userId);
|
||||
|
||||
if (!hasAccess) {
|
||||
throw new ForbiddenException('No access to this campaign');
|
||||
}
|
||||
|
||||
return campaign;
|
||||
}
|
||||
|
||||
// Check if user can edit character
|
||||
private async checkCharacterAccess(characterId: string, userId: string, requireOwnership = false) {
|
||||
const character = await this.prisma.character.findUnique({
|
||||
where: { id: characterId },
|
||||
include: { campaign: { include: { members: true } } },
|
||||
});
|
||||
|
||||
if (!character) {
|
||||
throw new NotFoundException('Character not found');
|
||||
}
|
||||
|
||||
const isGM = character.campaign.gmId === userId;
|
||||
const isOwner = character.ownerId === userId;
|
||||
|
||||
if (requireOwnership && !isOwner && !isGM) {
|
||||
throw new ForbiddenException('Only the owner or GM can modify this character');
|
||||
}
|
||||
|
||||
const isMember = character.campaign.members.some((m) => m.userId === userId);
|
||||
if (!isGM && !isMember) {
|
||||
throw new ForbiddenException('No access to this character');
|
||||
}
|
||||
|
||||
return character;
|
||||
}
|
||||
|
||||
async create(campaignId: string, dto: CreateCharacterDto, userId: string) {
|
||||
await this.checkCampaignAccess(campaignId, userId);
|
||||
|
||||
return this.prisma.character.create({
|
||||
data: {
|
||||
...dto,
|
||||
campaignId,
|
||||
ownerId: userId,
|
||||
pathbuilderData: dto.pathbuilderData as any,
|
||||
},
|
||||
include: {
|
||||
owner: { select: { id: true, username: true } },
|
||||
abilities: true,
|
||||
skills: true,
|
||||
feats: true,
|
||||
spells: true,
|
||||
items: true,
|
||||
conditions: true,
|
||||
resources: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findAllByCampaign(campaignId: string, userId: string) {
|
||||
await this.checkCampaignAccess(campaignId, userId);
|
||||
|
||||
return this.prisma.character.findMany({
|
||||
where: { campaignId },
|
||||
include: {
|
||||
owner: { select: { id: true, username: true } },
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string, userId: string) {
|
||||
const character = await this.checkCharacterAccess(id, userId);
|
||||
|
||||
return this.prisma.character.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
owner: { select: { id: true, username: true, avatarUrl: true } },
|
||||
abilities: true,
|
||||
skills: { orderBy: { skillName: 'asc' } },
|
||||
feats: { orderBy: [{ level: 'asc' }, { name: 'asc' }] },
|
||||
spells: { orderBy: [{ spellLevel: 'asc' }, { name: 'asc' }] },
|
||||
items: { orderBy: { name: 'asc' } },
|
||||
conditions: true,
|
||||
resources: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateCharacterDto, userId: string) {
|
||||
await this.checkCharacterAccess(id, userId, true);
|
||||
|
||||
return this.prisma.character.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...dto,
|
||||
pathbuilderData: dto.pathbuilderData as any,
|
||||
},
|
||||
include: {
|
||||
owner: { select: { id: true, username: true } },
|
||||
abilities: true,
|
||||
skills: true,
|
||||
feats: true,
|
||||
spells: true,
|
||||
items: true,
|
||||
conditions: true,
|
||||
resources: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: string, userId: string) {
|
||||
await this.checkCharacterAccess(id, userId, true);
|
||||
|
||||
await this.prisma.character.delete({ where: { id } });
|
||||
return { message: 'Character deleted successfully' };
|
||||
}
|
||||
|
||||
// HP Management
|
||||
async updateHp(id: string, hpCurrent: number, hpTemp?: number, userId?: string) {
|
||||
if (userId) {
|
||||
await this.checkCharacterAccess(id, userId, true);
|
||||
}
|
||||
|
||||
return this.prisma.character.update({
|
||||
where: { id },
|
||||
data: {
|
||||
hpCurrent: Math.max(0, hpCurrent),
|
||||
...(hpTemp !== undefined && { hpTemp: Math.max(0, hpTemp) }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Abilities
|
||||
async setAbilities(characterId: string, abilities: CreateAbilityDto[], userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
// Delete existing and create new
|
||||
await this.prisma.characterAbility.deleteMany({ where: { characterId } });
|
||||
|
||||
return this.prisma.characterAbility.createMany({
|
||||
data: abilities.map((a) => ({ ...a, characterId })),
|
||||
});
|
||||
}
|
||||
|
||||
// Skills
|
||||
async setSkills(characterId: string, skills: CreateSkillDto[], userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
await this.prisma.characterSkill.deleteMany({ where: { characterId } });
|
||||
|
||||
return this.prisma.characterSkill.createMany({
|
||||
data: skills.map((s) => ({ ...s, characterId })),
|
||||
});
|
||||
}
|
||||
|
||||
async updateSkill(characterId: string, skillName: string, proficiency: string, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
return this.prisma.characterSkill.upsert({
|
||||
where: { characterId_skillName: { characterId, skillName } },
|
||||
update: { proficiency: proficiency as any },
|
||||
create: { characterId, skillName, proficiency: proficiency as any },
|
||||
});
|
||||
}
|
||||
|
||||
// Feats
|
||||
async addFeat(characterId: string, dto: CreateFeatDto, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
return this.prisma.characterFeat.create({
|
||||
data: { ...dto, characterId },
|
||||
});
|
||||
}
|
||||
|
||||
async removeFeat(characterId: string, featId: string, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
await this.prisma.characterFeat.delete({ where: { id: featId } });
|
||||
return { message: 'Feat removed' };
|
||||
}
|
||||
|
||||
// Spells
|
||||
async addSpell(characterId: string, dto: CreateSpellDto, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
return this.prisma.characterSpell.create({
|
||||
data: { ...dto, characterId },
|
||||
});
|
||||
}
|
||||
|
||||
async updateSpell(characterId: string, spellId: string, prepared: boolean, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
return this.prisma.characterSpell.update({
|
||||
where: { id: spellId },
|
||||
data: { prepared },
|
||||
});
|
||||
}
|
||||
|
||||
async removeSpell(characterId: string, spellId: string, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
await this.prisma.characterSpell.delete({ where: { id: spellId } });
|
||||
return { message: 'Spell removed' };
|
||||
}
|
||||
|
||||
// Items
|
||||
async addItem(characterId: string, dto: CreateItemDto, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
return this.prisma.characterItem.create({
|
||||
data: { ...dto, characterId, bulk: dto.bulk as any },
|
||||
});
|
||||
}
|
||||
|
||||
async updateItem(characterId: string, itemId: string, data: Partial<CreateItemDto>, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
return this.prisma.characterItem.update({
|
||||
where: { id: itemId },
|
||||
data: { ...data, bulk: data.bulk as any },
|
||||
});
|
||||
}
|
||||
|
||||
async removeItem(characterId: string, itemId: string, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
await this.prisma.characterItem.delete({ where: { id: itemId } });
|
||||
return { message: 'Item removed' };
|
||||
}
|
||||
|
||||
// Conditions
|
||||
async addCondition(characterId: string, dto: CreateConditionDto, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
return this.prisma.characterCondition.create({
|
||||
data: { ...dto, characterId },
|
||||
});
|
||||
}
|
||||
|
||||
async updateCondition(characterId: string, conditionId: string, value: number, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
return this.prisma.characterCondition.update({
|
||||
where: { id: conditionId },
|
||||
data: { value },
|
||||
});
|
||||
}
|
||||
|
||||
async removeCondition(characterId: string, conditionId: string, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
await this.prisma.characterCondition.delete({ where: { id: conditionId } });
|
||||
return { message: 'Condition removed' };
|
||||
}
|
||||
|
||||
// Resources
|
||||
async setResources(characterId: string, resources: CreateResourceDto[], userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
await this.prisma.characterResource.deleteMany({ where: { characterId } });
|
||||
|
||||
return this.prisma.characterResource.createMany({
|
||||
data: resources.map((r) => ({ ...r, characterId })),
|
||||
});
|
||||
}
|
||||
|
||||
async updateResource(characterId: string, resourceName: string, current: number, userId: string) {
|
||||
await this.checkCharacterAccess(characterId, userId, true);
|
||||
|
||||
return this.prisma.characterResource.update({
|
||||
where: { characterId_name: { characterId, name: resourceName } },
|
||||
data: { current: Math.max(0, current) },
|
||||
});
|
||||
}
|
||||
}
|
||||
236
server/src/modules/characters/dto/create-character.dto.ts
Normal file
236
server/src/modules/characters/dto/create-character.dto.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsEnum, IsInt, Min, Max, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { CharacterType, AbilityType, Proficiency, FeatSource, SpellTradition } from '../../../generated/prisma/client.js';
|
||||
|
||||
export class CreateCharacterDto {
|
||||
@ApiProperty({ description: 'Character name' })
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: ['PC', 'NPC'], default: 'PC' })
|
||||
@IsOptional()
|
||||
@IsEnum(CharacterType)
|
||||
type?: CharacterType = CharacterType.PC;
|
||||
|
||||
@ApiPropertyOptional({ default: 1 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(20)
|
||||
level?: number = 1;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatarUrl?: string;
|
||||
|
||||
@ApiProperty({ description: 'Current HP' })
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
hpCurrent: number;
|
||||
|
||||
@ApiProperty({ description: 'Maximum HP' })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
hpMax: number;
|
||||
|
||||
@ApiPropertyOptional({ default: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
hpTemp?: number = 0;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ancestryId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
heritageId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
classId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
backgroundId?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
experiencePoints?: number = 0;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Raw Pathbuilder JSON data' })
|
||||
@IsOptional()
|
||||
pathbuilderData?: unknown;
|
||||
}
|
||||
|
||||
export class CreateAbilityDto {
|
||||
@ApiProperty({ enum: ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA'] })
|
||||
@IsEnum(AbilityType)
|
||||
ability: AbilityType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(30)
|
||||
score: number;
|
||||
}
|
||||
|
||||
export class CreateSkillDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
skillName: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: ['UNTRAINED', 'TRAINED', 'EXPERT', 'MASTER', 'LEGENDARY'], default: 'UNTRAINED' })
|
||||
@IsOptional()
|
||||
@IsEnum(Proficiency)
|
||||
proficiency?: Proficiency = Proficiency.UNTRAINED;
|
||||
}
|
||||
|
||||
export class CreateFeatDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
featId?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nameGerman?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
level: number;
|
||||
|
||||
@ApiProperty({ enum: ['CLASS', 'ANCESTRY', 'GENERAL', 'SKILL', 'BONUS', 'ARCHETYPE'] })
|
||||
@IsEnum(FeatSource)
|
||||
source: FeatSource;
|
||||
}
|
||||
|
||||
export class CreateSpellDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
spellId?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nameGerman?: string;
|
||||
|
||||
@ApiProperty({ enum: ['ARCANE', 'DIVINE', 'OCCULT', 'PRIMAL'] })
|
||||
@IsEnum(SpellTradition)
|
||||
tradition: SpellTradition;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(10)
|
||||
spellLevel: number;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
prepared?: boolean = false;
|
||||
}
|
||||
|
||||
export class CreateItemDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
equipmentId?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nameGerman?: string;
|
||||
|
||||
@ApiPropertyOptional({ default: 1 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
quantity?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ default: 0 })
|
||||
@IsOptional()
|
||||
bulk?: number = 0;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
equipped?: boolean = false;
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
invested?: boolean = false;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
containerId?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class CreateConditionDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nameGerman?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
value?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
duration?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export class CreateResourceDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
current: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
max: number;
|
||||
}
|
||||
3
server/src/modules/characters/dto/index.ts
Normal file
3
server/src/modules/characters/dto/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './create-character.dto';
|
||||
export * from './update-character.dto';
|
||||
export * from './pathbuilder-import.dto';
|
||||
152
server/src/modules/characters/dto/pathbuilder-import.dto.ts
Normal file
152
server/src/modules/characters/dto/pathbuilder-import.dto.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsObject, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
// Pathbuilder JSON structure
|
||||
export interface PathbuilderBuild {
|
||||
name: string;
|
||||
class: string;
|
||||
dualClass?: string | null;
|
||||
level: number;
|
||||
xp: number;
|
||||
ancestry: string;
|
||||
heritage: string;
|
||||
background: string;
|
||||
alignment: string;
|
||||
gender: string;
|
||||
age: string;
|
||||
deity: string;
|
||||
size: number;
|
||||
sizeName: string;
|
||||
keyability: string;
|
||||
languages: string[];
|
||||
abilities: {
|
||||
str: number;
|
||||
dex: number;
|
||||
con: number;
|
||||
int: number;
|
||||
wis: number;
|
||||
cha: number;
|
||||
breakdown?: unknown;
|
||||
};
|
||||
attributes: {
|
||||
ancestryhp: number;
|
||||
classhp: number;
|
||||
bonushp: number;
|
||||
bonushpPerLevel: number;
|
||||
speed: number;
|
||||
speedBonus: number;
|
||||
};
|
||||
proficiencies: {
|
||||
classDC: number;
|
||||
perception: number;
|
||||
fortitude: number;
|
||||
reflex: number;
|
||||
will: number;
|
||||
heavy: number;
|
||||
medium: number;
|
||||
light: number;
|
||||
unarmored: number;
|
||||
advanced: number;
|
||||
martial: number;
|
||||
simple: number;
|
||||
unarmed: number;
|
||||
castingArcane: number;
|
||||
castingDivine: number;
|
||||
castingOccult: number;
|
||||
castingPrimal: number;
|
||||
acrobatics: number;
|
||||
arcana: number;
|
||||
athletics: number;
|
||||
crafting: number;
|
||||
deception: number;
|
||||
diplomacy: number;
|
||||
intimidation: number;
|
||||
medicine: number;
|
||||
nature: number;
|
||||
occultism: number;
|
||||
performance: number;
|
||||
religion: number;
|
||||
society: number;
|
||||
stealth: number;
|
||||
survival: number;
|
||||
thievery: number;
|
||||
};
|
||||
feats: Array<[string, unknown, string, number, ...unknown[]]>;
|
||||
specials: string[];
|
||||
lores: Array<[string, number]>;
|
||||
equipment: Array<[string, number, string?]>;
|
||||
weapons: Array<{
|
||||
name: string;
|
||||
qty: number;
|
||||
prof: string;
|
||||
die: string;
|
||||
pot: number;
|
||||
str: string;
|
||||
mat: string | null;
|
||||
display: string;
|
||||
runes: string[];
|
||||
damageType: string;
|
||||
attack: number;
|
||||
damageBonus: number;
|
||||
extraDamage: string[];
|
||||
increasedDice: boolean;
|
||||
isInventor: boolean;
|
||||
}>;
|
||||
armor: Array<{
|
||||
name: string;
|
||||
qty: number;
|
||||
prof: string;
|
||||
pot: number;
|
||||
res: string;
|
||||
mat: string | null;
|
||||
display: string;
|
||||
worn: boolean;
|
||||
runes: string[];
|
||||
}>;
|
||||
money: {
|
||||
cp: number;
|
||||
sp: number;
|
||||
gp: number;
|
||||
pp: number;
|
||||
};
|
||||
spellCasters: Array<{
|
||||
name: string;
|
||||
magicTradition: string;
|
||||
spellcastingType: string;
|
||||
ability: string;
|
||||
proficiency: number;
|
||||
focusPoints: number;
|
||||
spells: Array<{
|
||||
spellLevel: number;
|
||||
list: string[];
|
||||
}>;
|
||||
perDay: number[];
|
||||
}>;
|
||||
focusPoints: number;
|
||||
focus: unknown;
|
||||
formula: Array<{
|
||||
type: string;
|
||||
known: string[];
|
||||
}>;
|
||||
acTotal: {
|
||||
acProfBonus: number;
|
||||
acAbilityBonus: number;
|
||||
acItemBonus: number;
|
||||
acTotal: number;
|
||||
shieldBonus: number | null;
|
||||
};
|
||||
pets: unknown[];
|
||||
familiars: unknown[];
|
||||
}
|
||||
|
||||
export interface PathbuilderJson {
|
||||
success: boolean;
|
||||
build: PathbuilderBuild;
|
||||
}
|
||||
|
||||
export class PathbuilderImportDto {
|
||||
@ApiProperty({ description: 'Raw Pathbuilder export JSON' })
|
||||
@IsObject()
|
||||
pathbuilderJson: PathbuilderJson;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateCharacterDto } from './create-character.dto';
|
||||
|
||||
export class UpdateCharacterDto extends PartialType(CreateCharacterDto) {}
|
||||
407
server/src/modules/characters/pathbuilder-import.service.ts
Normal file
407
server/src/modules/characters/pathbuilder-import.service.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { TranslationsService } from '../translations/translations.service';
|
||||
import {
|
||||
AbilityType,
|
||||
Proficiency,
|
||||
FeatSource,
|
||||
TranslationType,
|
||||
CharacterType,
|
||||
} from '../../generated/prisma/client.js';
|
||||
import { PathbuilderJson, PathbuilderBuild } from './dto/pathbuilder-import.dto';
|
||||
|
||||
// Skill name mappings (English -> German)
|
||||
const SKILL_TRANSLATIONS: Record<string, string> = {
|
||||
acrobatics: 'Akrobatik',
|
||||
arcana: 'Arkane Künste',
|
||||
athletics: 'Athletik',
|
||||
crafting: 'Handwerk',
|
||||
deception: 'Täuschung',
|
||||
diplomacy: 'Diplomatie',
|
||||
intimidation: 'Einschüchtern',
|
||||
medicine: 'Medizin',
|
||||
nature: 'Naturkunde',
|
||||
occultism: 'Okkultismus',
|
||||
performance: 'Darbietung',
|
||||
religion: 'Religionskunde',
|
||||
society: 'Gesellschaftskunde',
|
||||
stealth: 'Heimlichkeit',
|
||||
survival: 'Überleben',
|
||||
thievery: 'Diebeskunst',
|
||||
};
|
||||
|
||||
// Proficiency value mappings (Pathbuilder uses 0, 2, 4, 6, 8)
|
||||
function proficiencyFromValue(value: number): Proficiency {
|
||||
switch (value) {
|
||||
case 8: return Proficiency.LEGENDARY;
|
||||
case 6: return Proficiency.MASTER;
|
||||
case 4: return Proficiency.EXPERT;
|
||||
case 2: return Proficiency.TRAINED;
|
||||
default: return Proficiency.UNTRAINED;
|
||||
}
|
||||
}
|
||||
|
||||
// Feat source mapping
|
||||
function featSourceFromType(type: string): FeatSource {
|
||||
const normalized = type.toLowerCase();
|
||||
if (normalized.includes('class')) return FeatSource.CLASS;
|
||||
if (normalized.includes('ancestry') || normalized.includes('human')) return FeatSource.ANCESTRY;
|
||||
if (normalized.includes('skill')) return FeatSource.SKILL;
|
||||
if (normalized.includes('general')) return FeatSource.GENERAL;
|
||||
if (normalized.includes('archetype')) return FeatSource.ARCHETYPE;
|
||||
return FeatSource.BONUS;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PathbuilderImportService {
|
||||
private readonly logger = new Logger(PathbuilderImportService.name);
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private translationsService: TranslationsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Import a character from Pathbuilder JSON
|
||||
*/
|
||||
async importCharacter(
|
||||
campaignId: string,
|
||||
ownerId: string,
|
||||
pathbuilderJson: PathbuilderJson,
|
||||
) {
|
||||
const build = pathbuilderJson.build;
|
||||
this.logger.log(`Importing character: ${build.name} (Level ${build.level} ${build.class})`);
|
||||
|
||||
// Calculate HP
|
||||
const conModifier = Math.floor((build.abilities.con - 10) / 2);
|
||||
const hpMax = build.attributes.ancestryhp +
|
||||
build.attributes.classhp +
|
||||
build.attributes.bonushp +
|
||||
(conModifier * build.level) +
|
||||
(build.attributes.bonushpPerLevel * build.level);
|
||||
|
||||
// Translate basics
|
||||
const [classTranslation, ancestryTranslation, heritageTranslation, backgroundTranslation] =
|
||||
await Promise.all([
|
||||
this.translationsService.getTranslation(TranslationType.CLASS, build.class),
|
||||
this.translationsService.getTranslation(TranslationType.ANCESTRY, build.ancestry),
|
||||
this.translationsService.getTranslation(TranslationType.HERITAGE, build.heritage),
|
||||
this.translationsService.getTranslation(TranslationType.BACKGROUND, build.background),
|
||||
]);
|
||||
|
||||
// Create character
|
||||
const character = await this.prisma.character.create({
|
||||
data: {
|
||||
campaignId,
|
||||
ownerId,
|
||||
name: build.name,
|
||||
type: CharacterType.PC,
|
||||
level: build.level,
|
||||
hpCurrent: hpMax,
|
||||
hpMax,
|
||||
hpTemp: 0,
|
||||
ancestryId: build.ancestry,
|
||||
heritageId: build.heritage,
|
||||
classId: build.class,
|
||||
backgroundId: build.background,
|
||||
experiencePoints: build.xp || 0,
|
||||
pathbuilderData: JSON.parse(JSON.stringify({
|
||||
...pathbuilderJson,
|
||||
translations: {
|
||||
class: classTranslation,
|
||||
ancestry: ancestryTranslation,
|
||||
heritage: heritageTranslation,
|
||||
background: backgroundTranslation,
|
||||
},
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`Character created with ID: ${character.id}`);
|
||||
|
||||
// Import all related data in parallel
|
||||
await Promise.all([
|
||||
this.importAbilities(character.id, build),
|
||||
this.importSkills(character.id, build),
|
||||
this.importFeats(character.id, build),
|
||||
this.importItems(character.id, build),
|
||||
this.importResources(character.id, build),
|
||||
]);
|
||||
|
||||
// Fetch complete character with relations
|
||||
return this.prisma.character.findUnique({
|
||||
where: { id: character.id },
|
||||
include: {
|
||||
owner: { select: { id: true, username: true } },
|
||||
abilities: true,
|
||||
skills: { orderBy: { skillName: 'asc' } },
|
||||
feats: { orderBy: [{ level: 'asc' }, { name: 'asc' }] },
|
||||
items: { orderBy: { name: 'asc' } },
|
||||
resources: true,
|
||||
conditions: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Import ability scores
|
||||
*/
|
||||
private async importAbilities(characterId: string, build: PathbuilderBuild) {
|
||||
const abilities = [
|
||||
{ ability: AbilityType.STR, score: build.abilities.str },
|
||||
{ ability: AbilityType.DEX, score: build.abilities.dex },
|
||||
{ ability: AbilityType.CON, score: build.abilities.con },
|
||||
{ ability: AbilityType.INT, score: build.abilities.int },
|
||||
{ ability: AbilityType.WIS, score: build.abilities.wis },
|
||||
{ ability: AbilityType.CHA, score: build.abilities.cha },
|
||||
];
|
||||
|
||||
await this.prisma.characterAbility.createMany({
|
||||
data: abilities.map(a => ({ ...a, characterId })),
|
||||
});
|
||||
|
||||
this.logger.debug(`Imported ${abilities.length} abilities`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import skills with proficiencies
|
||||
*/
|
||||
private async importSkills(characterId: string, build: PathbuilderBuild) {
|
||||
const prof = build.proficiencies;
|
||||
const skills = [
|
||||
{ skillName: 'Acrobatics', skillNameGerman: SKILL_TRANSLATIONS.acrobatics, proficiency: proficiencyFromValue(prof.acrobatics) },
|
||||
{ skillName: 'Arcana', skillNameGerman: SKILL_TRANSLATIONS.arcana, proficiency: proficiencyFromValue(prof.arcana) },
|
||||
{ skillName: 'Athletics', skillNameGerman: SKILL_TRANSLATIONS.athletics, proficiency: proficiencyFromValue(prof.athletics) },
|
||||
{ skillName: 'Crafting', skillNameGerman: SKILL_TRANSLATIONS.crafting, proficiency: proficiencyFromValue(prof.crafting) },
|
||||
{ skillName: 'Deception', skillNameGerman: SKILL_TRANSLATIONS.deception, proficiency: proficiencyFromValue(prof.deception) },
|
||||
{ skillName: 'Diplomacy', skillNameGerman: SKILL_TRANSLATIONS.diplomacy, proficiency: proficiencyFromValue(prof.diplomacy) },
|
||||
{ skillName: 'Intimidation', skillNameGerman: SKILL_TRANSLATIONS.intimidation, proficiency: proficiencyFromValue(prof.intimidation) },
|
||||
{ skillName: 'Medicine', skillNameGerman: SKILL_TRANSLATIONS.medicine, proficiency: proficiencyFromValue(prof.medicine) },
|
||||
{ skillName: 'Nature', skillNameGerman: SKILL_TRANSLATIONS.nature, proficiency: proficiencyFromValue(prof.nature) },
|
||||
{ skillName: 'Occultism', skillNameGerman: SKILL_TRANSLATIONS.occultism, proficiency: proficiencyFromValue(prof.occultism) },
|
||||
{ skillName: 'Performance', skillNameGerman: SKILL_TRANSLATIONS.performance, proficiency: proficiencyFromValue(prof.performance) },
|
||||
{ skillName: 'Religion', skillNameGerman: SKILL_TRANSLATIONS.religion, proficiency: proficiencyFromValue(prof.religion) },
|
||||
{ skillName: 'Society', skillNameGerman: SKILL_TRANSLATIONS.society, proficiency: proficiencyFromValue(prof.society) },
|
||||
{ skillName: 'Stealth', skillNameGerman: SKILL_TRANSLATIONS.stealth, proficiency: proficiencyFromValue(prof.stealth) },
|
||||
{ skillName: 'Survival', skillNameGerman: SKILL_TRANSLATIONS.survival, proficiency: proficiencyFromValue(prof.survival) },
|
||||
{ skillName: 'Thievery', skillNameGerman: SKILL_TRANSLATIONS.thievery, proficiency: proficiencyFromValue(prof.thievery) },
|
||||
// Saves
|
||||
{ skillName: 'Perception', skillNameGerman: 'Wahrnehmung', proficiency: proficiencyFromValue(prof.perception) },
|
||||
{ skillName: 'Fortitude', skillNameGerman: 'Zähigkeit', proficiency: proficiencyFromValue(prof.fortitude) },
|
||||
{ skillName: 'Reflex', skillNameGerman: 'Reflex', proficiency: proficiencyFromValue(prof.reflex) },
|
||||
{ skillName: 'Will', skillNameGerman: 'Wille', proficiency: proficiencyFromValue(prof.will) },
|
||||
];
|
||||
|
||||
// Add lore skills
|
||||
for (const [loreName, profValue] of build.lores) {
|
||||
skills.push({
|
||||
skillName: `Lore: ${loreName}`,
|
||||
skillNameGerman: `Wissen: ${loreName}`,
|
||||
proficiency: proficiencyFromValue(profValue),
|
||||
});
|
||||
}
|
||||
|
||||
await this.prisma.characterSkill.createMany({
|
||||
data: skills.map(s => ({
|
||||
characterId,
|
||||
skillName: s.skillName,
|
||||
proficiency: s.proficiency,
|
||||
})),
|
||||
});
|
||||
|
||||
this.logger.debug(`Imported ${skills.length} skills`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import feats with translations
|
||||
*/
|
||||
private async importFeats(characterId: string, build: PathbuilderBuild) {
|
||||
if (!build.feats || build.feats.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract feat names for batch translation
|
||||
const featNames = build.feats.map(f => ({ englishName: f[0] as string }));
|
||||
const translations = await this.translationsService.getTranslationsBatch(
|
||||
TranslationType.FEAT,
|
||||
featNames,
|
||||
);
|
||||
|
||||
const feats = build.feats.map(feat => {
|
||||
const name = feat[0] as string;
|
||||
const featType = feat[2] as string;
|
||||
const level = feat[3] as number;
|
||||
const translation = translations.get(name);
|
||||
|
||||
return {
|
||||
characterId,
|
||||
name,
|
||||
nameGerman: translation?.germanName || name,
|
||||
level,
|
||||
source: featSourceFromType(featType),
|
||||
};
|
||||
});
|
||||
|
||||
await this.prisma.characterFeat.createMany({ data: feats });
|
||||
this.logger.debug(`Imported ${feats.length} feats`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import items (weapons, armor, equipment)
|
||||
*/
|
||||
private async importItems(characterId: string, build: PathbuilderBuild) {
|
||||
const items: Array<{
|
||||
characterId: string;
|
||||
name: string;
|
||||
nameGerman?: string;
|
||||
quantity: number;
|
||||
bulk: number;
|
||||
equipped: boolean;
|
||||
invested: boolean;
|
||||
notes?: string;
|
||||
}> = [];
|
||||
|
||||
// Collect all item names for batch translation
|
||||
const itemNames: Array<{ englishName: string }> = [];
|
||||
|
||||
// Weapons
|
||||
for (const weapon of build.weapons || []) {
|
||||
itemNames.push({ englishName: weapon.name });
|
||||
items.push({
|
||||
characterId,
|
||||
name: weapon.name,
|
||||
quantity: weapon.qty || 1,
|
||||
bulk: 1, // Default bulk for weapons
|
||||
equipped: true,
|
||||
invested: false,
|
||||
notes: `${weapon.die} ${weapon.damageType}${weapon.extraDamage?.length ? ' + ' + weapon.extraDamage.join(', ') : ''}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Armor
|
||||
for (const armor of build.armor || []) {
|
||||
itemNames.push({ englishName: armor.name });
|
||||
items.push({
|
||||
characterId,
|
||||
name: armor.name,
|
||||
quantity: armor.qty || 1,
|
||||
bulk: 1, // Default bulk for armor
|
||||
equipped: armor.worn,
|
||||
invested: false,
|
||||
});
|
||||
}
|
||||
|
||||
// General equipment
|
||||
for (const equip of build.equipment || []) {
|
||||
const name = equip[0] as string;
|
||||
const qty = equip[1] as number;
|
||||
const invested = equip[2] === 'Invested';
|
||||
|
||||
itemNames.push({ englishName: name });
|
||||
items.push({
|
||||
characterId,
|
||||
name,
|
||||
quantity: qty,
|
||||
bulk: 0,
|
||||
equipped: false,
|
||||
invested,
|
||||
});
|
||||
}
|
||||
|
||||
// Add money as a note item
|
||||
if (build.money) {
|
||||
const moneyNote: string[] = [];
|
||||
if (build.money.pp > 0) moneyNote.push(`${build.money.pp} PP`);
|
||||
if (build.money.gp > 0) moneyNote.push(`${build.money.gp} GP`);
|
||||
if (build.money.sp > 0) moneyNote.push(`${build.money.sp} SP`);
|
||||
if (build.money.cp > 0) moneyNote.push(`${build.money.cp} CP`);
|
||||
|
||||
if (moneyNote.length > 0) {
|
||||
items.push({
|
||||
characterId,
|
||||
name: 'Münzbeutel',
|
||||
quantity: 1,
|
||||
bulk: 0,
|
||||
equipped: false,
|
||||
invested: false,
|
||||
notes: moneyNote.join(', '),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Batch translate items
|
||||
if (itemNames.length > 0) {
|
||||
const translations = await this.translationsService.getTranslationsBatch(
|
||||
TranslationType.EQUIPMENT,
|
||||
itemNames,
|
||||
);
|
||||
|
||||
// Apply translations
|
||||
for (const item of items) {
|
||||
if (item.name !== 'Münzbeutel') {
|
||||
const translation = translations.get(item.name);
|
||||
if (translation) {
|
||||
item.nameGerman = translation.germanName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
await this.prisma.characterItem.createMany({
|
||||
data: items.map(i => ({
|
||||
...i,
|
||||
bulk: i.bulk,
|
||||
})),
|
||||
});
|
||||
this.logger.debug(`Imported ${items.length} items`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import resources (focus points, spell slots, etc.)
|
||||
*/
|
||||
private async importResources(characterId: string, build: PathbuilderBuild) {
|
||||
const resources: Array<{ characterId: string; name: string; current: number; max: number }> = [];
|
||||
|
||||
// Focus Points
|
||||
if (build.focusPoints > 0) {
|
||||
resources.push({
|
||||
characterId,
|
||||
name: 'Fokuspunkte',
|
||||
current: build.focusPoints,
|
||||
max: build.focusPoints,
|
||||
});
|
||||
}
|
||||
|
||||
// Spell slots from spellcasters
|
||||
for (const caster of build.spellCasters || []) {
|
||||
if (caster.perDay) {
|
||||
for (let level = 0; level < caster.perDay.length; level++) {
|
||||
const slots = caster.perDay[level];
|
||||
if (slots > 0) {
|
||||
resources.push({
|
||||
characterId,
|
||||
name: level === 0 ? 'Zaubertricks' : `Zauberplätze Grad ${level}`,
|
||||
current: slots,
|
||||
max: slots,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hero Points (always starts at 1)
|
||||
resources.push({
|
||||
characterId,
|
||||
name: 'Heldenpunkte',
|
||||
current: 1,
|
||||
max: 3,
|
||||
});
|
||||
|
||||
if (resources.length > 0) {
|
||||
await this.prisma.characterResource.createMany({ data: resources });
|
||||
this.logger.debug(`Imported ${resources.length} resources`);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
server/src/modules/claude/claude.module.ts
Normal file
9
server/src/modules/claude/claude.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ClaudeService } from './claude.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [ClaudeService],
|
||||
exports: [ClaudeService],
|
||||
})
|
||||
export class ClaudeModule {}
|
||||
138
server/src/modules/claude/claude.service.ts
Normal file
138
server/src/modules/claude/claude.service.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
export interface TranslationRequest {
|
||||
type: 'FEAT' | 'ITEM' | 'SPELL' | 'ACTION' | 'SKILL' | 'CLASS' | 'ANCESTRY' | 'HERITAGE' | 'BACKGROUND' | 'CONDITION' | 'TRAIT';
|
||||
englishName: string;
|
||||
englishDescription?: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export interface TranslationResponse {
|
||||
englishName: string;
|
||||
germanName: string;
|
||||
germanDescription?: string;
|
||||
translationQuality: 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ClaudeService {
|
||||
private readonly logger = new Logger(ClaudeService.name);
|
||||
private client: Anthropic;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const apiKey = this.configService.get<string>('ANTHROPIC_API_KEY');
|
||||
if (apiKey) {
|
||||
this.client = new Anthropic({ apiKey });
|
||||
this.logger.log('Claude API initialized');
|
||||
} else {
|
||||
this.logger.warn('ANTHROPIC_API_KEY not set - translations will be disabled');
|
||||
}
|
||||
}
|
||||
|
||||
async translateBatch(items: TranslationRequest[]): Promise<TranslationResponse[]> {
|
||||
if (!this.client) {
|
||||
this.logger.warn('Claude API not available, returning untranslated items');
|
||||
return items.map(item => ({
|
||||
englishName: item.englishName,
|
||||
germanName: item.englishName,
|
||||
germanDescription: item.englishDescription,
|
||||
translationQuality: 'LOW' as const,
|
||||
}));
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const itemsList = items.map((item, i) =>
|
||||
`${i + 1}. Type: ${item.type}, Name: "${item.englishName}"${item.englishDescription ? `, Description: "${item.englishDescription}"` : ''}${item.context ? `, Context: ${item.context}` : ''}`
|
||||
).join('\n');
|
||||
|
||||
const prompt = `Du bist ein Übersetzer für Pathfinder 2e Spielinhalte von Englisch nach Deutsch.
|
||||
|
||||
WICHTIGE ÜBERSETZUNGSREGELN:
|
||||
- "Feat" = "Talent"
|
||||
- "Action" = "Aktion"
|
||||
- "Spell" = "Zauber"
|
||||
- "Weapon" = "Waffe"
|
||||
- "Armor" = "Rüstung"
|
||||
- "Shield" = "Schild"
|
||||
- "Item" = "Gegenstand"
|
||||
- "Skill" = "Fertigkeit"
|
||||
- "Class" = "Klasse"
|
||||
- "Ancestry" = "Abstammung"
|
||||
- "Heritage" = "Erbe"
|
||||
- "Background" = "Hintergrund"
|
||||
- "Condition" = "Zustand"
|
||||
- "Trait" = "Merkmal"
|
||||
|
||||
Behalte Pathfinder-spezifische Begriffe bei (z.B. "Versatile", "Finesse" bleiben auf Englisch als Spielmechanik-Begriffe).
|
||||
Übersetze Eigennamen nicht (z.B. "Alchemist's Fire" → "Alchemistenfeuer", aber "Bane" bleibt "Bane").
|
||||
|
||||
Übersetze folgende ${items.length} Einträge:
|
||||
|
||||
${itemsList}
|
||||
|
||||
Antworte NUR mit einem JSON-Array in diesem Format:
|
||||
[
|
||||
{
|
||||
"englishName": "Original English Name",
|
||||
"germanName": "Deutscher Name",
|
||||
"germanDescription": "Deutsche Beschreibung (falls vorhanden)",
|
||||
"confidence": 0.9
|
||||
}
|
||||
]
|
||||
|
||||
Gib confidence zwischen 0.0 und 1.0 an basierend auf der Übersetzungsqualität.`;
|
||||
|
||||
const response = await this.client.messages.create({
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
max_tokens: 4000,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
||||
const content = response.content[0];
|
||||
if (content.type !== 'text') {
|
||||
throw new Error('Unexpected response type from Claude');
|
||||
}
|
||||
|
||||
// Extract JSON from response
|
||||
const jsonMatch = content.text.match(/\[[\s\S]*\]/);
|
||||
if (!jsonMatch) {
|
||||
this.logger.error('Could not extract JSON from Claude response');
|
||||
throw new Error('Invalid JSON response from Claude');
|
||||
}
|
||||
|
||||
const translations = JSON.parse(jsonMatch[0]) as Array<{
|
||||
englishName: string;
|
||||
germanName: string;
|
||||
germanDescription?: string;
|
||||
confidence: number;
|
||||
}>;
|
||||
|
||||
return translations.map(t => ({
|
||||
englishName: t.englishName,
|
||||
germanName: t.germanName,
|
||||
germanDescription: t.germanDescription,
|
||||
translationQuality: t.confidence >= 0.8 ? 'HIGH' : t.confidence >= 0.6 ? 'MEDIUM' : 'LOW',
|
||||
}));
|
||||
} catch (error) {
|
||||
this.logger.error('Claude translation failed:', error);
|
||||
// Return untranslated items as fallback
|
||||
return items.map(item => ({
|
||||
englishName: item.englishName,
|
||||
germanName: item.englishName,
|
||||
germanDescription: item.englishDescription,
|
||||
translationQuality: 'LOW' as const,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async translateSingle(item: TranslationRequest): Promise<TranslationResponse> {
|
||||
const results = await this.translateBatch([item]);
|
||||
return results[0];
|
||||
}
|
||||
}
|
||||
2
server/src/modules/claude/index.ts
Normal file
2
server/src/modules/claude/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './claude.service';
|
||||
export * from './claude.module';
|
||||
3
server/src/modules/translations/index.ts
Normal file
3
server/src/modules/translations/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './translations.service';
|
||||
export * from './translations.controller';
|
||||
export * from './translations.module';
|
||||
57
server/src/modules/translations/translations.controller.ts
Normal file
57
server/src/modules/translations/translations.controller.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { TranslationsService } from './translations.service';
|
||||
import { TranslationType } from '../../generated/prisma/client.js';
|
||||
|
||||
class TranslateRequestDto {
|
||||
type: TranslationType;
|
||||
englishName: string;
|
||||
englishDescription?: string;
|
||||
}
|
||||
|
||||
class TranslateBatchRequestDto {
|
||||
type: TranslationType;
|
||||
items: Array<{ englishName: string; englishDescription?: string }>;
|
||||
}
|
||||
|
||||
@ApiTags('translations')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('translations')
|
||||
export class TranslationsController {
|
||||
constructor(private translationsService: TranslationsService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Translate a single item' })
|
||||
async translate(@Body() dto: TranslateRequestDto) {
|
||||
return this.translationsService.getTranslation(
|
||||
dto.type,
|
||||
dto.englishName,
|
||||
dto.englishDescription,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('batch')
|
||||
@ApiOperation({ summary: 'Translate multiple items' })
|
||||
async translateBatch(@Body() dto: TranslateBatchRequestDto) {
|
||||
const result = await this.translationsService.getTranslationsBatch(
|
||||
dto.type,
|
||||
dto.items,
|
||||
);
|
||||
return Object.fromEntries(result);
|
||||
}
|
||||
|
||||
@Get(':type')
|
||||
@ApiOperation({ summary: 'Get all cached translations of a type' })
|
||||
async getAllByType(@Param('type') type: TranslationType) {
|
||||
return this.translationsService.getAllByType(type);
|
||||
}
|
||||
}
|
||||
10
server/src/modules/translations/translations.module.ts
Normal file
10
server/src/modules/translations/translations.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TranslationsService } from './translations.service';
|
||||
import { TranslationsController } from './translations.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [TranslationsController],
|
||||
providers: [TranslationsService],
|
||||
exports: [TranslationsService],
|
||||
})
|
||||
export class TranslationsModule {}
|
||||
159
server/src/modules/translations/translations.service.ts
Normal file
159
server/src/modules/translations/translations.service.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { ClaudeService, TranslationRequest, TranslationResponse } from '../claude/claude.service';
|
||||
import { TranslationType, TranslationQuality } from '../../generated/prisma/client.js';
|
||||
|
||||
@Injectable()
|
||||
export class TranslationsService {
|
||||
private readonly logger = new Logger(TranslationsService.name);
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private claudeService: ClaudeService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a translation from cache or translate via Claude
|
||||
*/
|
||||
async getTranslation(
|
||||
type: TranslationType,
|
||||
englishName: string,
|
||||
englishDescription?: string,
|
||||
): Promise<TranslationResponse> {
|
||||
// Check cache first
|
||||
const cached = await this.prisma.translation.findUnique({
|
||||
where: { type_englishName: { type, englishName } },
|
||||
});
|
||||
|
||||
if (cached && !this.isIncomplete(cached.germanDescription)) {
|
||||
this.logger.debug(`Cache hit for ${type}: ${englishName}`);
|
||||
return {
|
||||
englishName: cached.englishName,
|
||||
germanName: cached.germanName,
|
||||
germanDescription: cached.germanDescription || undefined,
|
||||
translationQuality: cached.quality,
|
||||
};
|
||||
}
|
||||
|
||||
// Translate via Claude
|
||||
this.logger.debug(`Cache miss for ${type}: ${englishName}, calling Claude`);
|
||||
const translation = await this.claudeService.translateSingle({
|
||||
type: type as TranslationRequest['type'],
|
||||
englishName,
|
||||
englishDescription,
|
||||
});
|
||||
|
||||
// Cache the result
|
||||
await this.upsertTranslation(type, translation);
|
||||
|
||||
return translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple translations, using cache where possible
|
||||
*/
|
||||
async getTranslationsBatch(
|
||||
type: TranslationType,
|
||||
items: Array<{ englishName: string; englishDescription?: string }>,
|
||||
): Promise<Map<string, TranslationResponse>> {
|
||||
const result = new Map<string, TranslationResponse>();
|
||||
|
||||
if (items.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check cache for all items
|
||||
const englishNames = items.map(i => i.englishName);
|
||||
const cached = await this.prisma.translation.findMany({
|
||||
where: {
|
||||
type,
|
||||
englishName: { in: englishNames },
|
||||
},
|
||||
});
|
||||
|
||||
const cachedMap = new Map(cached.map(c => [c.englishName, c]));
|
||||
const toTranslate: TranslationRequest[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const cachedItem = cachedMap.get(item.englishName);
|
||||
if (cachedItem && !this.isIncomplete(cachedItem.germanDescription)) {
|
||||
result.set(item.englishName, {
|
||||
englishName: cachedItem.englishName,
|
||||
germanName: cachedItem.germanName,
|
||||
germanDescription: cachedItem.germanDescription || undefined,
|
||||
translationQuality: cachedItem.quality,
|
||||
});
|
||||
} else {
|
||||
toTranslate.push({
|
||||
type: type as TranslationRequest['type'],
|
||||
englishName: item.englishName,
|
||||
englishDescription: item.englishDescription,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`${type}: ${cached.length} cached, ${toTranslate.length} to translate`);
|
||||
|
||||
// Translate missing items in batches of 20
|
||||
if (toTranslate.length > 0) {
|
||||
const batchSize = 20;
|
||||
for (let i = 0; i < toTranslate.length; i += batchSize) {
|
||||
const batch = toTranslate.slice(i, i + batchSize);
|
||||
const translations = await this.claudeService.translateBatch(batch);
|
||||
|
||||
for (const translation of translations) {
|
||||
result.set(translation.englishName, translation);
|
||||
await this.upsertTranslation(type, translation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store or update a translation in the cache
|
||||
*/
|
||||
async upsertTranslation(
|
||||
type: TranslationType,
|
||||
translation: TranslationResponse,
|
||||
): Promise<void> {
|
||||
await this.prisma.translation.upsert({
|
||||
where: {
|
||||
type_englishName: { type, englishName: translation.englishName },
|
||||
},
|
||||
update: {
|
||||
germanName: translation.germanName,
|
||||
germanDescription: translation.germanDescription,
|
||||
quality: translation.translationQuality as TranslationQuality,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
type,
|
||||
englishName: translation.englishName,
|
||||
germanName: translation.germanName,
|
||||
germanDescription: translation.germanDescription,
|
||||
quality: translation.translationQuality as TranslationQuality,
|
||||
translatedBy: 'claude-api',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cached translations of a type
|
||||
*/
|
||||
async getAllByType(type: TranslationType) {
|
||||
return this.prisma.translation.findMany({
|
||||
where: { type },
|
||||
orderBy: { englishName: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a translation is incomplete (truncated)
|
||||
*/
|
||||
private isIncomplete(description?: string | null): boolean {
|
||||
if (!description) return false;
|
||||
return description.trim().endsWith('…') || description.trim().endsWith('...');
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import 'dotenv/config';
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '../generated/prisma';
|
||||
import { PrismaClient } from '../generated/prisma/client.js';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
constructor() {
|
||||
const adapter = new PrismaPg({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
super({
|
||||
adapter,
|
||||
log: process.env.NODE_ENV === 'development'
|
||||
? ['query', 'info', 'warn', 'error']
|
||||
: ['error'],
|
||||
|
||||
Reference in New Issue
Block a user