diff --git a/api/package-lock.json b/api/package-lock.json index c7c5f470..a8dfcacb 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -24,7 +24,7 @@ "jsonwebtoken": "^9.0.2", "nodemailer": "^6.10.1", "nodemailer-express-handlebars": "^6.1.2", - "prisma": "^6.18.0", + "prisma": "^6.16.3", "prisma-json-types-generator": "^3.6.2", "prom-client": "^15.1.3", "pure-rand": "^7.0.1", @@ -3627,6 +3627,14 @@ "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, "node_modules/@jest/environment": { @@ -3643,6 +3651,51 @@ }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@jest/expect": { @@ -4359,6 +4412,7 @@ "integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18" }, @@ -4485,178 +4539,826 @@ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@redocly/ajv": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.3.tgz", + "integrity": "sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/cli": { + "version": "1.34.5", + "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.34.5.tgz", + "integrity": "sha512-5IEwxs7SGP5KEXjBKLU8Ffdz9by/KqNSeBk6YUVQaGxMXK//uYlTJIPntgUXbo1KAGG2d2q2XF8y4iFz6qNeiw==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-node": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0", + "@redocly/config": "^0.22.0", + "@redocly/openapi-core": "1.34.5", + "@redocly/respect-core": "1.34.5", + "abort-controller": "^3.0.0", + "chokidar": "^3.5.1", + "colorette": "^1.2.0", + "core-js": "^3.32.1", + "dotenv": "16.4.7", + "form-data": "^4.0.4", + "get-port-please": "^3.0.1", + "glob": "^7.1.6", + "handlebars": "^4.7.6", + "mobx": "^6.0.4", + "pluralize": "^8.0.0", + "react": "^17.0.0 || ^18.2.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.2.0 || ^19.0.0", + "redoc": "2.5.0", + "semver": "^7.5.2", + "simple-websocket": "^9.0.0", + "styled-components": "^6.0.7", + "yargs": "17.0.1" + }, + "bin": { + "openapi": "bin/cli.js", + "redocly": "bin/cli.js" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/cli/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@redocly/cli/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.5", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.5.tgz", + "integrity": "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==", + "license": "MIT", + "dependencies": { + "@redocly/ajv": "^8.11.2", + "@redocly/config": "^0.22.0", + "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.5", + "js-levenshtein": "^1.1.6", + "js-yaml": "^4.1.0", + "minimatch": "^5.0.1", + "pluralize": "^8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/respect-core": { + "version": "1.34.5", + "resolved": "https://registry.npmjs.org/@redocly/respect-core/-/respect-core-1.34.5.tgz", + "integrity": "sha512-GheC/g/QFztPe9UA9LamooSplQuy9pe0Yr8XGTqkz0ahivLDl7svoy/LSQNn1QH3XGtLKwFYMfTwFR2TAYyh5Q==", + "license": "MIT", + "dependencies": { + "@faker-js/faker": "^7.6.0", + "@redocly/ajv": "8.11.2", + "@redocly/openapi-core": "1.34.5", + "better-ajv-errors": "^1.2.0", + "colorette": "^2.0.20", + "concat-stream": "^2.0.0", + "cookie": "^0.7.2", + "dotenv": "16.4.7", + "form-data": "^4.0.4", + "jest-diff": "^29.3.1", + "jest-matcher-utils": "^29.3.1", + "js-yaml": "4.1.0", + "json-pointer": "^0.6.2", + "jsonpath-plus": "^10.0.6", + "open": "^10.1.0", + "openapi-sampler": "^1.6.1", + "outdent": "^0.8.0", + "set-cookie-parser": "^2.3.5", + "undici": "^6.21.1" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/respect-core/node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/respect-core/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/@redocly/respect-core/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.0.tgz", + "integrity": "sha512-PLUYa+SUKOEZtXFURBu/CNxlsxfaFGxSBPcStL13KpVeVWIfdezWyDqkz7iDLmwnxojXD0s5KzuB5HGHvt4Aeg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.3.0.tgz", + "integrity": "sha512-9oH+n8AVNiLPK/iK/agOsoWfrKZ3FGP3502tkksd6SRsKMYiu7AFX0YXo6YBADdsAj7C+G/aLKdsafIJHxuCkQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.14.0.tgz", + "integrity": "sha512-XJ4z5FxvY/t0Dibms/+gLJrI5niRoY0BCmE02fwmPcRYFPI4KI876xaE79YGWIKnEslMbuQPsIEsoU/DXa0DoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-stream": "^4.4.0", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.0.tgz", + "integrity": "sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.0.tgz", + "integrity": "sha512-BG3KSmsx9A//KyIfw+sqNmWFr1YBUr+TwpxFT7yPqAk0yyDh7oSNgzfNH7pS6OC099EGx2ltOULvumCFe8bcgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.0", + "@smithy/querystring-builder": "^4.2.0", + "@smithy/types": "^4.6.0", + "@smithy/util-base64": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.0.tgz", + "integrity": "sha512-ugv93gOhZGysTctZh9qdgng8B+xO0cj+zN0qAZ+Sgh7qTQGPOJbMdIuyP89KNfUyfAqFSNh5tMvC+h2uCpmTtA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.0.tgz", + "integrity": "sha512-ZmK5X5fUPAbtvRcUPtk28aqIClVhbfcmfoS4M7UQBTnDdrNxhsrxYVv0ZEl5NaPSyExsPWqL4GsPlRvtlwg+2A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.0.tgz", + "integrity": "sha512-6ZAnwrXFecrA4kIDOcz6aLBhU5ih2is2NdcZtobBDSdSHtE9a+MThB5uqyK4XXesdOCvOcbCm2IGB95birTSOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.0.tgz", + "integrity": "sha512-jFVjuQeV8TkxaRlcCNg0GFVgg98tscsmIrIwRFeC74TIUyLE3jmY9xgc1WXrPQYRjQNK3aRoaIk6fhFRGOIoGw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.14.0", + "@smithy/middleware-serde": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.0.tgz", + "integrity": "sha512-yaVBR0vQnOnzex45zZ8ZrPzUnX73eUC8kVFaAAbn04+6V7lPtxn56vZEBBAhgS/eqD6Zm86o6sJs6FuQVoX5qg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/service-error-classification": "^4.2.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-retry": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.0.tgz", + "integrity": "sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.0.tgz", + "integrity": "sha512-G5CJ//eqRd9OARrQu9MK1H8fNm2sMtqFh6j8/rPozhEL+Dokpvi1Og+aCixTuwDAGZUkJPk6hJT5jchbk/WCyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.0.tgz", + "integrity": "sha512-5QgHNuWdT9j9GwMPPJCKxy2KDxZ3E5l4M3/5TatSZrqYVoEiqQrDfAq8I6KWZw7RZOHtVtCzEPdYz7rHZixwcA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.3.0.tgz", + "integrity": "sha512-RHZ/uWCmSNZ8cneoWEVsVwMZBKy/8123hEpm57vgGXA3Irf/Ja4v9TVshHK2ML5/IqzAZn0WhINHOP9xl+Qy6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/querystring-builder": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.0.tgz", + "integrity": "sha512-rV6wFre0BU6n/tx2Ztn5LdvEdNZ2FasQbPQmDOPfV9QQyDmsCkOAB0osQjotRCQg+nSKFmINhyda0D3AnjSBJw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.0.tgz", + "integrity": "sha512-6POSYlmDnsLKb7r1D3SVm7RaYW6H1vcNcTWGWrF7s9+2noNYvUsm7E4tz5ZQ9HXPmKn6Hb67pBDRIjrT4w/d7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.0.tgz", + "integrity": "sha512-Q4oFD0ZmI8yJkiPPeGUITZj++4HHYCW3pYBYfIobUCkYpI6mbkzmG1MAQQ3lJYYWj3iNqfzOenUZu+jqdPQ16A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.0.tgz", + "integrity": "sha512-BjATSNNyvVbQxOOlKse0b0pSezTWGMvA87SvoFoFlkRsKXVsN3bEtjCxvsNXJXfnAzlWFPaT9DmhWy1vn0sNEA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.0.tgz", + "integrity": "sha512-Ylv1ttUeKatpR0wEOMnHf1hXMktPUMObDClSWl2TpCVT4DwtJhCeighLzSLbgH3jr5pBNM0LDXT5yYxUvZ9WpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.0.tgz", + "integrity": "sha512-VCUPPtNs+rKWlqqntX0CbVvWyjhmX30JCtzO+s5dlzzxrvSfRh5SY0yxnkirvc1c80vdKQttahL71a9EsdolSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.0.tgz", + "integrity": "sha512-MKNyhXEs99xAZaFhm88h+3/V+tCRDQ+PrDzRqL0xdDpq4gjxcMmf5rBA3YXgqZqMZ/XwemZEurCBQMfxZOWq/g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.7.0.tgz", + "integrity": "sha512-3BDx/aCCPf+kkinYf5QQhdQ9UAGihgOVqI3QO5xQfSaIWvUE4KYLtiGRWsNe1SR7ijXC0QEPqofVp5Sb0zC8xQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.14.0", + "@smithy/middleware-endpoint": "^4.3.0", + "@smithy/middleware-stack": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-stream": "^4.4.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.6.0.tgz", + "integrity": "sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.0.tgz", + "integrity": "sha512-AlBmD6Idav2ugmoAL6UtR6ItS7jU5h5RNqLMZC7QrLCoITA9NzIN3nx9GWi8g4z1pfWh2r9r96SX/jHiNwPJ9A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.2.0.tgz", + "integrity": "sha512-+erInz8WDv5KPe7xCsJCp+1WCjSbah9gWcmUXc9NqmhyPx59tf7jqFz+za1tRG1Y5KM1Cy1rWCcGypylFp4mvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.0.tgz", + "integrity": "sha512-U8q1WsSZFjXijlD7a4wsDQOvOwV+72iHSfq1q7VD+V75xP/pdtm0WIGuaFJ3gcADDOKj2MIBn4+zisi140HEnQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@redocly/ajv": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.3.tgz", - "integrity": "sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==", - "license": "MIT", + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js-replace": "^1.0.1" + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@redocly/cli": { - "version": "1.34.5", - "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.34.5.tgz", - "integrity": "sha512-5IEwxs7SGP5KEXjBKLU8Ffdz9by/KqNSeBk6YUVQaGxMXK//uYlTJIPntgUXbo1KAGG2d2q2XF8y4iFz6qNeiw==", - "license": "MIT", + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "1.9.0", - "@opentelemetry/exporter-trace-otlp-http": "0.53.0", - "@opentelemetry/resources": "1.26.0", - "@opentelemetry/sdk-trace-node": "1.26.0", - "@opentelemetry/semantic-conventions": "1.27.0", - "@redocly/config": "^0.22.0", - "@redocly/openapi-core": "1.34.5", - "@redocly/respect-core": "1.34.5", - "abort-controller": "^3.0.0", - "chokidar": "^3.5.1", - "colorette": "^1.2.0", - "core-js": "^3.32.1", - "dotenv": "16.4.7", - "form-data": "^4.0.4", - "get-port-please": "^3.0.1", - "glob": "^7.1.6", - "handlebars": "^4.7.6", - "mobx": "^6.0.4", - "pluralize": "^8.0.0", - "react": "^17.0.0 || ^18.2.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.2.0 || ^19.0.0", - "redoc": "2.5.0", - "semver": "^7.5.2", - "simple-websocket": "^9.0.0", - "styled-components": "^6.0.7", - "yargs": "17.0.1" + "tslib": "^2.6.2" }, - "bin": { - "openapi": "bin/cli.js", - "redocly": "bin/cli.js" + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.2.0.tgz", + "integrity": "sha512-qzHp7ZDk1Ba4LDwQVCNp90xPGqSu7kmL7y5toBpccuhi3AH7dcVBIT/pUxYcInK4jOy6FikrcTGq5wxcka8UaQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.17.0", - "npm": ">=9.5.0" + "node": ">=18.0.0" } }, - "node_modules/@redocly/cli/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "license": "BSD-2-Clause", + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.0.tgz", + "integrity": "sha512-FxUHS3WXgx3bTWR6yQHNHHkQHZm/XKIi/CchTnKvBulN6obWpcbzJ6lDToXn+Wp0QlVKd7uYAz2/CTw1j7m+Kg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.3.0", + "@smithy/credential-provider-imds": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=12" + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.0.tgz", + "integrity": "sha512-TXeCn22D56vvWr/5xPqALc9oO+LN+QpFjrSM7peG/ckqEPoI3zaKZFp+bFwfmiHhn5MGWPaLCqDOJPPIixk9Wg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://dotenvx.com" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@redocly/cli/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { - "node": ">=10" + "node": ">=18.0.0" } }, - "node_modules/@redocly/config": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", - "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", - "license": "MIT" + "node_modules/@smithy/util-middleware": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.0.tgz", + "integrity": "sha512-u9OOfDa43MjagtJZ8AapJcmimP+K2Z7szXn8xbty4aza+7P1wjFmy2ewjSbhEiYQoW1unTlOAIV165weYAaowA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@redocly/openapi-core": { - "version": "1.34.5", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.5.tgz", - "integrity": "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==", - "license": "MIT", + "node_modules/@smithy/util-retry": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.0.tgz", + "integrity": "sha512-BWSiuGbwRnEE2SFfaAZEX0TqaxtvtSYPM/J73PFVm+A29Fg1HTPiYFb8TmX1DXp4hgcdyJcNQmprfd5foeORsg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.22.0", - "colorette": "^1.2.0", - "https-proxy-agent": "^7.0.5", - "js-levenshtein": "^1.1.6", - "js-yaml": "^4.1.0", - "minimatch": "^5.0.1", - "pluralize": "^8.0.0", - "yaml-ast-parser": "0.0.43" + "@smithy/service-error-classification": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.17.0", - "npm": ">=9.5.0" + "node": ">=18.0.0" } }, - "node_modules/@redocly/respect-core": { - "version": "1.34.5", - "resolved": "https://registry.npmjs.org/@redocly/respect-core/-/respect-core-1.34.5.tgz", - "integrity": "sha512-GheC/g/QFztPe9UA9LamooSplQuy9pe0Yr8XGTqkz0ahivLDl7svoy/LSQNn1QH3XGtLKwFYMfTwFR2TAYyh5Q==", - "license": "MIT", + "node_modules/@smithy/util-stream": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.4.0.tgz", + "integrity": "sha512-vtO7ktbixEcrVzMRmpQDnw/Ehr9UWjBvSJ9fyAbadKkC4w5Cm/4lMO8cHz8Ysb8uflvQUNRcuux/oNHKPXkffg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@faker-js/faker": "^7.6.0", - "@redocly/ajv": "8.11.2", - "@redocly/openapi-core": "1.34.5", - "better-ajv-errors": "^1.2.0", - "colorette": "^2.0.20", - "concat-stream": "^2.0.0", - "cookie": "^0.7.2", - "dotenv": "16.4.7", - "form-data": "^4.0.4", - "jest-diff": "^29.3.1", - "jest-matcher-utils": "^29.3.1", - "js-yaml": "4.1.0", - "json-pointer": "^0.6.2", - "jsonpath-plus": "^10.0.6", - "open": "^10.1.0", - "openapi-sampler": "^1.6.1", - "outdent": "^0.8.0", - "set-cookie-parser": "^2.3.5", - "undici": "^6.21.1" + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=18.17.0", - "npm": ">=9.5.0" + "node": ">=18.0.0" } }, "node_modules/@redocly/respect-core/node_modules/@redocly/ajv": { @@ -5943,6 +6645,23 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz", + "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/type-utils": { "version": "8.46.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", @@ -11222,7 +11941,7 @@ "jest-validate": "30.2.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { diff --git a/api/prisma/migrations/20251003121931_translations/migration.sql b/api/prisma/migrations/20251003121931_translations/migration.sql new file mode 100644 index 00000000..b4c68f39 --- /dev/null +++ b/api/prisma/migrations/20251003121931_translations/migration.sql @@ -0,0 +1,18 @@ +-- AlterTable +ALTER TABLE "Goal" ADD COLUMN "translations" JSONB; + +-- AlterTable +ALTER TABLE "Game" ADD COLUMN "translations" TEXT[] DEFAULT ARRAY[]::TEXT[]; + +/* + Warnings: + + - Made the column `translations` on table `Goal` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Game" ADD COLUMN "defaultLanguage" TEXT NOT NULL DEFAULT 'English'; + +-- AlterTable +ALTER TABLE "Goal" ALTER COLUMN "translations" SET NOT NULL, +ALTER COLUMN "translations" SET DEFAULT '{}'; \ No newline at end of file diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 26db060a..480ff6c3 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -6,7 +6,9 @@ generator client { } generator json { - provider = "prisma-json-types-generator" + provider = "prisma-json-types-generator" + namespace = "PrismaJson" + allowAny = false } datasource db { @@ -17,6 +19,9 @@ datasource db { model Goal { id String @id @unique @default(cuid()) goal String + // DO NOT REMOVE + /// [GoalTranslations] + translations Json @default("{}") description String? oldCategories String[] categories Category[] @relation("GoalCategories") @@ -56,6 +61,8 @@ model Game { coverImage String? variants Variant[] goals Goal[] + defaultLanguage String @default("English") + translations String[] @default([]) owners User[] @relation("GameOwners") moderators User[] @relation("GameModerators") createdAt DateTime @default(now()) diff --git a/api/prisma/seed.ts b/api/prisma/seed.ts index dfa97e82..36302bf8 100644 --- a/api/prisma/seed.ts +++ b/api/prisma/seed.ts @@ -77,6 +77,7 @@ async function main() { slug: 'nodiff', enableSRLv5: false, racetimeBeta: false, + translations: ['German'], owners: { connect: [{ id: staff.id }, { id: owner.id }] }, moderators: { connect: [{ id: mod.id }] }, usersFavorited: { @@ -101,6 +102,7 @@ async function main() { gameId: game1.id, goal: `Goal ${i + 1}`, description: `Description for Goal ${i + 1}`, + translations: {German: `Ziel ${i + 1}`}, categories: { connect: { gameId_name: { @@ -141,6 +143,7 @@ async function main() { gameId: game2.id, goal: `Goal ${i + 1}`, description: `Description for Goal ${i + 1}`, + translations: {German: `Ziel ${i + 1}`}, categories: { connect: { gameId_name: { @@ -179,6 +182,7 @@ async function main() { gameId: game3.id, goal: `Goal ${i + 1}`, description: `Description for Goal ${i + 1}`, + translations: {German: `Ziel ${i + 1}`}, categories: { connect: { gameId_name: { @@ -216,6 +220,7 @@ async function main() { gameId: game4.id, goal: `Goal ${i + 1}`, description: `Description for Goal ${i + 1}`, + translations: {German: `Ziel ${i + 1}`}, categories: { connect: { gameId_name: { @@ -254,6 +259,7 @@ async function main() { gameId: game5.id, goal: `Goal ${i + 1}`, description: `Description for Goal ${i + 1}`, + translations: {German: `Ziel ${i + 1}`}, categories: { connect: { gameId_name: { @@ -285,6 +291,7 @@ async function main() { gameId: id, goal: 'Collect 100 Coins', description: 'Gather 100 coins from various levels.', + translations: {German: `Sammle 100 Münzen`}, categories: { connect: ['Collecting'].map((cat) => ({ gameId_name: { @@ -299,6 +306,7 @@ async function main() { gameId: id, goal: 'Defeat the Fire Dragon', description: 'Slay the Fire Dragon in the Volcano Dungeon.', + translations: {German: `Töte den Feuerdrachen`}, categories: { connect: ['Combat', 'Boss Battle'].map((cat) => ({ gameId_name: { @@ -312,6 +320,7 @@ async function main() { { gameId: id, goal: 'Find 3 Hidden Keys', + translations: {German: `Finde 3 versteckte Schluessel`}, description: 'Locate 3 keys hidden throughout the forest.', categories: { connect: ['Exploration', 'Puzzle'].map((cat) => ({ @@ -327,6 +336,7 @@ async function main() { gameId: id, goal: 'Solve the Ancient Temple Puzzle', description: 'Unlock the secret chamber in the temple.', + translations: {German: `Loese das Puzzle im antiken Tempel`}, categories: { connect: ['Puzzle'].map((cat) => ({ gameId_name: { @@ -341,6 +351,7 @@ async function main() { gameId: id, goal: 'Craft a Health Potion', description: 'Use herbs and water to craft a basic health potion.', + translations: {German: `Crafte einen Heilungstrank`}, categories: { connect: ['Resource Management'].map((cat) => ({ gameId_name: { @@ -355,6 +366,7 @@ async function main() { gameId: id, goal: 'Complete the Speedrun Trial', description: 'Finish the speedrun course in under 3 minutes.', + translations: {German: `Schliesse die Speedrun Pruefung ab`}, categories: { connect: ['Time Challenge'].map((cat) => ({ gameId_name: { @@ -369,6 +381,7 @@ async function main() { gameId: id, goal: 'Pickpocket a Guard', description: 'Steal a gold coin from a guard without being caught.', + translations: {German: `Stehle von einer Wache`}, categories: { connect: ['Stealth'].map((cat) => ({ gameId_name: { @@ -383,6 +396,7 @@ async function main() { gameId: id, goal: 'Reach Level 10', description: 'Gain experience and reach level 10.', + translations: {German: `Erreiche Level 10`}, categories: { connect: ['Story Progression'].map((cat) => ({ gameId_name: { @@ -397,6 +411,7 @@ async function main() { gameId: id, goal: 'Defeat 50 Enemies', description: 'Eliminate 50 enemies to prove your strength.', + translations: {German: `Besiege 50 Gegner`}, categories: { connect: ['Combat'].map((cat) => ({ gameId_name: { @@ -411,6 +426,7 @@ async function main() { gameId: id, goal: 'Cross the Lava Pit', description: 'Use platforms to cross the lava pit safely.', + translations: {German: `Ueberquere die Lava Schlucht`}, categories: { connect: ['Movement', 'Puzzle'].map((cat) => ({ gameId_name: { @@ -425,6 +441,7 @@ async function main() { gameId: id, goal: 'Rescue the Lost Villager', description: 'Find and rescue a villager trapped in the cave.', + translations: {German: `Rette den verlorenen Dorfbewohner`}, categories: { connect: ['Exploration', 'Story Progression'].map((cat) => ({ gameId_name: { @@ -439,6 +456,7 @@ async function main() { gameId: id, goal: 'Gather 10 Rare Herbs', description: 'Collect rare herbs from the mountain region.', + translations: {German: `Sammle 10 seltene Kraeuter`}, categories: { connect: ['Collecting', 'Resource Management'].map((cat) => ({ gameId_name: { @@ -453,6 +471,7 @@ async function main() { gameId: id, goal: 'Defeat the Ice Golem', description: 'Defeat the Ice Golem in the Frozen Cavern.', + translations: {German: `Besiege den Eis Golem`}, categories: { connect: ['Combat', 'Boss Battle'].map((cat) => ({ gameId_name: { @@ -467,6 +486,7 @@ async function main() { gameId: id, goal: 'Escape the Dungeon Maze', description: 'Navigate and escape the maze in the dungeon.', + translations: {German: `Fliehe aus dem Labyrinth`}, categories: { connect: ['Puzzle', 'Exploration'].map((cat) => ({ gameId_name: { @@ -481,6 +501,7 @@ async function main() { gameId: id, goal: 'Solve the Color Matching Puzzle', description: 'Match colors correctly to unlock the gate.', + translations: {German: 'Loese das Farb Puzzle'}, categories: { connect: ['Puzzle'].map((cat) => ({ gameId_name: { @@ -495,6 +516,7 @@ async function main() { gameId: id, goal: 'Survive the Night in the Forest', description: 'Survive until morning without getting caught.', + translations: {German: 'Ueberlebe die Nacht im Wald'}, categories: { connect: ['Stealth', 'Time Challenge'].map((cat) => ({ gameId_name: { @@ -509,6 +531,7 @@ async function main() { gameId: id, goal: 'Gather 20 Crystals', description: 'Mine 20 crystals from the underground cave.', + translations: {German: 'Sammle 20 Kristalle'}, categories: { connect: ['Collecting'].map((cat) => ({ gameId_name: { @@ -537,6 +560,7 @@ async function main() { gameId: id, goal: 'Repair the Broken Bridge', description: 'Find materials and repair the bridge.', + translations: {German: 'Repariere die kaputte Bruecke'}, categories: { connect: ['Resource Management', 'Exploration'].map((cat) => ({ gameId_name: { @@ -551,6 +575,7 @@ async function main() { gameId: id, goal: 'Climb the Tower of Trials', description: 'Reach the top of the Tower of Trials.', + translations: {German: 'Erklimme den Turm der Pruefungen'}, categories: { connect: ['Movement', 'Story Progression'].map((cat) => ({ gameId_name: { @@ -565,6 +590,7 @@ async function main() { gameId: id, goal: 'Find the Golden Sword', description: 'Retrieve the legendary Golden Sword.', + translations: {German: 'Finde das goldene Schwert'}, categories: { connect: ['Exploration'].map((cat) => ({ gameId_name: { @@ -579,6 +605,7 @@ async function main() { gameId: id, goal: 'Complete the Puzzle Gauntlet', description: 'Solve a series of puzzles back-to-back.', + translations: {German: 'Schliesse den Puzzle Wettbewerb ab'}, categories: { connect: ['Puzzle'].map((cat) => ({ gameId_name: { @@ -593,6 +620,7 @@ async function main() { gameId: id, goal: 'Steal a Rare Artifact', description: 'Steal an artifact from the museum undetected.', + translations: {German: 'Stehle ein seltenes Artefakt'}, categories: { connect: ['Stealth', 'Resource Management'].map((cat) => ({ gameId_name: { @@ -607,6 +635,7 @@ async function main() { gameId: id, goal: 'Defeat the Dark Lord', description: 'Defeat the final boss and save the kingdom.', + translations: {German: 'Toete den dunken Lord'}, categories: { connect: ['Combat', 'Boss Battle'].map((cat) => ({ gameId_name: { @@ -621,6 +650,7 @@ async function main() { gameId: id, goal: 'Find and Open 10 Treasure Chests', description: 'Locate and open 10 treasure chests.', + translations: {German: 'Finde und oeffne 10 Schatztruhen'}, categories: { connect: ['Collecting'].map((cat) => ({ gameId_name: { @@ -635,6 +665,7 @@ async function main() { gameId: id, goal: 'Survive the Arena for 5 Rounds', description: 'Defeat all enemies across 5 rounds in the arena.', + translations: {German: 'Ueberlebe die Arena fuer 5 Runden'}, categories: { connect: ['Combat', 'Time Challenge'].map((cat) => ({ gameId_name: { @@ -651,6 +682,7 @@ async function main() { description: `Complete side quest number ${ i + 27 } for extra rewards.`, + translations: {German: `Nebenquest ${i + 27}`}, categories: { connect: { gameId_name: { diff --git a/api/src/@types/prisma-json-types/index.ts b/api/src/@types/prisma-json-types/index.ts new file mode 100644 index 00000000..68df1c20 --- /dev/null +++ b/api/src/@types/prisma-json-types/index.ts @@ -0,0 +1,11 @@ +// This file must be a module, so we include an empty export. +export {}; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace PrismaJson { + type GoalTranslations = { + [k: string]: string; + }; + } +} \ No newline at end of file diff --git a/api/src/core/generation/GeneratorCore.ts b/api/src/core/generation/GeneratorCore.ts index edff3716..72ea4a14 100644 --- a/api/src/core/generation/GeneratorCore.ts +++ b/api/src/core/generation/GeneratorCore.ts @@ -2,6 +2,7 @@ export interface GeneratorGoal { id: string; goal: string; description: string | null; + translations: {[k: string]: string}; categories: string[]; difficulty: number | null; } diff --git a/api/src/database/games/Games.ts b/api/src/database/games/Games.ts index 20b6173b..36e27208 100644 --- a/api/src/database/games/Games.ts +++ b/api/src/database/games/Games.ts @@ -173,6 +173,10 @@ export const updateRacetimeGoal = (slug: string, racetimeGoal: string) => { return prisma.game.update({ where: { slug }, data: { racetimeGoal } }); }; +export const updateDefaultLanguage = (slug: string, defaultLanguage: string) => { + return prisma.game.update({ where: { slug }, data: { defaultLanguage } }); +} + export const updateSlugWords = (slug: string, slugWords: string[]) => { return prisma.game.update({ where: { slug }, data: { slugWords } }); }; diff --git a/api/src/database/games/Goals.ts b/api/src/database/games/Goals.ts index 68258d65..613c90e4 100644 --- a/api/src/database/games/Goals.ts +++ b/api/src/database/games/Goals.ts @@ -29,6 +29,7 @@ export const createGoal = async ( goal: string, description?: string, categories?: string[], + translations?: {[lang: string]: string}, difficulty?: number, ) => { const gameId = (await gameForSlug(gameSlug))?.id; @@ -55,6 +56,7 @@ export const createGoal = async ( })), }, difficulty, + translations, game: { connect: { slug: gameSlug } }, }, }); diff --git a/api/src/routes/games/Games.ts b/api/src/routes/games/Games.ts index d4faf292..0b6be81d 100644 --- a/api/src/routes/games/Games.ts +++ b/api/src/routes/games/Games.ts @@ -1,5 +1,13 @@ -import { makeGeneratorSchema } from '@playbingo/shared'; import { Game } from '@playbingo/types'; +import { + GenerationBoardLayout, + GenerationGlobalAdjustments, + GenerationGoalRestriction, + GenerationGoalSelection, + GenerationListMode, + GenerationListTransform, +} from '@prisma/client'; +import { makeGeneratorSchema } from '@playbingo/shared'; import { Router } from 'express'; import { BoardGenerator } from '../../core/generation/BoardGenerator'; import { GenerationFailedError } from '../../core/generation/GenerationFailedError'; @@ -19,6 +27,7 @@ import { removeModerator, removeOwner, unfavoriteGame, + updateDefaultLanguage, updateDescription, updateDifficultyGroups, updateDifficultyVariant, @@ -47,10 +56,13 @@ import { import { getVariant } from '../../database/games/Variants'; import { getUser, getUsersEligibleToModerateGame } from '../../database/Users'; import { deleteFile, saveFile } from '../../media/MediaServer'; +import { translationsRouter } from './translations/Translations'; import variants from './Variants'; const games = Router(); +games.use(translationsRouter); + games.get('/', async (req, res) => { const result = await allGames(req.session.user); res.status(200).json(result); @@ -81,6 +93,8 @@ games.get('/:slug', async (req, res) => { avatar: mod.avatar ?? undefined, })), enableSRLv5: game.enableSRLv5, + defaultLanguage: game.defaultLanguage, + translations: game.translations, racetimeBeta: game.racetimeBeta, racetimeCategory: game.racetimeCategory ?? undefined, racetimeGoal: game.racetimeGoal ?? undefined, @@ -162,6 +176,7 @@ games.post('/:slug', async (req, res) => { enableSRLv5, racetimeCategory, racetimeGoal, + defaultLanguage, difficultyVariantsEnabled, difficultyGroups, slugWords, @@ -199,6 +214,9 @@ games.post('/:slug', async (req, res) => { if (racetimeGoal) { result = await updateRacetimeGoal(slug, racetimeGoal); } + if (defaultLanguage) { + result = await updateDefaultLanguage(slug, defaultLanguage); + } if (difficultyVariantsEnabled !== undefined) { result = await updateDifficultyVariantsEnabled( slug, @@ -261,7 +279,7 @@ games.get('/:slug/goals', async (req, res) => { games.post('/:slug/goals', async (req, res) => { const { slug } = req.params; - const { goal, description, categories, difficulty } = req.body; + const { goal, description, categories, difficulty, translations } = req.body; let difficultyNum: number | undefined = undefined; if (difficulty) { difficultyNum = Number(difficulty); @@ -279,6 +297,7 @@ games.post('/:slug/goals', async (req, res) => { goal, description, categories, + translations, difficultyNum, ); res.status(200).json(newGoal); diff --git a/api/src/routes/games/translations/Translations.ts b/api/src/routes/games/translations/Translations.ts new file mode 100644 index 00000000..a01482b7 --- /dev/null +++ b/api/src/routes/games/translations/Translations.ts @@ -0,0 +1,184 @@ +import { Router } from 'express'; +import { prisma } from '../../../database/Database'; +import { Prisma } from '@prisma/client'; + +const translationsRouter = Router(); + +translationsRouter.get('/:slug/translations', async (req, res) => { + const { slug } = req.params; + + const result = (await prisma.game.findUnique({ where: { slug } })) + ?.translations; + res.json(result); + return; +}); + +translationsRouter.post('/:slug/translations', async (req, res) => { + const { slug } = req.params; + const newTranslations = req.body.translations; + if (!Array.isArray(newTranslations)) { + res.status(400).json({ + error: 'Translations must be an array', + }); + } + const oldTranslations = (await prisma.game.findUnique({ where: { slug } })) + ?.translations; + if (!oldTranslations) { + const dbRes = await prisma.game.update({ + where: { slug }, + data: { translations: newTranslations }, + }); + res.json(dbRes); + return; + } + const allTranslations = oldTranslations.concat(newTranslations); + const dbRes = await prisma.game.update({ + where: { slug }, + data: { translations: allTranslations }, + }); + res.json(dbRes); +}); + +translationsRouter.delete('/:slug/translations', async (req, res) => { + const { slug } = req.params; + const translationsToDelete: string[] = req.body.translations; + if (!Array.isArray(translationsToDelete)) { + res.status(400).json({ + error: 'Translations must be an array', + }); + return; + } + + const goals = await prisma.goal.findMany({ + where: { game: { slug } }, + }); + goals.forEach((goal) => { + const goalTranslations = goal.translations as unknown as { + [k: string]: string; + }; + if (!goalTranslations) { + return; + } + translationsToDelete.forEach((lang) => { + delete goalTranslations[lang]; + }); + }); + + const translations = (await prisma.game.findUnique({ where: { slug } })) + ?.translations; + + if (!translations) { + res.status(404).json({ + error: 'No translations found.', + }); + return; + } + + const translationIndexesUpdate = translationsToDelete.map((lang) => { + return translations.indexOf(lang); + }); + + translationIndexesUpdate.forEach((i) => { + translations.splice(i, 1); + }); + + try { + await prisma.$transaction(async () => { + await Promise.all( + goals.map((goal) => { + return prisma.goal.update({ + where: { id: goal.id }, + data: { translations: goal.translations }, + }); + }), + ); + await prisma.game.update({ + where: { slug }, + data: { translations }, + }); + }); + } catch (error: unknown) { + const e = error as Prisma.PrismaClientKnownRequestError; + res.status(500).json({ success: false, error: e.message }); + } + + res.status(200).json({ success: true }); +}); + +translationsRouter.put('/:slug/translations', async (req, res) => { + const { slug } = req.params; + const updatePairs: { old: string; new: string }[] = req.body.translations; + if (!Array.isArray(updatePairs)) { + res.status(400).json({ + error: 'Translations must be an array, in the form of {old: language, new: language}', + }); + } + const translations = (await prisma.game.findUnique({ where: { slug } })) + ?.translations; + + if (!translations) { + res.status(404).json({ + error: 'No translations found.', + }); + return; + } + + let error = false; + updatePairs.forEach((p) => { + if (!p.old || !p.new) { + res.status(400).json({ + error: 'Translations must be an array, in the form of {old: language, new: language}', + }); + error = true; + return; + } + const i = translations.indexOf(p.old); + if (i === -1) { + res.status(404).json({ + error: `Translation ${p.old} not found.`, + }); + error = true; + return; + } + translations[i] = p.new; + }); + + if (error) return; + + const goals = await prisma.goal.findMany({ where: { game: { slug } } }); + + goals.forEach((goal) => { + if (!goal.translations) { + return; + } + + updatePairs.forEach((p) => { + goal.translations[p.new] = goal.translations[p.old]; + delete goal.translations[p.old]; + }); + }); + + try { + await prisma.$transaction(async () => { + await Promise.all( + goals.map((goal) => { + return prisma.goal.update({ + where: { id: goal.id }, + data: { translations: goal.translations }, + }); + }), + ); + await prisma.game.update({ + where: { slug }, + data: { translations }, + }); + }); + } catch (error: unknown) { + const e = error as Prisma.PrismaClientKnownRequestError; + res.status(500).json({ success: false, error: e.message }); + } + + res.status(200).json({ success: true }); +}); + +export { translationsRouter }; diff --git a/api/src/routes/goals/Goals.ts b/api/src/routes/goals/Goals.ts index d62d802d..fc07ebb3 100644 --- a/api/src/routes/goals/Goals.ts +++ b/api/src/routes/goals/Goals.ts @@ -31,9 +31,9 @@ goals.post('/:id', async (req, res) => { } const { id } = req.params; - const { goal, description, categories, difficulty } = req.body; + const { goal, description, categories, difficulty, translations } = req.body; - if (!goal && description === undefined && !categories && !difficulty) { + if (!goal && description === undefined && !categories && !difficulty && !translations) { res.status(400).send('No changes submitted'); return; } @@ -41,6 +41,7 @@ goals.post('/:id', async (req, res) => { const input: Prisma.GoalUpdateInput = { goal, description, + translations, }; if (difficulty === 0) { diff --git a/api/src/tests/core/boardGenerator.test.ts b/api/src/tests/core/boardGenerator.test.ts index 36a244f1..4e9b8fce 100644 --- a/api/src/tests/core/boardGenerator.test.ts +++ b/api/src/tests/core/boardGenerator.test.ts @@ -20,6 +20,7 @@ const goals: GeneratorGoal[] = Array.from({ length: 100 }).map((_, i) => ({ categories[i % categories.length].name, categories[(i + 1) % categories.length].name, ], + translations: {German: `Ziel ${i + 1}`}, difficulty: (i % 25) + 1, })); diff --git a/api/src/tests/util/WinDetection.test.ts b/api/src/tests/util/WinDetection.test.ts index d79dabb5..2317a11a 100644 --- a/api/src/tests/util/WinDetection.test.ts +++ b/api/src/tests/util/WinDetection.test.ts @@ -11,6 +11,7 @@ const createBoard = (): Cell[][] => id: `${i}`, goal: `Goal ${i + 1}`, categories: [], + translations: {German: `Ziel ${i + 1}`}, difficulty: 0, description: '', })), diff --git a/schema/schemas/Game.json b/schema/schemas/Game.json index d3e2f1f3..5265e3ad 100644 --- a/schema/schemas/Game.json +++ b/schema/schemas/Game.json @@ -42,6 +42,15 @@ "enableSRLv5": { "type": "boolean" }, + "defaultLanguage": { + "type": "string" + }, + "translations": { + "type": "array", + "items": { + "type": "string" + } + }, "racetimeBeta": { "type": "boolean" }, diff --git a/schema/schemas/Goal.json b/schema/schemas/Goal.json index 3f9a2033..e43c023e 100644 --- a/schema/schemas/Goal.json +++ b/schema/schemas/Goal.json @@ -2,13 +2,18 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "additionalProperties": false, - "required": ["id", "goal", "description"], + "required": ["id", "goal", "description", "translations"], "description": "A single objective for a bingo game.", "properties": { "id": {"type": "string"}, "goal": {"type": "string"}, "description": {"type": ["string", "null"]}, - "difficulty": {"type": ["number", "null"]}, + "translations": { + "type": "object", + "additionalProperties": {"type": "string"}, + "description": "Translation texts for this goal in json object" + }, + "difficulty": {"type": ["number", "null"]}, "categories": {"type": "array", "items": {"type": "string"}} } } \ No newline at end of file diff --git a/schema/types/Board.d.ts b/schema/types/Board.d.ts index e294129b..8688d47d 100644 --- a/schema/types/Board.d.ts +++ b/schema/types/Board.d.ts @@ -26,6 +26,12 @@ export interface Goal { id: string; goal: string; description: string | null; + /** + * Translation texts for this goal in json object + */ + translations: { + [k: string]: string; + }; difficulty?: number | null; categories?: string[]; } diff --git a/schema/types/Cell.d.ts b/schema/types/Cell.d.ts index ecafcd6d..ca14442a 100644 --- a/schema/types/Cell.d.ts +++ b/schema/types/Cell.d.ts @@ -19,6 +19,12 @@ export interface Goal { id: string; goal: string; description: string | null; + /** + * Translation texts for this goal in json object + */ + translations: { + [k: string]: string; + }; difficulty?: number | null; categories?: string[]; } diff --git a/schema/types/Game.d.ts b/schema/types/Game.d.ts index 69e8a48f..e9d8dcf6 100644 --- a/schema/types/Game.d.ts +++ b/schema/types/Game.d.ts @@ -15,6 +15,8 @@ export interface Game { favorited?: boolean; isMod?: boolean; enableSRLv5?: boolean; + defaultLanguage?: string; + translations?: string[]; racetimeBeta?: boolean; racetimeCategory?: string; racetimeGoal?: string; diff --git a/schema/types/Goal.d.ts b/schema/types/Goal.d.ts index a5c0f6d5..63eea51c 100644 --- a/schema/types/Goal.d.ts +++ b/schema/types/Goal.d.ts @@ -12,6 +12,12 @@ export interface Goal { id: string; goal: string; description: string | null; + /** + * Translation texts for this goal in json object + */ + translations: { + [k: string]: string; + }; difficulty?: number | null; categories?: string[]; } diff --git a/schema/types/ServerMessage.d.ts b/schema/types/ServerMessage.d.ts index 535bf91a..cc2cde11 100644 --- a/schema/types/ServerMessage.d.ts +++ b/schema/types/ServerMessage.d.ts @@ -77,6 +77,12 @@ export interface Goal { id: string; goal: string; description: string | null; + /** + * Translation texts for this goal in json object + */ + translations: { + [k: string]: string; + }; difficulty?: number | null; categories?: string[]; } diff --git a/web/src/actions/Game.ts b/web/src/actions/Game.ts index 87ae444d..8330cdf7 100644 --- a/web/src/actions/Game.ts +++ b/web/src/actions/Game.ts @@ -15,3 +15,17 @@ export async function deleteCategory(slug: string, categoryId: string) { status: res.status, }; } + +export async function deleteLanguage(slug: string, lang: string) { + const res = await serverFetch(`/api/games/${slug}/translations`, { + method: 'DELETE', + body: JSON.stringify({ translations: [lang] }), + }); + + revalidatePath(`/api/games/${slug}/translations`); + + return { + ok: res.ok, + status: res.status, + }; +} diff --git a/web/src/app/(main)/games/[slug]/_components/GameTabs.tsx b/web/src/app/(main)/games/[slug]/_components/GameTabs.tsx index 73d2ef2e..c0b86c27 100644 --- a/web/src/app/(main)/games/[slug]/_components/GameTabs.tsx +++ b/web/src/app/(main)/games/[slug]/_components/GameTabs.tsx @@ -50,6 +50,7 @@ export default function GameTabs({ tabs.push('variants'); if (canModerate) { tabs.push('categories'); + tabs.push('translations'); } if (isOwner) { diff --git a/web/src/app/(main)/games/[slug]/goals/_components/GoalEditor.tsx b/web/src/app/(main)/games/[slug]/goals/_components/GoalEditor.tsx index 944099ba..b5b5995d 100644 --- a/web/src/app/(main)/games/[slug]/goals/_components/GoalEditor.tsx +++ b/web/src/app/(main)/games/[slug]/goals/_components/GoalEditor.tsx @@ -7,8 +7,8 @@ import { Box, Button, Checkbox, - TextField, createFilterOptions, + TextField, } from '@mui/material'; import { Form, Formik, useField } from 'formik'; import { KeyedMutator } from 'swr'; @@ -86,6 +86,8 @@ function CategorySelect({ categories }: CategorySelectProps) { interface GoalEditorProps { slug: string; goal: Goal; + language: string; + isDefaultLanguage: boolean; isNew?: boolean; cancelNew?: () => void; mutateGoals: KeyedMutator; @@ -96,6 +98,8 @@ interface GoalEditorProps { export default function GoalEditor({ slug, goal, + language, + isDefaultLanguage, isNew, cancelNew, mutateGoals, @@ -105,7 +109,9 @@ export default function GoalEditor({ return ( diff --git a/web/src/app/(main)/games/[slug]/goals/_components/GoalManagement.tsx b/web/src/app/(main)/games/[slug]/goals/_components/GoalManagement.tsx index 30e62b59..f0bd52b2 100644 --- a/web/src/app/(main)/games/[slug]/goals/_components/GoalManagement.tsx +++ b/web/src/app/(main)/games/[slug]/goals/_components/GoalManagement.tsx @@ -20,6 +20,8 @@ export default function GoalManagement() { slug, canModerate, selectedGoal, + language, + defaultLanguage, goals, shownGoals, catList, @@ -132,6 +134,10 @@ export default function GoalManagement() { setNewGoal(false)} mutateGoals={mutateGoals} diff --git a/web/src/app/(main)/games/[slug]/translations/_components/languages.tsx b/web/src/app/(main)/games/[slug]/translations/_components/languages.tsx new file mode 100644 index 00000000..55d7ebd6 --- /dev/null +++ b/web/src/app/(main)/games/[slug]/translations/_components/languages.tsx @@ -0,0 +1,213 @@ +'use client'; +import { Box, IconButton, List, ListItem, TextField } from '@mui/material'; +import { useState } from 'react'; +import { Form, Formik } from 'formik'; +import { alertError } from '@/lib/Utils'; +import FormikTextField from '@/components/input/FormikTextField'; +import { Add, Check, Close, Delete, Edit } from '@mui/icons-material'; +import { deleteLanguage } from '@/actions/Game'; + +interface LanguagesProps { + slug: string; + languages: string[]; +} + +interface LanguageFormProps { + slug: string; + language: string; + onDelete: (lang: string) => void; +} + +function LanguageForm({ language, slug, onDelete }: LanguageFormProps) { + const [edit, setEdit] = useState(false); + if (language === 'New Language') { + setEdit(true); + } + return ( + theme.palette.divider, + }} + > + + { + const res = await fetch( + `/api/games/${slug}/translations`, + { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + translations: [ + { old: language, new: newVal.language }, + ], + }), + }, + ); + + if (!res.ok) { + return alertError('Failed to update language'); + } + setEdit(false); + }} + > + {({ resetForm }) => ( +
+ + + + {!edit && ( + setEdit(true)} + > + + + )} + {edit && language !== 'New Language' && ( + <> + { + resetForm(); + setEdit(false); + }} + color="error" + > + + + + + + + )} + { + const res = await deleteLanguage( + slug, + language, + ); + + if (!res.ok) { + alertError( + `Unable to delete language`, + ); + return; + } + onDelete(language); + }} + > + + + +
+ )} +
+
+
+ ); +} + +export default function Languages({ + slug, + languages: initialLanguages, +}: LanguagesProps) { + const [newLang, setNewLang] = useState(''); + const [languages, setLanguages] = useState(initialLanguages); + + const shownLanguages = languages.sort((a, b) => a.localeCompare(b)); + + const handleAddLang = async () => { + if (!newLang.trim()) return alertError('Please enter a new language'); + + const res = await fetch(`/api/games/${slug}/translations`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ translations: [newLang] }), + }); + + if (!res.ok) return alertError('Failed to add language'); + setLanguages((prev) => + [...prev, newLang.trim()].sort((a, b) => a.localeCompare(b)), + ); + setNewLang(''); + }; + + return ( + + + {shownLanguages.map((lang) => ( + { + setLanguages((prev) => + prev.filter((l) => lang !== l), + ); + }} + > + ))} + + + setNewLang(e.target.value)} + fullWidth + size="small" + /> + + + + + + ); +} diff --git a/web/src/app/(main)/games/[slug]/translations/page.tsx b/web/src/app/(main)/games/[slug]/translations/page.tsx new file mode 100644 index 00000000..81432231 --- /dev/null +++ b/web/src/app/(main)/games/[slug]/translations/page.tsx @@ -0,0 +1,29 @@ +import { getFullUrl } from '../../../../../lib/Utils'; +import { serverFetch } from '../../../../ServerUtils'; +import Languages from './_components/languages'; + +async function getLanguages(slug: string): Promise { + const res = await serverFetch( + getFullUrl(`/api/games/${slug}/translations`), + ); + if (!res.ok) { + return undefined; + } + return res.json(); +} + +interface Props { + params: Promise<{ slug: string }>; +} + +export default async function GamePermissions({ params }: Props) { + const { slug } = await params; + + const languages = await getLanguages(slug); + + if (!languages) { + return null; + } + + return ; +} diff --git a/web/src/components/board/Cell.tsx b/web/src/components/board/Cell.tsx index d17fcbcf..cc7aa9b5 100644 --- a/web/src/components/board/Cell.tsx +++ b/web/src/components/board/Cell.tsx @@ -58,7 +58,9 @@ export default function BoardCell({ showCounters, connectedPlayer, colorMap, - } = useRoomContext(); + language, + defaultLanguage, + } = useContext(RoomContext); const [wasRevealed, setWasRevealed] = useState(false); const [animating, setAnimating] = useState(false); @@ -188,7 +190,11 @@ export default function BoardCell({ }} > { - const { settings, setSettings } = useGoalManagerContext(); + const { + settings, + setSettings, + language, + setLanguage, + allLanguages, + defaultLanguage, + } = useGoalManagerContext(); + useGoalManagerContext(); return ( <> Goal Manager Settings - - setSettings(() => ({ - ...settings, - showDetails: e.target.checked, - })) - } - /> - } - label="Display additional information in goal list" - /> + + + setSettings({ + ...settings, + showDetails: e.target.checked, + }) + } + /> + } + label="Display additional information in goal list" + /> + + + Language + + + }> + Choose which language to edit for goals + + + ); diff --git a/web/src/components/room/PlayerInfo.tsx b/web/src/components/room/PlayerInfo.tsx index cfd8ba00..938123b6 100644 --- a/web/src/components/room/PlayerInfo.tsx +++ b/web/src/components/room/PlayerInfo.tsx @@ -1,11 +1,15 @@ import { - Card, - CardContent, Box, - Typography, Button, + Card, + CardContent, + FormControl, FormControlLabel, + InputLabel, + MenuItem, + Select, Switch, + Typography, } from '@mui/material'; import { ConnectionStatus, useRoomContext } from '../../context/RoomContext'; import ColorSelect from './ColorSelect'; @@ -21,6 +25,10 @@ export default function PlayerInfo() { showCounters, toggleCounters, connectedPlayer, + language, + defaultLanguage, + availableLanguages, + setLanguage, changeAuth, } = useRoomContext(); @@ -83,6 +91,36 @@ export default function PlayerInfo() { label="Show All Goal Details" /> + + + + Language + + + + ); diff --git a/web/src/context/GoalManagerContext.tsx b/web/src/context/GoalManagerContext.tsx index f524d5fc..680872f5 100644 --- a/web/src/context/GoalManagerContext.tsx +++ b/web/src/context/GoalManagerContext.tsx @@ -1,15 +1,15 @@ 'use client'; import { + createContext, Dispatch, ReactNode, SetStateAction, - createContext, useCallback, useContext, useEffect, useState, } from 'react'; -import { Goal } from '@playbingo/types'; +import { Game, Goal } from '@playbingo/types'; import { useApi } from '../lib/Hooks'; import { alertError } from '../lib/Utils'; import { KeyedMutator } from 'swr'; @@ -36,6 +36,9 @@ interface GoalManagerContext { canModerate: boolean; selectedGoal: Goal | undefined; goals: Goal[]; + language: string; + defaultLanguage?: string; + allLanguages?: string[]; shownGoals: Goal[]; catList: string[]; searchParams: SearchParams; @@ -44,6 +47,7 @@ interface GoalManagerContext { deleteGoal: (id: string) => void; setShownCats: (cats: string[]) => void; setSort: (sort: SortOptions) => void; + setLanguage: (language: string) => void; setReverse: Dispatch>; setSearch: (search: string) => void; setSettings: Dispatch>; @@ -57,6 +61,7 @@ const GoalManagerContext = createContext({ canModerate: false, selectedGoal: undefined, goals: [], + language: '', shownGoals: [], catList: [], searchParams: { sort: null, reverse: false, search: '', shownCats: [] }, @@ -65,6 +70,7 @@ const GoalManagerContext = createContext({ }, setSelectedGoal() {}, deleteGoal() {}, + setLanguage() {}, setShownCats() {}, setSort() {}, setReverse() {}, @@ -95,11 +101,16 @@ export function GoalManagerContextProvider({ mutate: mutateGoals, } = useApi(`/api/games/${slug}/goals`); + const { data: game, isLoading: gameLoading } = useApi( + `/api/games/${slug}`, + ); + // state // core const [selectedGoal, setSelectedGoal] = useState(); const [catList, setCatList] = useState([]); const [newGoal, setNewGoal] = useState(false); + const [language, setLanguage] = useState(''); // search params const [sort, setSort] = useState(SortOptions.DEFAULT); const [shownCats, setShownCats] = useState([]); @@ -143,11 +154,24 @@ export function GoalManagerContextProvider({ setCatList(cats); }, [goals]); + useEffect(() => { + if (game?.defaultLanguage && !language) { + setLanguage(game.defaultLanguage); + } + }, [game?.defaultLanguage, language]); + // base case if (!goals || goalsLoading) { return null; } + if (!game || gameLoading) { + return null; + } + + const allLanguages = game.translations || []; + const defaultLanguage = game.defaultLanguage; + // calculations const shownGoals = goals .filter((goal) => { @@ -191,6 +215,9 @@ export function GoalManagerContextProvider({ canModerate, selectedGoal, goals, + language, + defaultLanguage, + allLanguages, shownGoals, catList, searchParams: { sort, search, reverse, shownCats }, @@ -198,6 +225,7 @@ export function GoalManagerContextProvider({ setSelectedGoal, deleteGoal, setShownCats, + setLanguage, setSort, setSearch, setReverse, diff --git a/web/src/context/RoomContext.tsx b/web/src/context/RoomContext.tsx index 3192d62b..a8892600 100644 --- a/web/src/context/RoomContext.tsx +++ b/web/src/context/RoomContext.tsx @@ -3,16 +3,18 @@ import { Board, Cell, ChatMessage, + Game, Player, RoomAction, RoomData, ServerMessage, } from '@playbingo/types'; import { - ReactNode, createContext, + ReactNode, useCallback, useContext, + useEffect, useMemo, useState, useSyncExternalStore, @@ -27,6 +29,7 @@ import { } from '../lib/BoardStore'; import { alertError } from '../lib/Utils'; import { useUserContext } from './UserContext'; +import { useApi } from '@/lib/Hooks'; const websocketBase = (process.env.NEXT_PUBLIC_API_PATH ?? '').replace( 'http', @@ -58,6 +61,9 @@ interface RoomContext { starredGoals: number[]; showGoalDetails: boolean; showCounters: boolean; + language: string; + availableLanguages: string[]; + defaultLanguage: string; connectedPlayer?: Player; colorMap: { [k: string]: string }; connect: ( @@ -65,6 +71,7 @@ interface RoomContext { password: string, spectator: boolean, ) => Promise<{ success: boolean; message?: string }>; + setLanguage: (language: string) => void; sendChatMessage: (message: string) => void; markGoal: (row: number, col: number) => void; unmarkGoal: (row: number, col: number) => void; @@ -91,12 +98,16 @@ export const RoomContext = createContext({ nickname: '', players: [], starredGoals: [], + language: '', + defaultLanguage: '', + availableLanguages: [], showGoalDetails: false, showCounters: false, colorMap: {}, async connect() { return { success: false }; }, + setLanguage() {}, sendChatMessage() {}, markGoal() {}, unmarkGoal() {}, @@ -149,6 +160,7 @@ export function RoomContextProvider({ : ConnectionStatus.UNINITIALIZED, ); const [players, setPlayers] = useState([]); + const [language, setLanguage] = useState(''); const [connectedPlayer, setConnectedPlayer] = useState(); const colorMap = useMemo(() => { const colorMap: { [k: string]: string } = {}; @@ -207,6 +219,10 @@ export function RoomContextProvider({ setRoomData(roomData); }, []); + const { data: gameData, isLoading: loadingGames } = useApi( + `/api/games/${roomData.gameSlug}`, + ); + // websocket const { sendJsonMessage } = useWebSocket( `${websocketBase}/socket/${roomData.slug}`, @@ -501,12 +517,28 @@ export function RoomContextProvider({ [sendJsonMessage, authToken], ); + useEffect(() => { + if (gameData?.defaultLanguage && !language) { + setLanguage(gameData.defaultLanguage); + } + }, [gameData?.defaultLanguage, language]); + + if (!gameData || loadingGames) { + return null; + } + + const availableLanguages = gameData?.translations || []; + const defaultLanguage = gameData?.defaultLanguage || ''; + return (