diff --git a/.github/file-filters.yml b/.github/file-filters.yml index 7dc1a3cba..e54a3a6af 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -36,6 +36,12 @@ backend: &backend config: &config - 'config-schema/**/*' +shared: &shared + - 'shared/**/*' + +core: &core + - 'core/**/*' + database: &database - 'database/**/*' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4ff2632e4..1416b2e04 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,6 +7,8 @@ jobs: runs-on: ubuntu-latest outputs: config-schema: ${{ steps.config-schema.outputs.success }} + shared: ${{ steps.shared.outputs.success }} + core: ${{ steps.core.outputs.success }} backend: ${{ steps.backend.outputs.success }} database: ${{ steps.database.outputs.success }} dht-node: ${{ steps.dht-node.outputs.success }} @@ -17,7 +19,7 @@ jobs: - name: Setup Biome uses: biomejs/setup-biome@v2 with: - version: latest + version: 2.0.0 - name: Lint - Config-Schema id: config-schema run: | @@ -25,6 +27,18 @@ jobs: biome ci . echo $? echo "success=$([ $? -eq 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT + - name: Lint - Shared + id: shared + run: | + cd ./shared + biome ci . + echo "success=$([ $? -eq 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT + - name: Lint - Core + id: core + run: | + cd ./core + biome ci . + echo "success=$([ $? -eq 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT - name: Lint - Backend id: backend run: | @@ -58,6 +72,22 @@ jobs: - name: Check result from previous step run: if [ "${{ needs.lint.outputs.config-schema }}" != "true" ]; then exit 1; fi + lint_shared: + name: Lint - Shared + needs: lint + runs-on: ubuntu-latest + steps: + - name: Check result from previous step + run: if [ "${{ needs.lint.outputs.shared }}" != "true" ]; then exit 1; fi + + lint_core: + name: Lint - Core + needs: lint + runs-on: ubuntu-latest + steps: + - name: Check result from previous step + run: if [ "${{ needs.lint.outputs.core }}" != "true" ]; then exit 1; fi + lint_backend: name: Lint - Backend needs: lint diff --git a/.github/workflows/lint_pr.yml b/.github/workflows/lint_pr.yml index d5b4cf72f..7ec370242 100644 --- a/.github/workflows/lint_pr.yml +++ b/.github/workflows/lint_pr.yml @@ -35,6 +35,7 @@ jobs: workflow docker other + shared # Configure that a scope must always be provided. requireScope: true # Configure which scopes (newline delimited) are disallowed in PR diff --git a/.github/workflows/test_backend.yml b/.github/workflows/test_backend.yml index ca3ce0e69..efb59ba37 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -9,6 +9,8 @@ jobs: outputs: backend: ${{ steps.changes.outputs.backend }} config: ${{ steps.changes.outputs.config }} + core: ${{ steps.changes.outputs.core }} + shared: ${{ steps.changes.outputs.shared }} database: ${{ steps.changes.outputs.database }} docker-compose: ${{ steps.changes.outputs.docker-compose }} mariadb: ${{ steps.changes.outputs.mariadb }} @@ -24,7 +26,7 @@ jobs: list-files: shell build_test: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.config == 'true' || needs.files-changed.outputs.database == 'true' + if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.config == 'true' || needs.files-changed.outputs.core == 'true' || needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.shared == 'true' || needs.files-changed.outputs.docker-compose == 'true' name: Docker Build Test - Backend needs: files-changed runs-on: ubuntu-latest @@ -36,7 +38,7 @@ jobs: run: docker build -f ./backend/Dockerfile --target production -t "gradido/backend:production" . unit_test: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.mariadb == 'true' || needs.files-changed.outputs.config == 'true' + if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.mariadb == 'true' || needs.files-changed.outputs.config == 'true' || needs.files-changed.outputs.core == 'true' || needs.files-changed.outputs.shared == 'true' name: Unit tests - Backend needs: files-changed runs-on: ubuntu-latest @@ -57,14 +59,14 @@ jobs: - name: install dependencies run: | - bun install --filter backend --frozen-lockfile + bun install --filter backend --filter core --frozen-lockfile bun install --global --no-save turbo@^2 - name: Backend | Unit tests run: turbo backend#test typecheck: - if: needs.files-changed.outputs.backend == 'true' + if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.core == 'true' || needs.files-changed.outputs.shared == 'true' || needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.config == 'true' name: Typecheck - Backend needs: files-changed runs-on: ubuntu-latest diff --git a/.github/workflows/test_config.yml b/.github/workflows/test_config.yml index 5ba0832c6..8643bf959 100644 --- a/.github/workflows/test_config.yml +++ b/.github/workflows/test_config.yml @@ -21,7 +21,7 @@ jobs: list-files: shell build: - name: typecheck - Config-Schema + name: Unit Tests, typecheck - Config-Schema if: needs.files-changed.outputs.config == 'true' || needs.files-changed.outputs.docker-compose == 'true' needs: files-changed runs-on: ubuntu-latest @@ -38,3 +38,6 @@ jobs: - name: typecheck run: cd config-schema && yarn typecheck + - name: unit tests + run: cd config-schema && yarn test + diff --git a/.github/workflows/test_core.yml b/.github/workflows/test_core.yml new file mode 100644 index 000000000..b6e95f60c --- /dev/null +++ b/.github/workflows/test_core.yml @@ -0,0 +1,43 @@ +name: Gradido Core Test CI + +on: push + +jobs: + files-changed: + name: Detect File Changes - Core + runs-on: ubuntu-latest + outputs: + core: ${{ steps.changes.outputs.core }} + database: ${{ steps.changes.outputs.database }} + shared: ${{ steps.changes.outputs.shared }} + steps: + - uses: actions/checkout@v3.3.0 + + - name: Check for core file changes + uses: dorny/paths-filter@v2.11.1 + id: changes + with: + token: ${{ github.token }} + filters: .github/file-filters.yml + list-files: shell + + build: + name: Unit Tests, typecheck - Core + if: needs.files-changed.outputs.core == 'true' || needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.shared == 'true' + needs: files-changed + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: install bun + uses: oven-sh/setup-bun@v2 + + - name: install dependencies + run: | + bun install --filter core --frozen-lockfile + bun install --global turbo@^2 + + - name: typecheck && unit test + run: turbo core#test core#typecheck + diff --git a/.github/workflows/test_database.yml b/.github/workflows/test_database.yml index a234c7eec..2c1a7ef94 100644 --- a/.github/workflows/test_database.yml +++ b/.github/workflows/test_database.yml @@ -8,6 +8,7 @@ jobs: runs-on: ubuntu-latest outputs: database: ${{ steps.changes.outputs.database }} + shared: ${{ steps.changes.outputs.shared }} docker-compose: ${{ steps.changes.outputs.docker-compose }} mariadb: ${{ steps.changes.outputs.mariadb }} steps: @@ -22,7 +23,7 @@ jobs: list-files: shell build: - if: needs.files-changed.outputs.database == 'true' + if: needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.shared == 'true' name: Docker Build Test - Database up needs: files-changed runs-on: ubuntu-latest @@ -34,7 +35,7 @@ jobs: run: docker build --target build -t "gradido/database:build" -f database/Dockerfile . database_migration_test: - if: needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.mariadb == 'true' + if: needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.mariadb == 'true' || needs.files-changed.outputs.shared == 'true' name: Database Migration Test - Up + Reset needs: files-changed runs-on: ubuntu-latest @@ -58,11 +59,11 @@ jobs: bun install --filter database --frozen-lockfile bun install --global --no-save turbo@^2 - - name: Database | up - run: turbo up + - name: Database | up + test + run: turbo database#test - name: Database | reset - run: turbo reset + run: turbo database#reset lint: if: needs.files-changed.outputs.database == 'true' diff --git a/.github/workflows/test_shared.yml b/.github/workflows/test_shared.yml new file mode 100644 index 000000000..6e377dbbf --- /dev/null +++ b/.github/workflows/test_shared.yml @@ -0,0 +1,42 @@ +name: Gradido Shared Test CI + +on: push + +jobs: + files-changed: + name: Detect File Changes - Shared + runs-on: ubuntu-latest + outputs: + shared: ${{ steps.changes.outputs.shared }} + steps: + - uses: actions/checkout@v3.3.0 + + - name: Check for shared file changes + uses: dorny/paths-filter@v2.11.1 + id: changes + with: + token: ${{ github.token }} + filters: .github/file-filters.yml + list-files: shell + + build: + name: Unit Tests, typecheck - Shared + if: needs.files-changed.outputs.shared == 'true' + needs: files-changed + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: install bun + uses: oven-sh/setup-bun@v2 + + - name: install dependencies + run: bun install --filter shared --frozen-lockfile + + - name: typecheck + run: cd shared && yarn typecheck + + - name: unit tests + run: cd shared && yarn test + diff --git a/.gitignore b/.gitignore index 3d7f34078..d82288fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,10 @@ *.bak .turbo vite.config.mjs.timestamp-* +log4js-config*.json /node_modules/* +node_modules +build messages.pot nbproject .metadata diff --git a/.vscode/launch.json b/.vscode/launch.json index faf3cff12..30fa8f6ed 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,11 +5,120 @@ "version": "0.2.0", "configurations": [ { - "type": "chrome", + "type": "node", "request": "launch", - "name": "Launch Chrome", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}/frontend", + "name": "Database Debug Tests", + "stopOnEntry": true, + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "run", + "test" + ], + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "cwd": "${workspaceFolder}/database" + }, + { + "type": "node", + "request": "launch", + "name": "DHT-Node Debug Tests", + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "run", + "test:debug" + ], + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "cwd": "${workspaceFolder}/dht-node" + }, + { + "type": "node", + "request": "launch", + "name": "DHT-Node Debug", + "stopOnEntry": true, + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "run", + "dev" + ], + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "openOnSessionStart", + "cwd": "${workspaceFolder}/dht-node" + }, + { + "type": "node", + "request": "launch", + "name": "Federation Debug Tests", + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "run", + "test:debug" + ], + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "cwd": "${workspaceFolder}/federation" + }, + { + "type": "node", + "request": "launch", + "name": "Federation Debug", + "stopOnEntry": true, + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "run", + "dev" + ], + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "openOnSessionStart", + "cwd": "${workspaceFolder}/federation" + }, + { + "type": "node", + "request": "launch", + "name": "Backend Debug", + "stopOnEntry": true, + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "run", + "dev" + ], + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "openOnSessionStart", + "cwd": "${workspaceFolder}/backend" + }, + { + "type": "node", + "request": "launch", + "name": "Shared Debug Test", + "runtimeExecutable": "bun", + "runtimeArgs": [ + "run", + "test:debug" + ], + "skipFiles": [ + "/**" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "openOnSessionStart", + "cwd": "${workspaceFolder}/shared" } ] } \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 4c1ceb36e..4ab2f1883 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -113,8 +113,6 @@ COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/backend/build/worker.js ./wo # add node_modules from production_node_modules COPY --chown=app:app --from=production-node-modules ${DOCKER_WORKDIR}/node_modules ./node_modules -# Copy log4js-config.json to provide log configuration -COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/backend/log4js-config.json ./log4js-config.json # Copy locales COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/backend/locales ./locales diff --git a/backend/esbuild.config.ts b/backend/esbuild.config.ts index 87efef48e..9cdd51cc1 100644 --- a/backend/esbuild.config.ts +++ b/backend/esbuild.config.ts @@ -13,4 +13,5 @@ build({ external: ['sodium-native', 'email-templates'], plugins: [esbuildDecorators()], minify: true, + sourcemap: true, }) diff --git a/backend/jest.config.js b/backend/jest.config.js index 87f32599d..ddf94f977 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -2,7 +2,7 @@ module.exports = { verbose: true, preset: 'ts-jest', - collectCoverage: true, + collectCoverage: false, collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { diff --git a/backend/log4js-config.json b/backend/log4js-config.json deleted file mode 100644 index 0807e6a12..000000000 --- a/backend/log4js-config.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "appenders": - { - "access": - { - "type": "dateFile", - "filename": "../logs/backend/access.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "apollo": - { - "type": "dateFile", - "filename": "../logs/backend/apollo.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "backend": - { - "type": "dateFile", - "filename": "../logs/backend/backend.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "klicktipp": - { - "type": "dateFile", - "filename": "../logs/backend/klicktipp.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "gms": - { - "type": "dateFile", - "filename": "../logs/backend/gms.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "errorFile": - { - "type": "dateFile", - "filename": "../logs/backend/errors.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m %s" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "errors": - { - "type": "logLevelFilter", - "level": "error", - "appender": "errorFile" - }, - "out": - { - "type": "stdout", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - } - }, - "apolloOut": - { - "type": "stdout", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - } - } - }, - "categories": - { - "default": - { - "appenders": - [ - "out", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "apollo": - { - "appenders": - [ - "apollo", - "apolloOut", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "backend": - { - "appenders": - [ - "backend", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "klicktipp": - { - "appenders": - [ - "klicktipp", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "gms": - { - "appenders": - [ - "gms", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "http": - { - "appenders": - [ - "access" - ], - "level": "info" - } - } -} diff --git a/backend/package.json b/backend/package.json index 8382fc9a1..cd06351aa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,9 +9,9 @@ "main": "src/index.ts", "scripts": { "build": "ts-node ./esbuild.config.ts && mkdirp build/templates/ && ncp src/emails/templates build/templates && mkdirp locales/ && ncp src/locales locales", - "clean": "tsc --build --clean", "dev": "cross-env TZ=UTC nodemon -w src --ext ts,pug,json,css -r tsconfig-paths/register src/index.ts", "test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_backend jest --runInBand --forceExit --detectOpenHandles", + "test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_backend jest --coverage --runInBand --forceExit --detectOpenHandles", "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts", "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/executeKlicktipp.ts", "gmsusers": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/apis/gms/ExportUsers.ts", @@ -21,7 +21,7 @@ "lint:fix:unsafe": "biome check --fix --unsafe", "locales": "scripts/sort.sh", "locales:fix": "scripts/sort.sh --fix", - "start": "cross-env TZ=UTC NODE_ENV=production node build/index.js", + "start": "cross-env TZ=UTC node build/index.js", "typecheck": "tsc --noEmit" }, "nodemonConfig": { @@ -36,7 +36,7 @@ }, "devDependencies": { "@anatine/esbuild-decorators": "^0.2.19", - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", "@swc/cli": "^0.7.3", "@swc/core": "^1.11.24", "@swc/helpers": "^0.5.17", @@ -49,6 +49,7 @@ "@types/node": "^17.0.21", "@types/nodemailer": "^6.4.4", "@types/sodium-native": "^2.3.5", + "@types/source-map-support": "^0.5.10", "@types/uuid": "^8.3.4", "apollo-server-express": "^2.25.2", "apollo-server-testing": "^2.25.2", @@ -56,6 +57,7 @@ "axios": "^0.21.1", "class-validator": "^0.13.1", "config-schema": "*", + "core": "*", "cors": "^2.8.5", "database": "*", "decimal.js-light": "^2.5.1", @@ -64,7 +66,7 @@ "express": "^4.17.21", "express-slow-down": "^2.0.1", "faker": "^5.5.3", - "graphql": "^15.10.1", + "graphql": "15.10.1", "graphql-parse-resolve-info": "^4.13.1", "graphql-request": "5.0.0", "graphql-tag": "^2.12.6", @@ -87,12 +89,14 @@ "random-bigint": "^0.0.1", "reflect-metadata": "^0.1.13", "regenerator-runtime": "^0.14.1", - "ts-jest": "27.0.5", + "shared": "*", + "source-map-support": "^0.5.21", + "ts-jest": "29.4.0", "ts-node": "^10.9.2", "tsconfig-paths": "^4.1.1", "type-graphql": "^1.1.1", "typed-rest-client": "^1.8.11", - "typeorm": "^0.3.22", + "typeorm": "^0.3.25", "typescript": "^4.9.5", "uuid": "^8.3.2", "workerpool": "^9.2.0", diff --git a/backend/src/apis/HttpRequest.ts b/backend/src/apis/HttpRequest.ts index ef6f540b7..9a775ee28 100644 --- a/backend/src/apis/HttpRequest.ts +++ b/backend/src/apis/HttpRequest.ts @@ -1,7 +1,10 @@ import axios from 'axios' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.HttpRequest`) import { httpAgent, httpsAgent } from './ConnectionAgents' diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index cb665ea7f..5a33df9ae 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -1,9 +1,10 @@ +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { CONFIG } from '@/config' -import { backendLogger as logger } from '@/server/logger' - import KlicktippConnector from 'klicktipp-api' +import { getLogger } from 'log4js' const klicktippConnector = new KlicktippConnector() +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.KlicktippController`) export const subscribe = async ( email: string, diff --git a/backend/src/apis/dltConnector/DltConnectorClient.test.ts b/backend/src/apis/dltConnector/DltConnectorClient.test.ts index 0367c6350..00b15348d 100644 --- a/backend/src/apis/dltConnector/DltConnectorClient.test.ts +++ b/backend/src/apis/dltConnector/DltConnectorClient.test.ts @@ -6,7 +6,6 @@ import { cleanDB, testEnvironment } from '@test/helpers' import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { DltConnectorClient } from './DltConnectorClient' @@ -76,7 +75,7 @@ describe.skip('transmitTransaction, without db connection', () => { describe('transmitTransaction', () => { beforeAll(async () => { - testEnv = await testEnvironment(logger) + testEnv = await testEnvironment() con = testEnv.con await cleanDB() }) diff --git a/backend/src/apis/dltConnector/DltConnectorClient.ts b/backend/src/apis/dltConnector/DltConnectorClient.ts index 2bebc84c0..6f5b437de 100644 --- a/backend/src/apis/dltConnector/DltConnectorClient.ts +++ b/backend/src/apis/dltConnector/DltConnectorClient.ts @@ -2,13 +2,16 @@ import { Transaction as DbTransaction } from 'database' import { GraphQLClient, gql } from 'graphql-request' import { CONFIG } from '@/config' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' import { TransactionResult } from './model/TransactionResult' import { UserIdentifier } from './model/UserIdentifier' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.dltConnector`) + const sendTransaction = gql` mutation ($input: TransactionInput!) { sendTransaction(data: $input) { diff --git a/backend/src/apis/gms/ExportUsers.ts b/backend/src/apis/gms/ExportUsers.ts index 981f9c90e..03cc92fa4 100644 --- a/backend/src/apis/gms/ExportUsers.ts +++ b/backend/src/apis/gms/ExportUsers.ts @@ -1,25 +1,32 @@ import { User as DbUser } from 'database' // import { createTestClient } from 'apollo-server-testing' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' // import { createGmsUser } from '@/apis/gms/GmsClient' // import { GmsUser } from '@/apis/gms/model/GmsUser' import { CONFIG } from '@/config' -import { getHomeCommunity } from '@/graphql/resolver/util/communities' import { sendUserToGms } from '@/graphql/resolver/util/sendUserToGms' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' -import { AppDatabase } from 'database' +import { initLogging } from '@/server/logger' +import { AppDatabase, getHomeCommunity } from 'database' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.gms.ExportUsers`) CONFIG.EMAIL = false // use force to copy over all user even if gmsRegistered is set to true const forceMode = process.argv.includes('--force') async function main() { + initLogging() // open mysql connection const con = AppDatabase.getInstance() await con.init() const homeCom = await getHomeCommunity() + if (!homeCom) { + throw new LogError('HomeCommunity not found') + } if (homeCom.gmsApiKey === null) { throw new LogError('HomeCommunity needs GMS-ApiKey to publish user data to GMS.') } @@ -70,7 +77,6 @@ async function main() { } main().catch((e) => { - // biome-ignore lint/suspicious/noConsole: logger isn't used here - console.error(e) + logger.error(e) process.exit(1) }) diff --git a/backend/src/apis/gms/GmsClient.ts b/backend/src/apis/gms/GmsClient.ts index 537fe36f2..bb4fce2e7 100644 --- a/backend/src/apis/gms/GmsClient.ts +++ b/backend/src/apis/gms/GmsClient.ts @@ -1,13 +1,16 @@ import axios from 'axios' import { httpAgent, httpsAgent } from '@/apis/ConnectionAgents' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { getLogger } from 'log4js' import { GmsUser } from './model/GmsUser' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.gms.GmsClient`) + /* export async function communityList(): Promise { const baseUrl = ensureUrlEndsWithSlash(CONFIG.GMS_URL) diff --git a/backend/src/apis/humhub/ExportUsers.ts b/backend/src/apis/humhub/ExportUsers.ts index dc1f4fb5d..99ae859eb 100644 --- a/backend/src/apis/humhub/ExportUsers.ts +++ b/backend/src/apis/humhub/ExportUsers.ts @@ -1,9 +1,9 @@ import { AppDatabase, User } from 'database' import { IsNull, Not } from 'typeorm' -import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' - +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { initLogging } from '@/server/logger' +import { getLogger } from 'log4js' import { HumHubClient } from './HumHubClient' import { GetUser } from './model/GetUser' import { UsersResponse } from './model/UsersResponse' @@ -11,6 +11,7 @@ import { ExecutedHumhubAction, syncUser } from './syncUser' const USER_BULK_SIZE = 20 const HUMHUB_BULK_SIZE = 50 +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.humhub.ExportUsers`) function getUsersPage(page: number, limit: number): Promise<[User[], number]> { return User.findAndCount({ @@ -34,7 +35,7 @@ async function loadUsersFromHumHub(client: HumHubClient): Promise { - // biome-ignore lint/suspicious/noConsole: logger isn't used here - console.error(e) + logger.error(e) process.exit(1) }) diff --git a/backend/src/apis/humhub/HumHubClient.ts b/backend/src/apis/humhub/HumHubClient.ts index daa19b5b8..0cdc683c5 100644 --- a/backend/src/apis/humhub/HumHubClient.ts +++ b/backend/src/apis/humhub/HumHubClient.ts @@ -4,8 +4,9 @@ import { IRequestOptions, IRestResponse, RestClient } from 'typed-rest-client' import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' import { PostUserLoggingView } from './logging/PostUserLogging.view' import { GetUser } from './model/GetUser' import { PostUser } from './model/PostUser' @@ -13,6 +14,8 @@ import { Space } from './model/Space' import { SpacesResponse } from './model/SpacesResponse' import { UsersResponse } from './model/UsersResponse' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.humhub.HumHubClient`) + /** * HumHubClient as singleton class */ diff --git a/backend/src/apis/humhub/syncUser.ts b/backend/src/apis/humhub/syncUser.ts index 1e62871be..22ecde87c 100644 --- a/backend/src/apis/humhub/syncUser.ts +++ b/backend/src/apis/humhub/syncUser.ts @@ -1,13 +1,16 @@ import { User } from 'database' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' import { HumHubClient } from './HumHubClient' import { isHumhubUserIdenticalToDbUser } from './compareHumhubUserDbUser' import { GetUser } from './model/GetUser' import { PostUser } from './model/PostUser' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.humhub.syncUser`) + export enum ExecutedHumhubAction { UPDATE, CREATE, diff --git a/backend/src/apis/openai/OpenaiClient.ts b/backend/src/apis/openai/OpenaiClient.ts index b2a859581..35744a7dd 100644 --- a/backend/src/apis/openai/OpenaiClient.ts +++ b/backend/src/apis/openai/OpenaiClient.ts @@ -4,10 +4,14 @@ import { Message } from 'openai/resources/beta/threads/messages' import { httpsAgent } from '@/apis/ConnectionAgents' import { CONFIG } from '@/config' -import { backendLogger as logger } from '@/server/logger' import { Message as MessageModel } from './model/Message' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.openai.OpenaiClient`) + /** * The `OpenaiClient` class is a singleton that provides an interface to interact with the OpenAI API. * It ensures that only one instance of the client is created and used throughout the application. diff --git a/backend/src/auth/jwt/JWT.ts b/backend/src/auth/jwt/JWT.ts index ff5db0542..40fcd147e 100644 --- a/backend/src/auth/jwt/JWT.ts +++ b/backend/src/auth/jwt/JWT.ts @@ -1,8 +1,10 @@ - import { generateKeyPair, exportSPKI, exportPKCS8, SignJWT, decodeJwt, importPKCS8, importSPKI, jwtVerify, CompactEncrypt, compactDecrypt } from 'jose' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT`) import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { JwtPayloadType } from './payloadtypes/JwtPayloadType' import { EncryptedJWEJwtPayloadType } from './payloadtypes/EncryptedJWEJwtPayloadType' diff --git a/backend/src/config/const.ts b/backend/src/config/const.ts new file mode 100644 index 000000000..68bd124a8 --- /dev/null +++ b/backend/src/config/const.ts @@ -0,0 +1 @@ +export const LOG4JS_BASE_CATEGORY_NAME = 'backend' diff --git a/backend/src/config/index.test.ts b/backend/src/config/index.test.ts deleted file mode 100644 index 24908513a..000000000 --- a/backend/src/config/index.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CONFIG } from './index' - -describe('config/index', () => { - describe('decay start block', () => { - it('has the correct date set', () => { - expect(CONFIG.DECAY_START_TIME).toEqual(new Date('2021-05-13 17:46:31-0000')) - }) - }) -}) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index f29f5ed4b..9d61c2da4 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,21 +1,18 @@ // ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env) -import { validate } from 'config-schema' -import { Decimal } from 'decimal.js-light' +import { LogLevel, validate } from 'config-schema' import dotenv from 'dotenv' import { schema } from './schema' dotenv.config() -Decimal.set({ - precision: 25, - rounding: Decimal.ROUND_HALF_UP, -}) - -const constants = { - DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 - LOG4JS_CONFIG: 'log4js-config.json', +const logging = { + LOG4JS_CONFIG: process.env.LOG4JS_CONFIG ?? 'log4js-config.json', + // default log level on production should be info + // log level for default log4js-config.json, don't change existing log4js-config.json + LOG_LEVEL: (process.env.LOG_LEVEL ?? 'info') as LogLevel, + LOG_FILES_BASE_PATH: process.env.LOG_FILES_BASE_PATH ?? '../logs/backend', } const server = { @@ -27,8 +24,6 @@ const server = { GDT_ACTIVE: process.env.GDT_ACTIVE === 'true' || false, GDT_API_URL: process.env.GDT_API_URL ?? 'https://gdt.gradido.net', PRODUCTION: process.env.NODE_ENV === 'production' || false, - // default log level on production should be info - LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', } const klicktipp = { @@ -143,7 +138,7 @@ const openai = { } export const CONFIG = { - ...constants, + ...logging, ...server, ...klicktipp, ...dltConnector, diff --git a/backend/src/config/schema.ts b/backend/src/config/schema.ts index 61c1026b0..f305a7488 100644 --- a/backend/src/config/schema.ts +++ b/backend/src/config/schema.ts @@ -13,6 +13,7 @@ import { LOG4JS_CONFIG, LOGIN_APP_SECRET, LOGIN_SERVER_KEY, + LOG_FILES_BASE_PATH, LOG_LEVEL, NODE_ENV, OPENAI_ACTIVE, @@ -32,6 +33,7 @@ export const schema = Joi.object({ GRAPHIQL, HUMHUB_ACTIVE, HUMHUB_API_URL, + LOG_FILES_BASE_PATH, LOG4JS_CONFIG, LOGIN_APP_SECRET, LOGIN_SERVER_KEY, diff --git a/backend/src/emails/sendEmailTranslated.test.ts b/backend/src/emails/sendEmailTranslated.test.ts index 917d80ea6..103296080 100644 --- a/backend/src/emails/sendEmailTranslated.test.ts +++ b/backend/src/emails/sendEmailTranslated.test.ts @@ -1,11 +1,15 @@ import { createTransport } from 'nodemailer' -import { i18n, logger } from '@test/testSetup' +import { i18n } from '@test/testSetup' import { CONFIG } from '@/config' +import { getLogger } from 'config-schema/test/testSetup' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { sendEmailTranslated } from './sendEmailTranslated' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.emails.sendEmailTranslated`) + const testMailServerHost = 'localhost' const testMailServerPort = 1025 diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index ae52b3975..5b95cd7ad 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -5,7 +5,10 @@ import i18n from 'i18n' import { createTransport } from 'nodemailer' import { CONFIG } from '@/config' -import { backendLogger as logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.emails.sendEmailTranslated`) export const sendEmailTranslated = async ({ receiver, @@ -31,8 +34,8 @@ export const sendEmailTranslated = async ({ i18n.setLocale('en') // for logging logger.info( - `send Email: language=${locals.locale as string} to=${receiver.to}` + - (receiver.cc ? `, cc=${receiver.cc}` : '') + + `send Email: language=${locals.locale as string} to=${receiver.to.substring(0, 3)}...` + + (receiver.cc ? `, cc=${receiver.cc.substring(0, 3)}...` : '') + `, subject=${i18n.__('emails.' + template + '.subject')}`, ) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 5c8bde6f2..74eb940ed 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -3,7 +3,8 @@ import { Decimal } from 'decimal.js-light' import { DataSource } from 'typeorm' import { testEnvironment } from '@test/helpers' -import { i18n as localization, logger } from '@test/testSetup' +import { i18n as localization } from '@test/testSetup' +import { getLogger } from 'config-schema/test/testSetup' import { CONFIG } from '@/config' @@ -53,7 +54,7 @@ let testEnv: { } beforeAll(async () => { - testEnv = await testEnvironment(logger, localization) + testEnv = await testEnvironment(getLogger('apollo'), localization) con = testEnv.con }) diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 953f20976..5e38ebf6d 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -4,13 +4,16 @@ import { validate as validateUUID, version as versionUUID } from 'uuid' import { CONFIG } from '@/config' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient' -import { backendLogger as logger } from '@/server/logger' import { ensureUrlEndsWithSlash } from '@/util/utilities' import { encryptAndSign } from '@/auth/jwt/JWT' import { OpenConnectionJwtPayloadType } from '@/auth/jwt/payloadtypes/OpenConnectionJwtPayloadType' +import { getLogger } from 'log4js' import { OpenConnectionArgs } from './client/1_0/model/OpenConnectionArgs' import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities`) export async function startCommunityAuthentication( foreignFedCom: DbFederatedCommunity, diff --git a/backend/src/federation/client/1_0/AuthenticationClient.ts b/backend/src/federation/client/1_0/AuthenticationClient.ts index 264afe3a0..0d478a521 100644 --- a/backend/src/federation/client/1_0/AuthenticationClient.ts +++ b/backend/src/federation/client/1_0/AuthenticationClient.ts @@ -1,12 +1,15 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { GraphQLClient } from 'graphql-request' -import { backendLogger as logger } from '@/server/logger' import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' import { OpenConnectionArgs } from './model/OpenConnectionArgs' import { openConnection } from './query/openConnection' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.AuthenticationClient`) + export class AuthenticationClient { dbCom: DbFederatedCommunity endpoint: string @@ -25,25 +28,19 @@ export class AuthenticationClient { } async openConnection(args: OpenConnectionArgs): Promise { - logger.debug(`Authentication: openConnection at ${this.endpoint} for args:`, args) + logger.debug(`openConnection at ${this.endpoint} for args:`, args) try { const { data } = await this.client.rawRequest<{ openConnection: boolean }>(openConnection, { args, }) if (!data?.openConnection) { - logger.warn( - 'Authentication: openConnection without response data from endpoint', - this.endpoint, - ) + logger.warn('openConnection without response data from endpoint', this.endpoint) return false } - logger.debug( - 'Authentication: openConnection successfully started with endpoint', - this.endpoint, - ) + logger.debug('openConnection successfully started with endpoint', this.endpoint) return true } catch (err) { - logger.error('Authentication: error on openConnection: ', err) + logger.error('error on openConnection: ', err) } } } diff --git a/backend/src/federation/client/1_0/FederationClient.ts b/backend/src/federation/client/1_0/FederationClient.ts index b83da8a8b..df1140f9c 100644 --- a/backend/src/federation/client/1_0/FederationClient.ts +++ b/backend/src/federation/client/1_0/FederationClient.ts @@ -1,15 +1,18 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { GraphQLClient } from 'graphql-request' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { getPublicCommunityInfo } from '@/federation/client/1_0/query/getPublicCommunityInfo' import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey' -import { backendLogger as logger } from '@/server/logger' import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { getLogger } from 'log4js' import { PublicCommunityInfoLoggingView } from './logging/PublicCommunityInfoLogging.view' import { GetPublicKeyResult } from './model/GetPublicKeyResult' import { PublicCommunityInfo } from './model/PublicCommunityInfo' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.FederationClient`) + export class FederationClient { dbCom: DbFederatedCommunity endpoint: string @@ -32,25 +35,25 @@ export class FederationClient { } getPublicKey = async (): Promise => { - logger.debug('Federation: getPublicKey from endpoint', this.endpoint) + logger.debug('getPublicKey from endpoint', this.endpoint) try { const { data } = await this.client.rawRequest<{ getPublicKey: GetPublicKeyResult }>( getPublicKey, {}, ) if (!data?.getPublicKey?.publicKey) { - logger.warn('Federation: getPublicKey without response data from endpoint', this.endpoint) + logger.warn('getPublicKey without response data from endpoint', this.endpoint) return } logger.debug( - 'Federation: getPublicKey successful from endpoint', + 'getPublicKey successful from endpoint', this.endpoint, data.getPublicKey.publicKey, ) return data.getPublicKey.publicKey } catch (err) { const errorString = JSON.stringify(err) - logger.warn('Federation: getPublicKey failed for endpoint', { + logger.warn('getPublicKey failed for endpoint', { endpoint: this.endpoint, err: errorString.length <= 200 ? errorString : errorString.substring(0, 200) + '...', }) @@ -58,20 +61,17 @@ export class FederationClient { } getPublicCommunityInfo = async (): Promise => { - logger.debug(`Federation: getPublicCommunityInfo with endpoint='${this.endpoint}'...`) + logger.debug(`getPublicCommunityInfo with endpoint='${this.endpoint}'...`) try { const { data } = await this.client.rawRequest<{ getPublicCommunityInfo: PublicCommunityInfo }>(getPublicCommunityInfo, {}) if (!data?.getPublicCommunityInfo?.name) { - logger.warn( - 'Federation: getPublicCommunityInfo without response data from endpoint', - this.endpoint, - ) + logger.warn('getPublicCommunityInfo without response data from endpoint', this.endpoint) return } - logger.debug(`Federation: getPublicCommunityInfo successful from endpoint=${this.endpoint}`) + logger.debug(`getPublicCommunityInfo successful from endpoint=${this.endpoint}`) logger.debug( `publicCommunityInfo:`, new PublicCommunityInfoLoggingView(data.getPublicCommunityInfo), @@ -80,7 +80,7 @@ export class FederationClient { } catch (err) { logger.warn(' err', err) const errorString = JSON.stringify(err) - logger.warn('Federation: getPublicCommunityInfo failed for endpoint', { + logger.warn('getPublicCommunityInfo failed for endpoint', { endpoint: this.endpoint, err: errorString.length <= 200 ? errorString : errorString.substring(0, 200) + '...', }) diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index 91e7b827c..2bb0e099c 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -2,9 +2,10 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { GraphQLClient } from 'graphql-request' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { SendCoinsArgsLoggingView } from './logging/SendCoinsArgsLogging.view' import { SendCoinsResultLoggingView } from './logging/SendCoinsResultLogging.view' import { SendCoinsArgs } from './model/SendCoinsArgs' @@ -14,6 +15,8 @@ import { revertSettledSendCoins as revertSettledSendCoinsQuery } from './query/r import { settleSendCoins as settleSendCoinsQuery } from './query/settleSendCoins' import { voteForSendCoins as voteForSendCoinsQuery } from './query/voteForSendCoins' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.SendCoinsClient`) + export class SendCoinsClient { dbCom: DbFederatedCommunity endpoint: string @@ -32,123 +35,87 @@ export class SendCoinsClient { } async voteForSendCoins(args: SendCoinsArgs): Promise { - logger.debug('X-Com: voteForSendCoins against endpoint=', this.endpoint) + logger.debug('voteForSendCoins against endpoint=', this.endpoint) try { - logger.debug( - `X-Com: SendCoinsClient: voteForSendCoins with args=`, - new SendCoinsArgsLoggingView(args), - ) + logger.debug(`voteForSendCoins with args=`, new SendCoinsArgsLoggingView(args)) const { data } = await this.client.rawRequest<{ voteForSendCoins: SendCoinsResult }>( voteForSendCoinsQuery, { args }, ) const result = data.voteForSendCoins if (!data?.voteForSendCoins?.vote) { - logger.debug( - 'X-Com: voteForSendCoins failed with: ', - new SendCoinsResultLoggingView(result), - ) + logger.debug('voteForSendCoins failed with: ', new SendCoinsResultLoggingView(result)) return new SendCoinsResult() } logger.debug( - 'X-Com: voteForSendCoins successful with result=', + 'voteForSendCoins successful with result=', new SendCoinsResultLoggingView(result), ) return result } catch (err) { - throw new LogError(`X-Com: voteForSendCoins failed for endpoint=${this.endpoint}:`, err) + throw new LogError(`voteForSendCoins failed for endpoint=${this.endpoint}:`, err) } } async revertSendCoins(args: SendCoinsArgs): Promise { - logger.debug('X-Com: revertSendCoins against endpoint=', this.endpoint) + logger.debug('revertSendCoins against endpoint=', this.endpoint) try { - logger.debug( - `X-Com: SendCoinsClient: revertSendCoins with args=`, - new SendCoinsArgsLoggingView(args), - ) + logger.debug(`revertSendCoins with args=`, new SendCoinsArgsLoggingView(args)) const { data } = await this.client.rawRequest<{ revertSendCoins: boolean }>( revertSendCoinsQuery, { args }, ) - logger.debug(`X-Com: SendCoinsClient: after revertSendCoins: data=`, data) + logger.debug(`after revertSendCoins: data=`, data) if (!data?.revertSendCoins) { - logger.warn('X-Com: revertSendCoins without response data from endpoint', this.endpoint) + logger.warn('revertSendCoins without response data from endpoint', this.endpoint) return false } - logger.debug( - `X-Com: SendCoinsClient: revertSendCoins successful from endpoint=${this.endpoint}`, - ) + logger.debug(`revertSendCoins successful from endpoint=${this.endpoint}`) return true } catch (err) { - logger.error( - `X-Com: SendCoinsClient: revertSendCoins failed for endpoint=${this.endpoint}`, - err, - ) + logger.error(`revertSendCoins failed for endpoint=${this.endpoint}`, err) return false } } async settleSendCoins(args: SendCoinsArgs): Promise { - logger.debug(`X-Com: settleSendCoins against endpoint='${this.endpoint}'...`) + logger.debug(`settleSendCoins against endpoint='${this.endpoint}'...`) try { - logger.debug( - `X-Com: SendCoinsClient: settleSendCoins with args=`, - new SendCoinsArgsLoggingView(args), - ) + logger.debug(`settleSendCoins with args=`, new SendCoinsArgsLoggingView(args)) const { data } = await this.client.rawRequest<{ settleSendCoins: boolean }>( settleSendCoinsQuery, { args }, ) - logger.debug(`X-Com: SendCoinsClient: after settleSendCoins: data=`, data) + logger.debug(`after settleSendCoins: data=`, data) if (!data?.settleSendCoins) { - logger.warn( - 'X-Com: SendCoinsClient: settleSendCoins without response data from endpoint', - this.endpoint, - ) + logger.warn('settleSendCoins without response data from endpoint', this.endpoint) return false } - logger.debug( - `X-Com: SendCoinsClient: settleSendCoins successful from endpoint=${this.endpoint}`, - ) + logger.debug(`settleSendCoins successful from endpoint=${this.endpoint}`) return true } catch (err) { - throw new LogError( - `X-Com: SendCoinsClient: settleSendCoins failed for endpoint=${this.endpoint}`, - err, - ) + throw new LogError(`settleSendCoins failed for endpoint=${this.endpoint}`, err) } } async revertSettledSendCoins(args: SendCoinsArgs): Promise { - logger.debug(`X-Com: revertSettledSendCoins against endpoint='${this.endpoint}'...`) + logger.debug(`revertSettledSendCoins against endpoint='${this.endpoint}'...`) try { - logger.debug( - `X-Com: SendCoinsClient: revertSettledSendCoins with args=`, - new SendCoinsArgsLoggingView(args), - ) + logger.debug(`revertSettledSendCoins with args=`, new SendCoinsArgsLoggingView(args)) const { data } = await this.client.rawRequest<{ revertSettledSendCoins: boolean }>( revertSettledSendCoinsQuery, { args }, ) - logger.debug(`X-Com: SendCoinsClient: after revertSettledSendCoins: data=`, data) + logger.debug(`after revertSettledSendCoins: data=`, data) if (!data?.revertSettledSendCoins) { - logger.warn( - 'X-Com: SendCoinsClient: revertSettledSendCoins without response data from endpoint', - this.endpoint, - ) + logger.warn('revertSettledSendCoins without response data from endpoint', this.endpoint) return false } - logger.debug( - `X-Com: SendCoinsClient: revertSettledSendCoins successful from endpoint=${this.endpoint}`, - ) + logger.debug(`revertSettledSendCoins successful from endpoint=${this.endpoint}`) return true } catch (err) { - throw new LogError( - `X-Com: SendCoinsClient: revertSettledSendCoins failed for endpoint=${this.endpoint}`, - err, - ) + throw new LogError(`revertSettledSendCoins failed for endpoint=${this.endpoint}`, err) } } } diff --git a/backend/src/federation/validateCommunities.test.ts b/backend/src/federation/validateCommunities.test.ts index 731ca72a1..b6ef2418a 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -2,13 +2,19 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { FederatedCommunity as DbFederatedCommunity } from 'database' import { GraphQLClient } from 'graphql-request' import { Response } from 'graphql-request/dist/types' -import { DataSource } from 'typeorm' +import { DataSource, Not } from 'typeorm' import { cleanDB, testEnvironment } from '@test/helpers' -import { logger } from '@test/testSetup' +import { getLogger } from 'config-schema/test/testSetup' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { validateCommunities } from './validateCommunities' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`) +const federationClientLogger = getLogger( + `${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.FederationClient`, +) + let con: DataSource let testEnv: { mutate: ApolloServerTestClient['mutate'] @@ -49,7 +55,7 @@ describe('validate Communities', () => { }) it('logs zero communities found', () => { - expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`) + expect(logger.debug).toBeCalledWith(`found 0 dbCommunities`) }) describe('with one Community of api 1_0 but missing pubKey response', () => { @@ -79,11 +85,11 @@ describe('validate Communities', () => { }) it('logs one community found', () => { - expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`) + expect(logger.debug).toBeCalledWith(`found 1 dbCommunities`) }) it('logs requestGetPublicKey missing response data ', () => { - expect(logger.warn).toBeCalledWith( - 'Federation: getPublicKey without response data from endpoint', + expect(federationClientLogger.warn).toBeCalledWith( + 'getPublicKey without response data from endpoint', 'http//localhost:5001/api/1_0/', ) }) @@ -153,17 +159,17 @@ describe('validate Communities', () => { }) it('logs one community found', () => { - expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`) + expect(logger.debug).toBeCalledWith(`found 1 dbCommunities`) }) it('logs requestGetPublicKey for community api 1_0 ', () => { - expect(logger.debug).toBeCalledWith( - 'Federation: getPublicKey from endpoint', + expect(federationClientLogger.debug).toBeCalledWith( + 'getPublicKey from endpoint', 'http//localhost:5001/api/1_0/', ) }) it('logs not matching publicKeys', () => { expect(logger.debug).toBeCalledWith( - 'Federation: received not matching publicKey:', + 'received not matching publicKey:', 'somePubKey', expect.stringMatching('11111111111111111111111111111111'), ) @@ -197,24 +203,24 @@ describe('validate Communities', () => { overwrite: ['end_point', 'last_announced_at'], }) .execute() - await DbFederatedCommunity.update({}, { verifiedAt: null }) + await DbFederatedCommunity.update({ id: Not(0) }, { verifiedAt: null }) // jest.clearAllMocks() await validateCommunities() }) it('logs one community found', () => { - expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`) + expect(logger.debug).toBeCalledWith(`found 1 dbCommunities`) }) it('logs requestGetPublicKey for community api 1_0 ', () => { - expect(logger.debug).toBeCalledWith( - 'Federation: getPublicKey from endpoint', + expect(federationClientLogger.debug).toBeCalledWith( + 'getPublicKey from endpoint', 'http//localhost:5001/api/1_0/', ) }) it('logs community pubKey verified', () => { - expect(logger.debug).toHaveBeenNthCalledWith( - 5, - 'Federation: getPublicKey successful from endpoint', + expect(federationClientLogger.debug).toHaveBeenNthCalledWith( + 2, + 'getPublicKey successful from endpoint', 'http//localhost:5001/api/1_0/', '11111111111111111111111111111111', ) @@ -264,22 +270,22 @@ describe('validate Communities', () => { }) .execute() - await DbFederatedCommunity.update({}, { verifiedAt: null }) + await DbFederatedCommunity.update({ id: Not(0) }, { verifiedAt: null }) // jest.clearAllMocks() await validateCommunities() }) it('logs two communities found', () => { - expect(logger.debug).toBeCalledWith(`Federation: found 2 dbCommunities`) + expect(logger.debug).toBeCalledWith(`found 2 dbCommunities`) }) it('logs requestGetPublicKey for community api 1_0 ', () => { - expect(logger.debug).toBeCalledWith( - 'Federation: getPublicKey from endpoint', + expect(federationClientLogger.debug).toBeCalledWith( + 'getPublicKey from endpoint', 'http//localhost:5001/api/1_0/', ) }) it('logs requestGetPublicKey for community api 1_1 ', () => { - expect(logger.debug).toBeCalledWith( - 'Federation: getPublicKey from endpoint', + expect(federationClientLogger.debug).toBeCalledWith( + 'getPublicKey from endpoint', 'http//localhost:5001/api/1_1/', ) }) @@ -316,28 +322,28 @@ describe('validate Communities', () => { dbCom = await DbFederatedCommunity.findOneOrFail({ where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion }, }) - await DbFederatedCommunity.update({}, { verifiedAt: null }) + await DbFederatedCommunity.update({ id: Not(0) }, { verifiedAt: null }) // jest.clearAllMocks() await validateCommunities() }) it('logs three community found', () => { - expect(logger.debug).toBeCalledWith(`Federation: found 3 dbCommunities`) + expect(logger.debug).toBeCalledWith(`found 3 dbCommunities`) }) it('logs requestGetPublicKey for community api 1_0 ', () => { - expect(logger.debug).toBeCalledWith( - 'Federation: getPublicKey from endpoint', + expect(federationClientLogger.debug).toBeCalledWith( + 'getPublicKey from endpoint', 'http//localhost:5001/api/1_0/', ) }) it('logs requestGetPublicKey for community api 1_1 ', () => { - expect(logger.debug).toBeCalledWith( - 'Federation: getPublicKey from endpoint', + expect(federationClientLogger.debug).toBeCalledWith( + 'getPublicKey from endpoint', 'http//localhost:5001/api/1_1/', ) }) it('logs unsupported api for community with api 2_0 ', () => { expect(logger.debug).toBeCalledWith( - 'Federation: dbCom with unsupported apiVersion', + 'dbCom with unsupported apiVersion', dbCom.endPoint, '2_0', ) diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 9ad44d232..6b17a13cf 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -9,20 +9,20 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1 import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' - +import { getLogger } from 'log4js' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' import { ApiVersionType } from './enum/apiVersionType' import { createKeyPair } from '@/auth/jwt/JWT' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`) export async function startValidateCommunities(timerInterval: number): Promise { if (Number.isNaN(timerInterval) || timerInterval <= 0) { throw new LogError('FEDERATION_VALIDATE_COMMUNITY_TIMER is not a positive number') } - logger.info( - `Federation: startValidateCommunities loop with an interval of ${timerInterval} ms...`, - ) + logger.info(`startValidateCommunities loop with an interval of ${timerInterval} ms...`) // delete all foreign federated community entries to avoid increasing validation efforts and log-files await DbFederatedCommunity.delete({ foreign: true }) @@ -42,17 +42,13 @@ export async function validateCommunities(): Promise { .orWhere('verified_at < last_announced_at') .getMany() - logger.debug(`Federation: found ${dbFederatedCommunities.length} dbCommunities`) + logger.debug(`found ${dbFederatedCommunities.length} dbCommunities`) for (const dbCom of dbFederatedCommunities) { - logger.debug('Federation: dbCom', new FederatedCommunityLoggingView(dbCom)) + logger.debug('dbCom', new FederatedCommunityLoggingView(dbCom)) const apiValueStrings: string[] = Object.values(ApiVersionType) logger.debug(`suppported ApiVersions=`, apiValueStrings) if (!apiValueStrings.includes(dbCom.apiVersion)) { - logger.debug( - 'Federation: dbCom with unsupported apiVersion', - dbCom.endPoint, - dbCom.apiVersion, - ) + logger.debug('dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion) continue } try { @@ -62,7 +58,7 @@ export async function validateCommunities(): Promise { const pubKey = await client.getPublicKey() if (pubKey && pubKey === dbCom.publicKey.toString('hex')) { await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) - logger.debug(`Federation: verified community with:`, dbCom.endPoint) + logger.debug(`verified community with:`, dbCom.endPoint) const pubComInfo = await client.getPublicCommunityInfo() if (pubComInfo) { await writeForeignCommunity(dbCom, pubComInfo) @@ -71,16 +67,12 @@ export async function validateCommunities(): Promise { } catch (err) { logger.warn(`Warning: Community Authentication still not ready:`, err) } - logger.debug(`Federation: write publicInfo of community: name=${pubComInfo.name}`) + logger.debug(`write publicInfo of community: name=${pubComInfo.name}`) } else { - logger.debug('Federation: missing result of getPublicCommunityInfo') + logger.debug('missing result of getPublicCommunityInfo') } } else { - logger.debug( - 'Federation: received not matching publicKey:', - pubKey, - dbCom.publicKey.toString('hex'), - ) + logger.debug('received not matching publicKey:', pubKey, dbCom.publicKey.toString('hex')) } } } catch (err) { diff --git a/backend/src/graphql/enum/PendingTransactionState.ts b/backend/src/graphql/enum/PendingTransactionState.ts index d89b0b0eb..e59f3fd7d 100644 --- a/backend/src/graphql/enum/PendingTransactionState.ts +++ b/backend/src/graphql/enum/PendingTransactionState.ts @@ -1,11 +1,5 @@ import { registerEnumType } from 'type-graphql' - -export enum PendingTransactionState { - NEW = 1, - PENDING = 2, - SETTLED = 3, - REVERTED = 4, -} +import { PendingTransactionState } from 'shared' registerEnumType(PendingTransactionState, { name: 'PendingTransactionState', // this one is mandatory diff --git a/backend/src/graphql/model/Decay.ts b/backend/src/graphql/model/Decay.ts index a32b96c13..838e442f7 100644 --- a/backend/src/graphql/model/Decay.ts +++ b/backend/src/graphql/model/Decay.ts @@ -1,14 +1,6 @@ import { Decimal } from 'decimal.js-light' import { Field, Int, ObjectType } from 'type-graphql' - -interface DecayInterface { - balance: Decimal - decay: Decimal - roundedDecay: Decimal - start: Date | null - end: Date | null - duration: number | null -} +import { Decay as DecayInterface } from 'shared' @ObjectType() export class Decay { diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index 2ea34cc5a..f7388c027 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -9,9 +9,10 @@ import { RIGHTS } from '@/auth/RIGHTS' import { BalanceLoggingView } from '@/logging/BalanceLogging.view' import { DecayLoggingView } from '@/logging/DecayLogging.view' import { Context, getUser } from '@/server/context' -import { backendLogger as logger } from '@/server/logger' -import { calculateDecay } from '@/util/decay' +import { calculateDecay } from 'shared' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { GdtResolver } from './GdtResolver' import { getLastTransaction } from './util/getLastTransaction' import { transactionLinkSummary } from './util/transactionLinkSummary' @@ -23,9 +24,10 @@ export class BalanceResolver { async balance(@Ctx() context: Context): Promise { const user = getUser(context) const now = new Date() + const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.BalanceResolver`) logger.addContext('user', user.id) - logger.info(`balance(userId=${user.id})...`) + logger.info(`balance...`) let balanceGDT if (!context.balanceGDT) { diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index f636bf53d..c9c925a2e 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -5,7 +5,7 @@ import { DataSource } from 'typeorm' import { v4 as uuidv4 } from 'uuid' import { cleanDB, testEnvironment } from '@test/helpers' -import { i18n as localization, logger } from '@test/testSetup' +import { i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations' @@ -18,10 +18,12 @@ import { } from '@/seeds/graphql/queries' import { peterLustig } from '@/seeds/users/peter-lustig' +import { getLogger } from 'config-schema/test/testSetup' import { getCommunityByUuid } from './util/communities' jest.mock('@/password/EncryptorUtils') + // to do: We need a setup for the tests that closes the connection let mutate: ApolloServerTestClient['mutate'] let query: ApolloServerTestClient['query'] @@ -40,7 +42,7 @@ const peterLoginData = { } beforeAll(async () => { - testEnv = await testEnvironment(logger, localization) + testEnv = await testEnvironment(getLogger('apollo'), localization) mutate = testEnv.mutate query = testEnv.query con = testEnv.con diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index ab1115a05..779e7db31 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -1,4 +1,4 @@ -import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database' +import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, getHomeCommunity } from 'database' import { Arg, Args, Authorized, Mutation, Query, Resolver } from 'type-graphql' import { IsNull, Not } from 'typeorm' @@ -16,7 +16,6 @@ import { getAllCommunities, getCommunityByIdentifier, getCommunityByUuid, - getHomeCommunity, } from './util/communities' @Resolver() diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts index 9c9bdfa55..9db6a7d99 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts @@ -5,8 +5,8 @@ import { GraphQLError } from 'graphql' import { DataSource } from 'typeorm' import { cleanDB, resetToken, testEnvironment } from '@test/helpers' -import { logger } from '@test/testSetup' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { EventType } from '@/event/Events' import { userFactory } from '@/seeds/factory/user' import { @@ -18,9 +18,12 @@ import { import { listContributionLinks } from '@/seeds/graphql/queries' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' +import { getLogger } from 'config-schema/test/testSetup' jest.mock('@/password/EncryptorUtils') +const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) + let mutate: ApolloServerTestClient['mutate'] let query: ApolloServerTestClient['query'] let con: DataSource @@ -286,7 +289,7 @@ describe('Contribution Links', () => { }) it('logs the error "A Start-Date must be set"', () => { - expect(logger.error).toBeCalledWith('A Start-Date must be set') + expect(logErrorLogger.error).toBeCalledWith('A Start-Date must be set') }) it('returns an error if missing endDate', async () => { @@ -307,7 +310,7 @@ describe('Contribution Links', () => { }) it('logs the error "An End-Date must be set"', () => { - expect(logger.error).toBeCalledWith('An End-Date must be set') + expect(logErrorLogger.error).toBeCalledWith('An End-Date must be set') }) it('returns an error if endDate is before startDate', async () => { @@ -331,7 +334,7 @@ describe('Contribution Links', () => { }) it('logs the error "The value of validFrom must before or equals the validTo"', () => { - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( `The value of validFrom must before or equals the validTo`, ) }) @@ -531,7 +534,7 @@ describe('Contribution Links', () => { }) it('logs the error "Contribution Link not found"', () => { - expect(logger.error).toBeCalledWith('Contribution Link not found', -1) + expect(logErrorLogger.error).toBeCalledWith('Contribution Link not found', -1) }) describe('valid id', () => { @@ -613,7 +616,7 @@ describe('Contribution Links', () => { }) it('logs the error "Contribution Link not found"', () => { - expect(logger.error).toBeCalledWith('Contribution Link not found', -1) + expect(logErrorLogger.error).toBeCalledWith('Contribution Link not found', -1) }) }) diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts index 980b7fa22..962b77766 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts @@ -5,8 +5,9 @@ import { DataSource } from 'typeorm' import { ContributionStatus } from '@enum/ContributionStatus' import { cleanDB, resetToken, testEnvironment } from '@test/helpers' -import { i18n as localization, logger } from '@test/testSetup' +import { i18n as localization } from '@test/testSetup' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants' import { EventType } from '@/event/Events' import { userFactory } from '@/seeds/factory/user' @@ -20,6 +21,13 @@ import { adminListContributionMessages, listContributionMessages } from '@/seeds import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { peterLustig } from '@/seeds/users/peter-lustig' +import { getLogger} from 'config-schema/test/testSetup' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionMessageResolver`) +const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) +const interactionLogger = getLogger( + `${LOG4JS_BASE_CATEGORY_NAME}.interactions.updateUnconfirmedContribution`, +) jest.mock('@/password/EncryptorUtils') jest.mock('@/emails/sendEmailVariants', () => { @@ -121,7 +129,7 @@ describe('ContributionMessageResolver', () => { }) it('logs the error "ContributionMessage was not sent successfully: Error: Contribution not found"', () => { - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( 'ContributionMessage was not sent successfully: Error: Contribution not found', new Error('Contribution not found'), ) @@ -148,9 +156,7 @@ describe('ContributionMessageResolver', () => { message: 'Test', }, }) - expect(logger.debug).toBeCalledTimes(5) - expect(logger.debug).toHaveBeenNthCalledWith( - 5, + expect(interactionLogger.debug).toBeCalledWith( 'use UnconfirmedContributionUserAddMessageRole', ) expect(mutationResult).toEqual( @@ -327,7 +333,7 @@ describe('ContributionMessageResolver', () => { }) it('logs the error "ContributionMessage was not sent successfully: Error: Contribution not found"', () => { - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( 'ContributionMessage was not sent successfully: Error: Contribution not found', new Error('Contribution not found'), ) @@ -348,9 +354,7 @@ describe('ContributionMessageResolver', () => { }, }) - expect(logger.debug).toBeCalledTimes(5) - expect(logger.debug).toHaveBeenNthCalledWith( - 5, + expect(interactionLogger.debug).toBeCalledWith( 'use UnconfirmedContributionAdminAddMessageRole', ) @@ -382,10 +386,7 @@ describe('ContributionMessageResolver', () => { message: 'Test', }, }) - - expect(logger.debug).toBeCalledTimes(5) - expect(logger.debug).toHaveBeenNthCalledWith( - 5, + expect(interactionLogger.debug).toBeCalledWith( 'use UnconfirmedContributionAdminAddMessageRole', ) @@ -401,13 +402,12 @@ describe('ContributionMessageResolver', () => { }) it('logs the error "ContributionMessage was not sent successfully: Error: missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user"', () => { - expect(logger.debug).toBeCalledTimes(5) - expect(logger.error).toHaveBeenNthCalledWith( + expect(logErrorLogger.error).toHaveBeenNthCalledWith( 1, 'missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user', expect.any(Number), ) - expect(logger.error).toHaveBeenNthCalledWith( + expect(logErrorLogger.error).toHaveBeenNthCalledWith( 2, 'ContributionMessage was not sent successfully: Error: missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user', new Error('missing right ADMIN_CREATE_CONTRIBUTION_MESSAGE for user'), diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts index 1a3323754..8ddf782b0 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -22,12 +22,14 @@ import { import { UpdateUnconfirmedContributionContext } from '@/interactions/updateUnconfirmedContribution/UpdateUnconfirmedContribution.context' import { LogError } from '@/server/LogError' import { Context, getUser } from '@/server/context' -import { backendLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { contributionFrontendLink } from './util/contributions' import { findContributionMessages } from './util/findContributionMessages' const db = AppDatabase.getInstance() +const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionMessageResolver`) @Resolver() export class ContributionMessageResolver { @@ -126,7 +128,9 @@ export class ContributionMessageResolver { @Args() contributionMessageArgs: ContributionMessageArgs, @Ctx() context: Context, ): Promise { + const logger = createLogger() const { contributionId, messageType } = contributionMessageArgs + logger.addContext('contribution', contributionMessageArgs.contributionId) const updateUnconfirmedContributionContext = new UpdateUnconfirmedContributionContext( contributionId, contributionMessageArgs, diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 4398c41e4..1bb3b08b7 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -14,8 +14,9 @@ import { resetToken, testEnvironment, } from '@test/helpers' -import { i18n as localization, logger } from '@test/testSetup' +import { i18n as localization } from '@test/testSetup' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { sendContributionConfirmedEmail, sendContributionDeletedEmail, @@ -50,10 +51,14 @@ import { peterLustig } from '@/seeds/users/peter-lustig' import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' import { stephenHawking } from '@/seeds/users/stephen-hawking' import { getFirstDayOfPreviousNMonth } from '@/util/utilities' +import { getLogger } from 'config-schema/test/testSetup' +import { getLogger as originalGetLogger } from 'log4js' jest.mock('@/emails/sendEmailVariants') jest.mock('@/password/EncryptorUtils') +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) + let mutate: ApolloServerTestClient['mutate'] let query: ApolloServerTestClient['query'] let con: DataSource @@ -72,7 +77,7 @@ let contributionToDelete: any let bibiCreatedContribution: Contribution beforeAll(async () => { - testEnv = await testEnvironment(logger, localization) + testEnv = await testEnvironment(originalGetLogger('apollo'), localization) mutate = testEnv.mutate query = testEnv.query con = testEnv.con diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 56e01fa27..c6f7b33c1 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -7,7 +7,7 @@ import { import { Decimal } from 'decimal.js-light' import { GraphQLResolveInfo } from 'graphql' import { Arg, Args, Authorized, Ctx, Info, Int, Mutation, Query, Resolver } from 'type-graphql' -import { EntityManager, IsNull, getConnection } from 'typeorm' +import { EntityManager, IsNull } from 'typeorm' import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs' import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs' @@ -19,7 +19,6 @@ import { ContributionType } from '@enum/ContributionType' import { TransactionTypeId } from '@enum/TransactionTypeId' import { AdminUpdateContribution } from '@model/AdminUpdateContribution' import { Contribution, ContributionListResult } from '@model/Contribution' -import { Decay } from '@model/Decay' import { OpenCreation } from '@model/OpenCreation' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' @@ -43,13 +42,14 @@ import { import { UpdateUnconfirmedContributionContext } from '@/interactions/updateUnconfirmedContribution/UpdateUnconfirmedContribution.context' import { LogError } from '@/server/LogError' import { Context, getClientTimezoneOffset, getUser } from '@/server/context' -import { backendLogger as logger } from '@/server/logger' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' -import { calculateDecay } from '@/util/decay' +import { calculateDecay, Decay } from 'shared' import { fullName } from '@/util/utilities' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { ContributionMessageType } from '@enum/ContributionMessageType' import { AppDatabase } from 'database' -import { ContributionMessageType } from '../enum/ContributionMessageType' +import { getLogger } from 'log4js' import { contributionFrontendLink, loadAllContributions, @@ -62,6 +62,7 @@ import { getLastTransaction } from './util/getLastTransaction' import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector' const db = AppDatabase.getInstance() +const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionResolver`) @Resolver(() => Contribution) export class ContributionResolver { @@ -85,6 +86,8 @@ export class ContributionResolver { const user = getUser(context) const creations = await getUserCreation(user.id, clientTimezoneOffset) + const logger = createLogger() + logger.addContext('user', user.id) logger.trace('creations', creations) const contributionDateObj = new Date(contributionDate) validateContribution(creations, amount, contributionDateObj, clientTimezoneOffset) @@ -215,6 +218,8 @@ export class ContributionResolver { @Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs, @Ctx() context: Context, ): Promise { + const logger = createLogger() + logger.addContext('admin', context.user?.id) logger.info( `adminCreateContribution(email=${email}, amount=${amount.toString()}, memo=${memo}, creationDate=${creationDate})`, ) @@ -266,6 +271,8 @@ export class ContributionResolver { @Args() adminUpdateContributionArgs: AdminUpdateContributionArgs, @Ctx() context: Context, ): Promise { + const logger = createLogger() + logger.addContext('contribution', adminUpdateContributionArgs.id) const updateUnconfirmedContributionContext = new UpdateUnconfirmedContributionContext( adminUpdateContributionArgs.id, adminUpdateContributionArgs, @@ -277,7 +284,6 @@ export class ContributionResolver { await transactionalEntityManager.save(contribution) // TODO: move into specialized view or formatting for logging class logger.debug('saved changed contribution', { - id: contribution.id, amount: contribution.amount.toString(), memo: contribution.memo, contributionDate: contribution.contributionDate.toString(), @@ -288,7 +294,6 @@ export class ContributionResolver { await transactionalEntityManager.save(contributionMessage) // TODO: move into specialized view or formatting for logging class logger.debug('save new contributionMessage', { - contributionId: contributionMessage.contributionId, type: contributionMessage.type, message: contributionMessage.message, isModerator: contributionMessage.isModerator, @@ -428,6 +433,9 @@ export class ContributionResolver { @Arg('id', () => Int) id: number, @Ctx() context: Context, ): Promise { + const logger = createLogger() + logger.addContext('contribution', id) + // acquire lock const releaseLock = await TRANSACTIONS_LOCK.acquire() try { diff --git a/backend/src/graphql/resolver/EmailOptinCodes.test.ts b/backend/src/graphql/resolver/EmailOptinCodes.test.ts index 37bf6cc8b..b916d23b6 100644 --- a/backend/src/graphql/resolver/EmailOptinCodes.test.ts +++ b/backend/src/graphql/resolver/EmailOptinCodes.test.ts @@ -101,6 +101,7 @@ describe('EmailOptinCodes', () => { describe('forgotPassword', () => { it('throws an error', async () => { + await mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }) await expect( mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }), ).resolves.toMatchObject({ diff --git a/backend/src/graphql/resolver/GdtResolver.ts b/backend/src/graphql/resolver/GdtResolver.ts index 933fdb397..c689cf9c2 100644 --- a/backend/src/graphql/resolver/GdtResolver.ts +++ b/backend/src/graphql/resolver/GdtResolver.ts @@ -10,8 +10,10 @@ import { RIGHTS } from '@/auth/RIGHTS' import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' import { Context, getUser } from '@/server/context' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { backendLogger as logger } from '@/server/logger' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.GdtResolver`) @Resolver() export class GdtResolver { diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index 6e6cdefca..f3ac85ef4 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -2,15 +2,19 @@ import { Event as DbEvent, UserContact } from 'database' import { GraphQLError } from 'graphql' import { cleanDB, resetToken, testEnvironment } from '@test/helpers' -import { i18n as localization, logger } from '@test/testSetup' +import { i18n as localization } from '@test/testSetup' +import { getLogger } from 'config-schema/test/testSetup' import { EventType } from '@/event/Events' import { userFactory } from '@/seeds/factory/user' import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' jest.mock('@/password/EncryptorUtils') +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.KlicktippResolver`) + let testEnv: any let mutate: any let con: any diff --git a/backend/src/graphql/resolver/ProjectBrandingResolver.ts b/backend/src/graphql/resolver/ProjectBrandingResolver.ts index 3742ac2c4..9e59dbbaa 100644 --- a/backend/src/graphql/resolver/ProjectBrandingResolver.ts +++ b/backend/src/graphql/resolver/ProjectBrandingResolver.ts @@ -9,7 +9,10 @@ import { SpaceList } from '@model/SpaceList' import { HumHubClient } from '@/apis/humhub/HumHubClient' import { RIGHTS } from '@/auth/RIGHTS' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ProjectBrandingResolver`) @Resolver(() => ProjectBranding) export class ProjectBrandingResolver { diff --git a/backend/src/graphql/resolver/StatisticsResolver.ts b/backend/src/graphql/resolver/StatisticsResolver.ts index 6713cbb54..7ba54fafb 100644 --- a/backend/src/graphql/resolver/StatisticsResolver.ts +++ b/backend/src/graphql/resolver/StatisticsResolver.ts @@ -5,7 +5,7 @@ import { Authorized, FieldResolver, Query, Resolver } from 'type-graphql' import { CommunityStatistics, DynamicStatisticsFields } from '@model/CommunityStatistics' import { RIGHTS } from '@/auth/RIGHTS' -import { calculateDecay } from '@/util/decay' +import { calculateDecay } from 'shared' const db = AppDatabase.getInstance() diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 92c147389..7862f71f0 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -12,7 +12,6 @@ import { DataSource } from 'typeorm' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { cleanDB, resetEntity, resetToken, testEnvironment } from '@test/helpers' -import { logger } from '@test/testSetup' import { EventType } from '@/event/Events' import { creations } from '@/seeds/creation/index' @@ -35,8 +34,12 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'config-schema/test/testSetup' import { transactionLinkCode } from './TransactionLinkResolver' +const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) + jest.mock('@/password/EncryptorUtils') // mock semaphore to allow use fake timers @@ -221,7 +224,7 @@ describe('TransactionLinkResolver', () => { }) }) it('logs the error "User has not enough GDD"', () => { - expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number)) + expect(logErrorLogger.error).toBeCalledWith('User has not enough GDD', expect.any(Number)) }) }) }) @@ -273,11 +276,11 @@ describe('TransactionLinkResolver', () => { }) it('logs the error "No contribution link found to given code"', () => { - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( 'No contribution link found to given code', 'CL-123456', ) - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( 'Creation from contribution link was not successful', new Error('No contribution link found to given code'), ) @@ -317,8 +320,11 @@ describe('TransactionLinkResolver', () => { }) it('logs the error "Contribution link is not valid yet"', () => { - expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom) - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( + 'Contribution link is not valid yet', + validFrom, + ) + expect(logErrorLogger.error).toBeCalledWith( 'Creation from contribution link was not successful', new Error('Contribution link is not valid yet'), ) @@ -356,8 +362,11 @@ describe('TransactionLinkResolver', () => { }) it('logs the error "Contribution link has unknown cycle"', () => { - expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID') - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( + 'Contribution link has unknown cycle', + 'INVALID', + ) + expect(logErrorLogger.error).toBeCalledWith( 'Creation from contribution link was not successful', new Error('Contribution link has unknown cycle'), ) @@ -395,8 +404,11 @@ describe('TransactionLinkResolver', () => { }) it('logs the error "Contribution link is no longer valid"', () => { - expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo) - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( + 'Contribution link is no longer valid', + validTo, + ) + expect(logErrorLogger.error).toBeCalledWith( 'Creation from contribution link was not successful', new Error('Contribution link is no longer valid'), ) @@ -491,7 +503,7 @@ describe('TransactionLinkResolver', () => { }) it('logs the error "Creation from contribution link was not successful"', () => { - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( 'Creation from contribution link was not successful', new Error( 'The amount to be created exceeds the amount still available for this month', @@ -566,7 +578,7 @@ describe('TransactionLinkResolver', () => { }) it('logs the error "Creation from contribution link was not successful"', () => { - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( 'Creation from contribution link was not successful', new Error('Contribution link already redeemed today'), ) @@ -618,7 +630,7 @@ describe('TransactionLinkResolver', () => { }) it('logs the error "Creation from contribution link was not successful"', () => { - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( 'Creation from contribution link was not successful', new Error('Contribution link already redeemed today'), ) @@ -652,7 +664,7 @@ describe('TransactionLinkResolver', () => { ).resolves.toMatchObject({ errors: [new GraphQLError('Transaction link not found')], }) - expect(logger.error).toBeCalledWith('Transaction link not found', 'not-valid') + expect(logErrorLogger.error).toBeCalledWith('Transaction link not found', 'not-valid') }) }) @@ -723,7 +735,7 @@ describe('TransactionLinkResolver', () => { ).resolves.toMatchObject({ errors: [new GraphQLError('Cannot redeem own transaction link')], }) - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( 'Cannot redeem own transaction link', expect.any(Number), ) @@ -927,7 +939,7 @@ describe('TransactionLinkResolver', () => { }) it('logs the error "Could not find requested User"', () => { - expect(logger.error).toBeCalledWith('Could not find requested User', -1) + expect(logErrorLogger.error).toBeCalledWith('Could not find requested User', -1) }) }) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 01be0fef1..c7861f366 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -21,10 +21,10 @@ import { Transaction as DbTransaction, TransactionLink as DbTransactionLink, User as DbUser, + getHomeCommunity, } from 'database' import { Decimal } from 'decimal.js-light' import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql' -import { getConnection } from 'typeorm' import { RIGHTS } from '@/auth/RIGHTS' import { decode, encode, verify } from '@/auth/jwt/JWT' @@ -37,25 +37,27 @@ import { } from '@/event/Events' import { LogError } from '@/server/LogError' import { Context, getClientTimezoneOffset, getUser } from '@/server/context' -import { backendLogger as logger } from '@/server/logger' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTION_LINK_LOCK } from '@/util/TRANSACTION_LINK_LOCK' -import { calculateDecay } from '@/util/decay' +import { calculateDecay } from 'shared' import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' import { DisburseJwtPayloadType } from '@/auth/jwt/payloadtypes/DisburseJwtPayloadType' +import { Logger, getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { executeTransaction } from './TransactionResolver' import { getAuthenticatedCommunities, getCommunityByUuid, - getHomeCommunity, } from './util/communities' import { getUserCreation, validateContribution } from './util/creations' import { getLastTransaction } from './util/getLastTransaction' import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector' import { transactionLinkList } from './util/transactionLinkList' +const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionLinkResolver`) + // TODO: do not export, test it inside the resolver export const transactionLinkCode = (date: Date): string => { const time = date.getTime().toString(16) @@ -149,7 +151,9 @@ export class TransactionLinkResolver { @Authorized([RIGHTS.QUERY_TRANSACTION_LINK]) @Query(() => QueryLinkResult) async queryTransactionLink(@Arg('code') code: string): Promise { - logger.debug('TransactionLinkResolver.queryTransactionLink... code=', code) + const logger = createLogger() + logger.addContext('code', code.substring(0, 6)) + logger.debug('TransactionLinkResolver.queryTransactionLink...') if (code.match(/^CL-/)) { const contributionLink = await DbContributionLink.findOneOrFail({ where: { code: code.replace('CL-', '') }, @@ -185,7 +189,7 @@ export class TransactionLinkResolver { return new TransactionLink(dbTransactionLink, new User(user), redeemedBy, communities) } else { // redeem jwt-token - return await this.queryRedeemJwtLink(code) + return await this.queryRedeemJwtLink(code, logger) } } } @@ -196,6 +200,8 @@ export class TransactionLinkResolver { @Arg('code', () => String) code: string, @Ctx() context: Context, ): Promise { + const logger = createLogger() + logger.addContext('code', code.substring(0, 6)) const clientTimezoneOffset = getClientTimezoneOffset(context) // const homeCom = await DbCommunity.findOneOrFail({ where: { foreign: false } }) const user = getUser(context) @@ -380,6 +386,7 @@ export class TransactionLinkResolver { transactionLink.memo, linkedUser, user, + logger, transactionLink, ) await EVENT_TRANSACTION_LINK_REDEEM( @@ -409,6 +416,8 @@ export class TransactionLinkResolver { @Arg('alias', { nullable: true }) alias?: string, @Arg('validUntil', { nullable: true }) validUntil?: string, ): Promise { + const logger = createLogger() + logger.addContext('code', code.substring(0, 6)) logger.debug('TransactionLinkResolver.queryRedeemJwt... args=', { gradidoId, senderCommunityUuid, @@ -433,6 +442,9 @@ export class TransactionLinkResolver { ) // TODO:encode/sign the jwt normally with the private key of the sender/home community, but interims with uuid const homeCom = await getHomeCommunity() + if (!homeCom) { + throw new LogError('Home community not found') + } if (!homeCom.communityUuid) { throw new LogError('Home community UUID is not set') } @@ -457,6 +469,8 @@ export class TransactionLinkResolver { @Arg('validUntil', { nullable: true }) validUntil?: string, @Arg('recipientAlias', { nullable: true }) recipientAlias?: string, ): Promise { + const logger = createLogger() + logger.addContext('code', code.substring(0, 6)) logger.debug('TransactionLinkResolver.disburseTransactionLink... args=', { senderGradidoId, senderCommunityUuid, @@ -528,7 +542,7 @@ export class TransactionLinkResolver { return transactionLinkList(paginated, filters, user) } - async queryRedeemJwtLink(code: string): Promise { + async queryRedeemJwtLink(code: string, logger: Logger): Promise { logger.debug('TransactionLinkResolver.queryRedeemJwtLink... redeem jwt-token found') // decode token first to get the senderCommunityUuid as input for verify token const decodedPayload = decode(code) @@ -619,6 +633,9 @@ export class TransactionLinkResolver { ) } const homeCommunity = await getHomeCommunity() + if (!homeCommunity) { + throw new LogError('Home community not found') + } const recipientCommunity = new Community(homeCommunity) const senderCommunity = new Community(senderCom) const senderUser = new User(null) @@ -653,6 +670,8 @@ export class TransactionLinkResolver { validUntil: string, recipientAlias: string, ): Promise { + const logger = createLogger() + logger.addContext('code', code.substring(0, 6)) logger.debug('TransactionLinkResolver.createDisburseJwt... args=', { senderCommunityUuid, senderGradidoId, diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index d164cd5a8..77026b445 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -8,13 +8,13 @@ import { User, } from 'database' import { GraphQLError } from 'graphql' -import { Connection, In } from 'typeorm' +import { DataSource, In } from 'typeorm' import { v4 as uuidv4 } from 'uuid' import { cleanDB, testEnvironment } from '@test/helpers' -import { logger } from '@test/testSetup' import { CONFIG } from '@/config' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { EventType } from '@/event/Events' import { SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient' import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs' @@ -32,16 +32,19 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { peterLustig } from '@/seeds/users/peter-lustig' import { stephenHawking } from '@/seeds/users/stephen-hawking' +import { getLogger } from 'config-schema/test/testSetup' jest.mock('@/password/EncryptorUtils') +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) + let mutate: ApolloServerTestClient['mutate'] let query: ApolloServerTestClient['query'] -let con: Connection +let con: DataSource let testEnv: { mutate: ApolloServerTestClient['mutate'] query: ApolloServerTestClient['query'] - con: Connection + con: DataSource } beforeAll(async () => { @@ -134,19 +137,11 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError('No user with this credentials')], + errors: [new GraphQLError('The recipient user was not found')], }), ) }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'No user with this credentials', - 'wrong@email.com', - homeCom.communityUuid, - ) - }) - describe('deleted recipient', () => { it('throws an error', async () => { jest.clearAllMocks() @@ -167,18 +162,10 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError('No user with this credentials')], + errors: [new GraphQLError('The recipient user was not found')], }), ) }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'No user with this credentials', - 'stephen@hawking.uk', - homeCom.communityUuid, - ) - }) }) describe('recipient account not activated', () => { @@ -201,18 +188,10 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError('No user with this credentials')], + errors: [new GraphQLError('The recipient user was not found')], }), ) }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'No user with this credentials', - 'garrick@ollivander.com', - homeCom.communityUuid, - ) - }) }) }) @@ -477,8 +456,6 @@ describe('send coins', () => { }) it('has wait till sendTransactionsToDltConnector created all dlt-transactions', () => { - expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...') - expect(dltTransactions).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index b842a2d34..67dcc4432 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -1,10 +1,12 @@ import { AppDatabase, + countOpenPendingTransactions, Community as DbCommunity, PendingTransaction as DbPendingTransaction, Transaction as dbTransaction, TransactionLink as dbTransactionLink, User as dbUser, + findUserByIdentifier } from 'database' import { Decimal } from 'decimal.js-light' import { Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql' @@ -13,7 +15,7 @@ import { In, IsNull } from 'typeorm' import { Paginated } from '@arg/Paginated' import { TransactionSendArgs } from '@arg/TransactionSendArgs' import { Order } from '@enum/Order' -import { PendingTransactionState } from '@enum/PendingTransactionState' +import { PendingTransactionState } from 'shared' import { TransactionTypeId } from '@enum/TransactionTypeId' import { Transaction } from '@model/Transaction' import { TransactionList } from '@model/TransactionList' @@ -29,17 +31,17 @@ import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Event import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' import { LogError } from '@/server/LogError' import { Context, getUser } from '@/server/context' -import { backendLogger as logger } from '@/server/logger' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { communityUser } from '@/util/communityUser' import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' import { virtualDecayTransaction, virtualLinkTransaction } from '@/util/virtualTransactions' +import { Logger, getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { BalanceResolver } from './BalanceResolver' import { GdtResolver } from './GdtResolver' import { getCommunityByIdentifier, getCommunityName, isHomeCommunity } from './util/communities' -import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' import { @@ -51,32 +53,23 @@ import { storeForeignUser } from './util/storeForeignUser' import { transactionLinkSummary } from './util/transactionLinkSummary' const db = AppDatabase.getInstance() +const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionResolver`) export const executeTransaction = async ( amount: Decimal, memo: string, sender: dbUser, recipient: dbUser, + logger: Logger, transactionLink?: dbTransactionLink | null, ): Promise => { // acquire lock const releaseLock = await TRANSACTIONS_LOCK.acquire() + try { logger.info('executeTransaction', amount, memo, sender, recipient) - const openSenderPendingTx = await DbPendingTransaction.count({ - where: [ - { userGradidoID: sender.gradidoID, state: PendingTransactionState.NEW }, - { linkedUserGradidoID: sender.gradidoID, state: PendingTransactionState.NEW }, - ], - }) - const openReceiverPendingTx = await DbPendingTransaction.count({ - where: [ - { userGradidoID: recipient.gradidoID, state: PendingTransactionState.NEW }, - { linkedUserGradidoID: recipient.gradidoID, state: PendingTransactionState.NEW }, - ], - }) - if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) { + if (await countOpenPendingTransactions([sender.gradidoID, recipient.gradidoID]) > 0) { throw new LogError( `There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`, ) @@ -228,9 +221,9 @@ export class TransactionResolver { ): Promise { const now = new Date() const user = getUser(context) - + const logger = createLogger() logger.addContext('user', user.id) - logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`) + logger.info(`transactionList`) let balanceGDTPromise: Promise = Promise.resolve(null) if (CONFIG.GDT_ACTIVE) { @@ -240,7 +233,7 @@ export class TransactionResolver { // find current balance const lastTransaction = await getLastTransaction(user.id) - logger.debug(`lastTransaction=${lastTransaction}`) + logger.debug(`lastTransaction=${lastTransaction?.id}`) const balanceResolver = new BalanceResolver() context.lastTransaction = lastTransaction @@ -288,10 +281,10 @@ export class TransactionResolver { }, ], }) - logger.debug('found dbRemoteUser:', dbRemoteUser) + logger.debug(`found dbRemoteUser: ${dbRemoteUser?.id}`) const remoteUser = new User(dbRemoteUser) if (dbRemoteUser === null) { - logger.debug('no dbRemoteUser found, init from tx:', transaction) + logger.debug(`no dbRemoteUser found, init from tx: ${transaction.id}`) if (transaction.linkedUserCommunityUuid !== null) { remoteUser.communityUuid = transaction.linkedUserCommunityUuid } @@ -312,7 +305,10 @@ export class TransactionResolver { } } logger.debug(`involvedUserIds=`, involvedUserIds) - logger.debug(`involvedRemoteUsers=`, involvedRemoteUsers) + logger.debug( + `involvedRemoteUsers=`, + involvedRemoteUsers.map((u) => u.id), + ) // We need to show the name for deleted users for old transactions const involvedDbUsers = await dbUser.find({ @@ -321,7 +317,10 @@ export class TransactionResolver { relations: ['emailContact'], }) const involvedUsers = involvedDbUsers.map((u) => new User(u)) - logger.debug(`involvedUsers=`, involvedUsers) + logger.debug( + `involvedUsers=`, + involvedUsers.map((u) => u.id), + ) const self = new User(user) const transactions: Transaction[] = [] @@ -332,11 +331,11 @@ export class TransactionResolver { context.linkCount = transactionLinkcount logger.debug(`transactionLinkcount=${transactionLinkcount}`) context.sumHoldAvailableAmount = sumHoldAvailableAmount - logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`) + logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount.toString()}`) // decay & link transactions if (currentPage === 1 && order === Order.DESC) { - logger.debug(`currentPage == 1: transactions=${transactions}`) + logger.debug(`currentPage == 1: transactions=${transactions.map((t) => t.id)}`) // The virtual decay is always on the booked amount, not including the generated, not yet booked links, // since the decay is substantially different when the amount is less transactions.push( @@ -348,7 +347,7 @@ export class TransactionResolver { sumHoldAvailableAmount, ), ) - logger.debug(`transactions=${transactions}`) + logger.debug(`transactions=${transactions.map((t) => t.id)}`) // virtual transaction for pending transaction-links sum if (sumHoldAvailableAmount.isZero()) { @@ -373,7 +372,7 @@ export class TransactionResolver { ) } } else if (sumHoldAvailableAmount.greaterThan(0)) { - logger.debug(`sumHoldAvailableAmount > 0: transactions=${transactions}`) + logger.debug(`sumHoldAvailableAmount > 0: transactions=${transactions.map((t) => t.id)}`) transactions.push( virtualLinkTransaction( lastTransaction.balance.minus(sumHoldAvailableAmount.toString()), @@ -386,7 +385,7 @@ export class TransactionResolver { (userTransactions.length && userTransactions[0].balance) || new Decimal(0), ), ) - logger.debug(`transactions=`, transactions) + logger.debug(`transactions=${transactions.map((t) => t.id)}`) } } @@ -401,19 +400,22 @@ export class TransactionResolver { let linkedUser: User | undefined if ((userTransaction.typeId as TransactionTypeId) === TransactionTypeId.CREATION) { linkedUser = communityUser - logger.debug('CREATION-linkedUser=', linkedUser) + logger.debug(`CREATION-linkedUser=${linkedUser.id}`) } else if (userTransaction.linkedUserId) { linkedUser = involvedUsers.find((u) => u.id === userTransaction.linkedUserId) - logger.debug('local linkedUser=', linkedUser) + logger.debug(`local linkedUser=${linkedUser?.id}`) } else if (userTransaction.linkedUserCommunityUuid) { linkedUser = involvedRemoteUsers.find( (u) => u.gradidoID === userTransaction.linkedUserGradidoID, ) - logger.debug('remote linkedUser=', linkedUser) + logger.debug(`remote linkedUser=${linkedUser?.id}`) } transactions.push(new Transaction(userTransaction, self, linkedUser)) }) - logger.debug(`TransactionTypeId.CREATION: transactions=`, transactions) + logger.debug( + `TransactionTypeId.CREATION: transactions=`, + transactions.map((t) => t.id), + ) transactions.forEach((transaction: Transaction) => { if (transaction.typeId !== TransactionTypeId.DECAY) { @@ -439,6 +441,9 @@ export class TransactionResolver { { recipientCommunityIdentifier, recipientIdentifier, amount, memo }: TransactionSendArgs, @Ctx() context: Context, ): Promise { + const logger = createLogger() + logger.addContext('from', context.user?.id) + logger.addContext('amount', amount.toString()) logger.debug( `sendCoins(recipientCommunityIdentifier=${recipientCommunityIdentifier}, recipientIdentifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`, ) @@ -454,28 +459,28 @@ export class TransactionResolver { if (!recipientUser) { throw new LogError('The recipient user was not found', recipientUser) } + logger.addContext('to', recipientUser?.id) if (recipientUser.foreign) { throw new LogError('Found foreign recipient user for a local transaction:', recipientUser) } - await executeTransaction(amount, memo, senderUser, recipientUser) - logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser) + await executeTransaction(amount, memo, senderUser, recipientUser, logger) + logger.info('successful executeTransaction') } else { // processing a x-community sendCoins - logger.debug('X-Com: processing a x-community transaction...') + logger.info('X-Com: processing a x-community transaction...') if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) { throw new LogError('X-Community sendCoins disabled per configuration!') } const recipCom = await getCommunityByIdentifier(recipientCommunityIdentifier) - logger.debug('recipient commuity: ', recipCom) + logger.debug('recipient community: ', recipCom?.id) if (recipCom === null) { throw new LogError( - 'no recipient commuity found for identifier:', - recipientCommunityIdentifier, + `no recipient community found for identifier: ${recipientCommunityIdentifier}`, ) } if (recipCom !== null && recipCom.authenticatedAt === null) { - throw new LogError('recipient commuity is connected, but still not authenticated yet!') + throw new LogError('recipient community is connected, but still not authenticated yet!') } let pendingResult: SendCoinsResult let committingResult: SendCoinsResult diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index e6c38c442..bf2cb1f59 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -20,7 +20,7 @@ import { UserContactType } from '@enum/UserContactType' import { ContributionLink } from '@model/ContributionLink' import { Location } from '@model/Location' import { cleanDB, headerPushMock, resetToken, testEnvironment } from '@test/helpers' -import { i18n as localization, logger } from '@test/testSetup' +import { i18n as localization } from '@test/testSetup' import { subscribe } from '@/apis/KlicktippController' import { CONFIG } from '@/config' @@ -67,6 +67,8 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking' import { printTimeDuration } from '@/util/time' import { objectValuesToArray } from '@/util/utilities' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'config-schema/test/testSetup' import { Location2Point } from './util/Location2Point' jest.mock('@/apis/humhub/HumHubClient') @@ -93,6 +95,9 @@ jest.mock('@/apis/KlicktippController', () => { } }) +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.UserResolver`) +const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) + CONFIG.EMAIL_CODE_REQUEST_TIME = 10 let admin: User @@ -107,7 +112,7 @@ let testEnv: { } beforeAll(async () => { - testEnv = await testEnvironment(logger, localization) + testEnv = await testEnvironment(getLogger('apollo'), localization) mutate = testEnv.mutate query = testEnv.query con = testEnv.con @@ -275,7 +280,8 @@ describe('UserResolver', () => { }) it('logs an info', () => { - expect(logger.info).toBeCalledWith('User already exists with this email=peter@lustig.de') + expect(logger.info).toBeCalledWith('User already exists') + expect(logger.addContext).toBeCalledWith('user', user[0].id) }) it('sends an account multi registration email', () => { @@ -642,7 +648,7 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( + expect(logErrorLogger.error).toBeCalledWith( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', ) }) @@ -672,7 +678,7 @@ describe('UserResolver', () => { }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith('Could not login with emailVerificationCode') + expect(logger.warn).toBeCalledWith('invalid emailVerificationCode=not valid') }) }) }) @@ -693,7 +699,8 @@ describe('UserResolver', () => { describe('no users in database', () => { it('throws an error', async () => { jest.clearAllMocks() - expect(await mutate({ mutation: login, variables })).toEqual( + const result = await mutate({ mutation: login, variables }) + expect(result).toEqual( expect.objectContaining({ errors: [new GraphQLError('No user with this credentials')], }), @@ -701,7 +708,9 @@ describe('UserResolver', () => { }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith('No user with this credentials', variables.email) + expect(logger.warn).toBeCalledWith( + `findUserByEmail failed, user with email=${variables.email} not found`, + ) }) }) @@ -782,8 +791,8 @@ describe('UserResolver', () => { ) }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('No user with this credentials', variables.email) + it('logs warning before error is thrown', () => { + expect(logger.warn).toBeCalledWith('login failed, wrong password') }) }) @@ -813,14 +822,8 @@ describe('UserResolver', () => { ) }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'This user was permanently deleted. Contact support for questions', - expect.objectContaining({ - firstName: stephenHawking.firstName, - lastName: stephenHawking.lastName, - }), - ) + it('logs warning before error is thrown', () => { + expect(logger.warn).toBeCalledWith('login failed, user was deleted') }) }) @@ -848,14 +851,8 @@ describe('UserResolver', () => { ) }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'The Users email is not validate yet', - expect.objectContaining({ - firstName: garrickOllivander.firstName, - lastName: garrickOllivander.lastName, - }), - ) + it('logs warning before error is thrown', () => { + expect(logger.warn).toBeCalledWith('login failed, user email not checked') }) }) @@ -881,14 +878,8 @@ describe('UserResolver', () => { ) }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'The User has not set a password yet', - expect.objectContaining({ - firstName: bibiBloxberg.firstName, - lastName: bibiBloxberg.lastName, - }), - ) + it('logs warning before error is thrown', () => { + expect(logger.warn).toBeCalledWith('login failed, user has not set a password yet') }) }) }) @@ -1114,7 +1105,7 @@ describe('UserResolver', () => { }) describe('request reset password again', () => { - it('thows an error', async () => { + it('throws an error', async () => { CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( expect.objectContaining({ @@ -1123,8 +1114,10 @@ describe('UserResolver', () => { ) }) - it('logs the error found', () => { - expect(logger.error).toBeCalledWith(`Email already sent less than 10 minutes ago`) + it('logs warning before throwing error', () => { + expect(logger.warn).toBeCalledWith( + 'email already sent 0 minutes ago, min wait time: 10 minutes', + ) }) }) }) @@ -1374,13 +1367,13 @@ describe('UserResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('Given language is not a valid language')], + errors: [new GraphQLError('Given language is not a valid language or not supported')], }), ) }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith('Given language is not a valid language', 'not-valid') + expect(logger.warn).toBeCalledWith('try to set unsupported language', 'not-valid') }) }) @@ -1403,8 +1396,8 @@ describe('UserResolver', () => { ) }) - it('logs the error found', () => { - expect(logger.error).toBeCalledWith(`Old password is invalid`) + it('logs if logger is in debug mode', () => { + expect(logger.debug).toBeCalledWith(`old password is invalid`) }) }) @@ -1430,10 +1423,8 @@ describe('UserResolver', () => { ) }) - it('logs the error found', () => { - expect(logger.error).toBeCalledWith( - 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', - ) + it('logs warning', () => { + expect(logger.warn).toBeCalledWith('try to set invalid password') }) }) @@ -1490,10 +1481,8 @@ describe('UserResolver', () => { ) }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', - ) + it('log warning', () => { + expect(logger.warn).toBeCalledWith('login failed, wrong password') }) }) }) @@ -1776,7 +1765,10 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Could not find user with given ID', admin.id + 1) + expect(logErrorLogger.error).toBeCalledWith( + 'Could not find user with given ID', + admin.id + 1, + ) }) }) @@ -1892,7 +1884,9 @@ describe('UserResolver', () => { ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Administrator can not change his own role') + expect(logErrorLogger.error).toBeCalledWith( + 'Administrator can not change his own role', + ) }) }) @@ -1937,7 +1931,10 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('User already has role=', RoleNames.ADMIN) + expect(logErrorLogger.error).toBeCalledWith( + 'User already has role=', + RoleNames.ADMIN, + ) }) }) @@ -1961,7 +1958,10 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('User already has role=', RoleNames.MODERATOR) + expect(logErrorLogger.error).toBeCalledWith( + 'User already has role=', + RoleNames.MODERATOR, + ) }) }) @@ -1982,7 +1982,7 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('User is already an usual user') + expect(logErrorLogger.error).toBeCalledWith('User is already an usual user') }) }) }) @@ -2055,7 +2055,10 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Could not find user with given ID', admin.id + 1) + expect(logErrorLogger.error).toBeCalledWith( + 'Could not find user with given ID', + admin.id + 1, + ) }) }) @@ -2072,7 +2075,7 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Moderator can not delete his own account') + expect(logErrorLogger.error).toBeCalledWith('Moderator can not delete his own account') }) }) @@ -2125,7 +2128,10 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Could not find user with given ID', user.id) + expect(logErrorLogger.error).toBeCalledWith( + 'Could not find user with given ID', + user.id, + ) }) }) }) @@ -2201,7 +2207,9 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('No user with this credentials', 'invalid') + expect(logger.warn).toBeCalledWith( + 'findUserByEmail failed, user with email=invalid not found', + ) }) }) @@ -2218,11 +2226,8 @@ describe('UserResolver', () => { ) }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'User with given email contact is deleted', - 'stephen@hawking.uk', - ) + it('log warning', () => { + expect(logger.warn).toBeCalledWith('call for activation of deleted user') }) }) @@ -2348,7 +2353,10 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Could not find user with given ID', admin.id + 1) + expect(logErrorLogger.error).toBeCalledWith( + 'Could not find user with given ID', + admin.id + 1, + ) }) }) @@ -2369,7 +2377,7 @@ describe('UserResolver', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('User is not deleted') + expect(logErrorLogger.error).toBeCalledWith('User is not deleted') }) describe('undelete deleted user', () => { @@ -2682,167 +2690,7 @@ describe('UserResolver', () => { errors: [new GraphQLError('401 Unauthorized')], }), ) - expect(logger.error).toBeCalledWith('401 Unauthorized') - }) - }) - - describe('authenticated', () => { - const uuid = uuidv4() - - beforeAll(async () => { - user = await userFactory(testEnv, bibiBloxberg) - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - // first set alias to null, because updating alias isn't currently allowed - await User.update({ alias: 'BBB' }, { alias: () => 'NULL' }) - await mutate({ - mutation: updateUserInfos, - variables: { - alias: 'bibi', - }, - }) - }) - - describe('identifier is no gradido ID, no email and no alias', () => { - it('throws and logs "Unknown identifier type" error', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: 'identifier_is_no_valid_alias!', - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Unknown identifier type')], - }), - ) - expect(logger.error).toBeCalledWith( - 'Unknown identifier type', - 'identifier_is_no_valid_alias!', - ) - }) - }) - - describe('identifier is not found', () => { - it('throws and logs "No user found to given identifier" error', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: uuid, - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('No user found to given identifier(s)')], - }), - ) - expect(logger.error).toBeCalledWith( - 'No user found to given identifier(s)', - uuid, - homeCom1.communityUuid, - ) - }) - }) - - describe('identifier is found via email, but not matching community', () => { - it('returns user', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: 'bibi@bloxberg.de', - communityIdentifier: foreignCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('No user with this credentials')], - }), - ) - expect(logger.error).toBeCalledWith( - 'No user with this credentials', - 'bibi@bloxberg.de', - foreignCom1.communityUuid, - ) - }) - }) - - describe('identifier is found via email', () => { - it('returns user', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: 'bibi@bloxberg.de', - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - user: expect.objectContaining({ - firstName: 'Bibi', - lastName: 'Bloxberg', - }), - }, - errors: undefined, - }), - ) - }) - }) - - describe('identifier is found via gradidoID', () => { - it('returns user', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: user.gradidoID, - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - user: expect.objectContaining({ - firstName: 'Bibi', - lastName: 'Bloxberg', - }), - }, - errors: undefined, - }), - ) - }) - }) - - describe('identifier is found via alias', () => { - it('returns user', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: 'bibi', - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - user: expect.objectContaining({ - firstName: 'Bibi', - lastName: 'Bloxberg', - }), - }, - errors: undefined, - }), - ) - }) + expect(logErrorLogger.error).toBeCalledWith('401 Unauthorized') }) }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index ee83937f0..dbfad91ef 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -6,6 +6,8 @@ import { UserContact as DbUserContact, ProjectBranding, UserLoggingView, + getHomeCommunity, + findUserByIdentifier } from 'database' import { GraphQLResolveInfo } from 'graphql' import i18n from 'i18n' @@ -23,7 +25,7 @@ import { Root, } from 'type-graphql' import { IRestResponse } from 'typed-rest-client' -import { In, Point } from 'typeorm' +import { EntityNotFoundError, In, Point } from 'typeorm' import { v4 as uuidv4 } from 'uuid' import { UserArgs } from '@arg//UserArgs' @@ -80,38 +82,38 @@ import { isValidPassword } from '@/password/EncryptorUtils' import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { LogError } from '@/server/LogError' import { Context, getClientTimezoneOffset, getUser } from '@/server/context' -import { backendLogger as logger } from '@/server/logger' import { communityDbUser } from '@/util/communityUser' import { hasElopageBuys } from '@/util/hasElopageBuys' -import { getTimeDurationObject, printTimeDuration } from '@/util/time' +import { durationInMinutesFromDates, getTimeDurationObject, printTimeDuration } from '@/util/time' import { delay } from '@/util/utilities' import random from 'random-bigint' import { randombytes_random } from 'sodium-native' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { Logger, getLogger } from 'log4js' import { FULL_CREATION_AVAILABLE } from './const/const' import { Location2Point, Point2Location } from './util/Location2Point' import { authenticateGmsUserPlayground } from './util/authenticateGmsUserPlayground' -import { getHomeCommunity } from './util/communities' import { compareGmsRelevantUserSettings } from './util/compareGmsRelevantUserSettings' import { getUserCreations } from './util/creations' import { extractGraphQLFieldsForSelect } from './util/extractGraphQLFields' -import { findUserByIdentifier } from './util/findUserByIdentifier' import { findUsers } from './util/findUsers' import { getKlicktippState } from './util/getKlicktippState' import { deleteUserRole, setUserRole } from './util/modifyUserRole' import { sendUserToGms } from './util/sendUserToGms' import { syncHumhub } from './util/syncHumhub' -import { validateAlias } from './util/validateAlias' +import { validateAlias } from 'core' const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] const DEFAULT_LANGUAGE = 'de' const db = AppDatabase.getInstance() +const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.UserResolver`) const isLanguage = (language: string): boolean => { return LANGUAGES.includes(language) } -const newEmailContact = (email: string, userId: number): DbUserContact => { +const newEmailContact = (email: string, userId: number, logger: Logger): DbUserContact => { logger.trace(`newEmailContact...`) const emailContact = new DbUserContact() emailContact.email = email @@ -124,12 +126,12 @@ const newEmailContact = (email: string, userId: number): DbUserContact => { return emailContact } -export const activationLink = (verificationCode: string): string => { +export const activationLink = (verificationCode: string, logger: Logger): string => { logger.debug(`activationLink(${verificationCode})...`) return CONFIG.EMAIL_LINK_SETPASSWORD + verificationCode.toString() } -const newGradidoID = async (): Promise => { +const newGradidoID = async (logger: Logger): Promise => { let gradidoId: string let countIds: number do { @@ -147,14 +149,16 @@ export class UserResolver { @Authorized([RIGHTS.VERIFY_LOGIN]) @Query(() => User) async verifyLogin(@Ctx() context: Context): Promise { + const logger = createLogger() logger.info('verifyLogin...') // TODO refactor and do not have duplicate code with login(see below) const userEntity = getUser(context) + logger.addContext('user', userEntity.id) const user = new User(userEntity) // Elopage Status & Stored PublisherId user.hasElopage = await this.hasElopage(context) - logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}`) + logger.debug(`verifyLogin... successful`) user.klickTipp = await getKlicktippState(userEntity.emailContact.email) return user } @@ -165,31 +169,38 @@ export class UserResolver { @Args() { email, password, publisherId, project }: UnsecureLoginArgs, @Ctx() context: Context, ): Promise { - logger.info(`login with ${email}, ***, ${publisherId}, project=${project} ...`) + const logger = createLogger() + logger.info(`login with ${email.substring(0, 3)}..., project=${project} ...`) email = email.trim().toLowerCase() let dbUser: DbUser try { dbUser = await findUserByEmail(email) + // add technical user identifier in logger-context for layout-pattern X{user} to print it in each logging message + logger.addContext('user', dbUser.id) + logger.trace('user before login', new UserLoggingView(dbUser)) } catch (e) { // simulate delay which occur on password encryption 650 ms +- 50 rnd await delay(650 + Math.floor(Math.random() * 101) - 50) throw e } - if (dbUser.deletedAt) { - throw new LogError('This user was permanently deleted. Contact support for questions', dbUser) + logger.warn('login failed, user was deleted') + throw new Error('This user was permanently deleted. Contact support for questions') } if (!dbUser.emailContact.emailChecked) { - throw new LogError('The Users email is not validate yet', dbUser) + logger.warn('login failed, user email not checked') + throw new Error('The Users email is not validate yet') } // TODO: at least in test this does not work since `dbUser.password = 0` and `BigInto(0) = 0n` if (dbUser.password === BigInt(0)) { // TODO we want to catch this on the frontend and ask the user to check his emails or resend code - throw new LogError('The User has not set a password yet', dbUser) + logger.warn('login failed, user has not set a password yet') + throw new Error('The User has not set a password yet') } if (!(await verifyPassword(dbUser, password))) { - throw new LogError('No user with this credentials', dbUser) + logger.warn('login failed, wrong password') + throw new Error('No user with this credentials') } // request to humhub and klicktipp run in parallel @@ -217,17 +228,14 @@ export class UserResolver { dbUser.password = await encryptPassword(dbUser, password) await dbUser.save() } - // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message - logger.addContext('user', dbUser.id) logger.debug('validation of login credentials successful...') const user = new User(dbUser) - logger.debug(`user= ${JSON.stringify(user, null, 2)}`) i18n.setLocale(user.language) // Elopage Status & Stored PublisherId user.hasElopage = await this.hasElopage({ ...context, user: dbUser }) - logger.info('user.hasElopage', user.hasElopage) + logger.debug('user.hasElopage', user.hasElopage) if (!user.hasElopage && publisherId) { user.publisherId = publisherId dbUser.publisherId = publisherId @@ -241,7 +249,7 @@ export class UserResolver { await EVENT_USER_LOGIN(dbUser) const projectBranding = await projectBrandingPromise - logger.debug('project branding: ', projectBranding) + logger.debug('project branding: ', projectBranding?.id) // load humhub state if (humhubUserPromise) { try { @@ -260,7 +268,8 @@ export class UserResolver { } } user.klickTipp = await klicktippStatePromise - logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`) + logger.info('successful Login') + logger.trace('user after login', new UserLoggingView(dbUser)) return user } @@ -268,8 +277,6 @@ export class UserResolver { @Mutation(() => Boolean) async logout(@Ctx() context: Context): Promise { await EVENT_USER_LOGOUT(getUser(context)) - // remove user from logger context - logger.addContext('user', 'unknown') return true } @@ -288,10 +295,24 @@ export class UserResolver { project = null, }: CreateUserArgs, ): Promise { - logger.addContext('user', 'unknown') - logger.info( - `createUser(email=${email}, firstName=${firstName}, lastName=${lastName}, language=${language}, publisherId=${publisherId}, redeemCode=${redeemCode}, project=${project})`, - ) + const logger = createLogger() + const shortEmail = email.substring(0, 3) + logger.addContext('email', shortEmail) + + const shortRedeemCode = redeemCode?.substring(0, 6) + const infos = [] + infos.push(`language=${language}`) + if (publisherId) { + infos.push(`publisherId=${publisherId}`) + } + if (redeemCode) { + infos.push(`redeemCode=${shortRedeemCode}`) + } + if (project) { + infos.push(`project=${project}`) + } + logger.info(`createUser(${infos.join(', ')})`) + // TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // default int publisher_id = 0; @@ -305,15 +326,16 @@ export class UserResolver { email = email.trim().toLowerCase() if (await checkEmailExists(email)) { const foundUser = await findUserByEmail(email) - logger.info('DbUser.findOne', email, foundUser) + logger.info('DbUser.findOne', foundUser.id) if (foundUser) { + logger.addContext('user', foundUser.id) + logger.removeContext('email') // ATTENTION: this logger-message will be exactly expected during tests, next line - logger.info(`User already exists with this email=${email}`) + logger.info(`User already exists`) logger.info( - `Specified username when trying to register multiple times with this email: firstName=${firstName}, lastName=${lastName}`, + `Specified username when trying to register multiple times with this email: firstName=${firstName.substring(0, 4)}, lastName=${lastName.substring(0, 4)}`, ) - // TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent. const user = new User(communityDbUser) user.id = randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in? @@ -325,7 +347,7 @@ export class UserResolver { if (alias && (await validateAlias(alias))) { user.alias = alias } - logger.debug('partly faked user', user) + logger.debug('partly faked user', { id: user.id, gradidoID: user.gradidoID }) await sendAccountMultiRegistrationEmail({ firstName: foundUser.firstName, // this is the real name of the email owner, but just "firstName" would be the name of the new registrant which shall not be passed to the outside @@ -335,9 +357,6 @@ export class UserResolver { }) await EVENT_EMAIL_ACCOUNT_MULTIREGISTRATION(foundUser) - logger.info( - `sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`, - ) /* uncomment this, when you need the activation link on the console */ // In case EMails are disabled log the activation link for the user logger.info('createUser() faked and send multi registration mail...') @@ -352,7 +371,7 @@ export class UserResolver { select: { logoUrl: true, spaceId: true }, }) } - const gradidoID = await newGradidoID() + const gradidoID = await newGradidoID(logger) const eventRegisterRedeem = Event( EventType.USER_REGISTER_REDEEM, @@ -361,6 +380,10 @@ export class UserResolver { ) let dbUser = new DbUser() const homeCom = await getHomeCommunity() + if (!homeCom) { + logger.error('no home community found, please start the dht-node first') + throw new Error(`Error creating user, please write the support team: ${CONFIG.COMMUNITY_SUPPORT_MAIL}`) + } if (homeCom.communityUuid) { dbUser.communityUuid = homeCom.communityUuid } @@ -375,21 +398,21 @@ export class UserResolver { } dbUser.publisherId = publisherId ?? 0 dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD - logger.debug('new dbUser', dbUser) + logger.debug('new dbUser', new UserLoggingView(dbUser)) if (redeemCode) { if (redeemCode.match(/^CL-/)) { const contributionLink = await DbContributionLink.findOne({ where: { code: redeemCode.replace('CL-', '') }, }) - logger.info('redeemCode found contributionLink', contributionLink) if (contributionLink) { + logger.info('redeemCode found contributionLink', contributionLink.id) dbUser.contributionLinkId = contributionLink.id eventRegisterRedeem.involvedContributionLink = contributionLink } } else { const transactionLink = await DbTransactionLink.findOne({ where: { code: redeemCode } }) - logger.info('redeemCode found transactionLink', transactionLink) if (transactionLink) { + logger.info('redeemCode found transactionLink', transactionLink.id) dbUser.referrerId = transactionLink.userId eventRegisterRedeem.involvedTransactionLink = transactionLink } @@ -404,7 +427,7 @@ export class UserResolver { dbUser = await queryRunner.manager.save(dbUser).catch((error) => { throw new LogError('Error while saving dbUser', error) }) - let emailContact = newEmailContact(email, dbUser.id) + let emailContact = newEmailContact(email, dbUser.id, logger) emailContact = await queryRunner.manager.save(emailContact).catch((error) => { throw new LogError('Error while saving user email contact', error) }) @@ -431,7 +454,7 @@ export class UserResolver { timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), logoUrl: projectBranding?.logoUrl, }) - logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`) + logger.info('sendAccountActivationEmail') await EVENT_EMAIL_CONFIRMATION(dbUser) @@ -485,18 +508,33 @@ export class UserResolver { @Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL]) @Mutation(() => Boolean) async forgotPassword(@Arg('email') email: string): Promise { - logger.addContext('user', 'unknown') - logger.info(`forgotPassword(${email})...`) + const logger = createLogger() + const shortEmail = email.substring(0, 3) + logger.addContext('email', shortEmail) + logger.info('forgotPassword...') email = email.trim().toLowerCase() - const user = await findUserByEmail(email).catch((error) => { - logger.warn(`fail on find UserContact per ${email} because: ${error}`) - }) + let user: DbUser + try { + user = await findUserByEmail(email) + logger.removeContext('email') + logger.addContext('user', user.id) + } catch (_e) { + logger.warn(`fail on find UserContact`) + return true + } - if (!user || user.deletedAt) { - logger.warn(`no user found with ${email}`) + if (user.deletedAt) { + logger.warn(`user was deleted`) return true } if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) { + const diff = durationInMinutesFromDates( + user.emailContact.updatedAt || user.emailContact.createdAt, + new Date(), + ) + logger.warn( + `email already sent ${printTimeDuration(diff)} ago, min wait time: ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)}`, + ) throw new LogError( `Email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} ago`, ) @@ -507,21 +545,19 @@ export class UserResolver { user.emailContact.emailVerificationCode = random(64).toString() user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD await user.emailContact.save().catch(() => { - throw new LogError('Unable to save email verification code', user.emailContact) + throw new LogError('Unable to save email verification code', user.emailContact.id) }) - logger.info('optInCode for', email, user.emailContact) - await sendResetPasswordEmail({ firstName: user.firstName, lastName: user.lastName, email, language: user.language, - resetLink: activationLink(user.emailContact.emailVerificationCode), + resetLink: activationLink(user.emailContact.emailVerificationCode, logger), timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) - logger.info(`forgotPassword(${email}) successful...`) + logger.info(`forgotPassword successful...`) await EVENT_EMAIL_FORGOT_PASSWORD(user) return true @@ -533,7 +569,8 @@ export class UserResolver { @Arg('code') code: string, @Arg('password') password: string, ): Promise { - logger.info(`setPassword(${code}, ***)...`) + const logger = createLogger() + logger.info(`setPassword...`) // Validate Password if (!isValidPassword(password)) { throw new LogError( @@ -545,8 +582,11 @@ export class UserResolver { where: { emailVerificationCode: code }, relations: ['user'], }).catch(() => { - throw new LogError('Could not login with emailVerificationCode') + // code wasn't in db, so we can write it into log without hesitation + logger.warn(`invalid emailVerificationCode=${code}`) + throw new Error('Could not login with emailVerificationCode') }) + logger.addContext('user', userContact.user.id) logger.debug('userContact loaded...') // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) { @@ -596,9 +636,7 @@ export class UserResolver { if ((userContact.emailOptInTypeId as OptInType) === OptInType.EMAIL_OPT_IN_REGISTER) { try { await subscribe(userContact.email, user.language, user.firstName, user.lastName) - logger.debug( - `subscribe(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`, - ) + logger.debug('Success subscribe to klicktipp') } catch (e) { logger.error('Error subscribing to klicktipp', e) } @@ -611,18 +649,21 @@ export class UserResolver { @Authorized([RIGHTS.QUERY_OPT_IN]) @Query(() => Boolean) async queryOptIn(@Arg('optIn') optIn: string): Promise { - logger.info(`queryOptIn(${optIn})...`) + const logger = createLogger() + logger.addContext('optIn', optIn.substring(0, 4)) + logger.info(`queryOptIn...`) const userContact = await DbUserContact.findOneOrFail({ where: { emailVerificationCode: optIn }, }) - logger.debug('found optInCode', userContact) + logger.addContext('user', userContact.userId) + logger.debug('found optInCode', userContact.id) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) { throw new LogError( `Email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, ) } - logger.info(`queryOptIn(${optIn}) successful...`) + logger.info(`queryOptIn successful...`) return true } @@ -659,10 +700,27 @@ export class UserResolver { gmsLocation, gmsPublishLocation, } = updateUserInfosArgs - logger.info( - `updateUserInfos(${firstName}, ${lastName}, ${alias}, ${language}, ***, ***, ${hideAmountGDD}, ${hideAmountGDT}, ${gmsAllowed}, ${gmsPublishName}, ${gmsLocation}, ${gmsPublishLocation})...`, - ) const user = getUser(context) + const logger = createLogger() + logger.addContext('user', user.id) + // log only if a value is set + logger.info(`updateUserInfos...`, { + firstName: firstName !== undefined, + lastName: lastName !== undefined, + alias: alias !== undefined, + language: language !== undefined, + password: password !== undefined, + passwordNew: passwordNew !== undefined, + hideAmountGDD: hideAmountGDD !== undefined, + hideAmountGDT: hideAmountGDT !== undefined, + humhubAllowed: humhubAllowed !== undefined, + gmsAllowed: gmsAllowed !== undefined, + gmsPublishName: gmsPublishName !== undefined, + humhubPublishName: humhubPublishName !== undefined, + gmsLocation: gmsLocation !== undefined, + gmsPublishLocation: gmsPublishLocation !== undefined, + }) + const updateUserInGMS = compareGmsRelevantUserSettings(user, updateUserInfosArgs) const publishNameLogic = new PublishNameLogic(user) const oldHumhubUsername = publishNameLogic.getUserIdentifier( @@ -685,7 +743,8 @@ export class UserResolver { if (language) { if (!isLanguage(language)) { - throw new LogError('Given language is not a valid language', language) + logger.warn('try to set unsupported language', language) + throw new LogError('Given language is not a valid language or not supported') } user.language = language i18n.setLocale(language) @@ -694,12 +753,15 @@ export class UserResolver { if (password && passwordNew) { // Validate Password if (!isValidPassword(passwordNew)) { - throw new LogError( + // TODO: log which rule(s) wasn't met + logger.warn('try to set invalid password') + throw new Error( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', ) } if (!(await verifyPassword(user, password))) { + logger.debug('old password is invalid') throw new LogError(`Old password is invalid`) } @@ -762,8 +824,14 @@ export class UserResolver { if (CONFIG.GMS_ACTIVE && updateUserInGMS) { logger.debug(`changed user-settings relevant for gms-user update...`) const homeCom = await getHomeCommunity() + if (!homeCom) { + logger.error('no home community found, please start the dht-node first') + throw new Error( + `Error updating user, please write the support team: ${CONFIG.COMMUNITY_SUPPORT_MAIL}` + ) + } if (homeCom.gmsApiKey !== null) { - logger.debug(`send User to Gms...`, user) + logger.debug(`send User to Gms...`) await sendUserToGms(user, homeCom) logger.debug(`sendUserToGms successfully.`) } @@ -785,21 +853,31 @@ export class UserResolver { @Authorized([RIGHTS.HAS_ELOPAGE]) @Query(() => Boolean) async hasElopage(@Ctx() context: Context): Promise { - logger.info(`hasElopage()...`) - const userEntity = getUser(context) - const elopageBuys = hasElopageBuys(userEntity.emailContact.email) - logger.debug('has ElopageBuys', elopageBuys) + const dbUser = getUser(context) + const logger = createLogger() + logger.addContext('user', dbUser.id) + const elopageBuys = await hasElopageBuys(dbUser.emailContact.email) + logger.info(`has Elopage (ablify): ${elopageBuys}`) return elopageBuys } @Authorized([RIGHTS.GMS_USER_PLAYGROUND]) @Query(() => GmsUserAuthenticationResult) async authenticateGmsUserSearch(@Ctx() context: Context): Promise { - logger.info(`authenticateGmsUserSearch()...`) const dbUser = getUser(context) + const logger = createLogger() + logger.addContext('user', dbUser.id) + logger.info(`authenticateGmsUserSearch()...`) + let result = new GmsUserAuthenticationResult() if (context.token) { const homeCom = await getHomeCommunity() + if (!homeCom) { + logger.error("couldn't authenticate for gms, no home community found, please start the dht-node first") + throw new Error( + `Error authenticating for gms, please write the support team: ${CONFIG.COMMUNITY_SUPPORT_MAIL}` + ) + } if (!homeCom.gmsApiKey) { throw new LogError('authenticateGmsUserSearch missing HomeCommunity GmsApiKey') } @@ -815,11 +893,20 @@ export class UserResolver { @Authorized([RIGHTS.GMS_USER_PLAYGROUND]) @Query(() => UserLocationResult) async userLocation(@Ctx() context: Context): Promise { - logger.info(`userLocation()...`) const dbUser = getUser(context) + const logger = createLogger() + logger.addContext('user', dbUser.id) + logger.info(`userLocation()...`) + const result = new UserLocationResult() if (context.token) { const homeCom = await getHomeCommunity() + if (!homeCom) { + logger.error("couldn't load home community location, no home community found, please start the dht-node first") + throw new Error( + `Error loading user location, please write the support team: ${CONFIG.COMMUNITY_SUPPORT_MAIL}` + ) + } result.communityLocation = Point2Location(homeCom.location as Point) result.userLocation = Point2Location(dbUser.location as Point) logger.info('userLocation=', result) @@ -835,8 +922,11 @@ export class UserResolver { @Ctx() context: Context, @Arg('project', () => String, { nullable: true }) project?: string | null, ): Promise { - logger.info(`authenticateHumhubAutoLogin()...`) const dbUser = getUser(context) + const logger = createLogger() + logger.addContext('user', dbUser.id) + logger.info(`authenticateHumhubAutoLogin()...`) + const humhubClient = HumHubClient.getInstance() if (!humhubClient) { throw new LogError('cannot create humhub client') @@ -1029,13 +1119,15 @@ export class UserResolver { @Arg('email') email: string, @Ctx() context: Context, ): Promise { + const logger = createLogger() email = email.trim().toLowerCase() - // const user = await dbUser.findOne({ id: emailContact.userId }) const user = await findUserByEmail(email) + logger.addContext('user', user.id) + logger.info('sendActivationEmail...') if (user.deletedAt || user.emailContact.deletedAt) { - throw new LogError('User with given email contact is deleted', email) + logger.warn('call for activation of deleted user') + throw new Error('User with given email contact is deleted') } - user.emailContact.emailResendCount++ await user.emailContact.save() @@ -1044,7 +1136,7 @@ export class UserResolver { lastName: user.lastName, email, language: user.language, - activationLink: activationLink(user.emailContact.emailVerificationCode), + activationLink: activationLink(user.emailContact.emailVerificationCode, logger), timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) @@ -1060,8 +1152,11 @@ export class UserResolver { { identifier, communityIdentifier }: UserArgs, ): Promise { const foundDbUser = await findUserByIdentifier(identifier, communityIdentifier) - const modelUser = new User(foundDbUser) - return modelUser + if (!foundDbUser) { + createLogger().debug('User not found', identifier, communityIdentifier) + throw new Error('User not found') + } + return new User(foundDbUser) } // FIELD RESOLVERS @@ -1090,16 +1185,24 @@ export class UserResolver { } export async function findUserByEmail(email: string): Promise { - const dbUser = await DbUser.findOneOrFail({ - where: { - emailContact: { email }, - }, - withDeleted: true, - relations: { userRoles: true, emailContact: true }, - }).catch(() => { - throw new LogError('No user with this credentials', email) - }) - return dbUser + try { + const dbUser = await DbUser.findOneOrFail({ + where: { + emailContact: { email }, + }, + withDeleted: true, + relations: { userRoles: true, emailContact: true }, + }) + return dbUser + } catch (e) { + const logger = createLogger() + if (e instanceof EntityNotFoundError || (e as Error).name === 'EntityNotFoundError') { + logger.warn(`findUserByEmail failed, user with email=${email} not found`) + } else { + logger.error(`findUserByEmail failed, unknown error: ${e}`) + } + throw new Error('No user with this credentials') + } } async function checkEmailExists(email: string): Promise { diff --git a/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts b/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts index 5d7b9f723..74593f1c7 100644 --- a/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts +++ b/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts @@ -3,8 +3,13 @@ import { User as DbUser } from 'database' import { verifyAuthToken } from '@/apis/gms/GmsClient' import { CONFIG } from '@/config' import { GmsUserAuthenticationResult } from '@/graphql/model/GmsUserAuthenticationResult' -import { backendLogger as logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { getLogger } from 'log4js' + +const logger = getLogger( + `${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.authenticateGmsUserPlayground`, +) export async function authenticateGmsUserPlayground( _apiKey: string, diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index 028116c99..ee7a0f96d 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -36,16 +36,6 @@ export async function isHomeCommunity(communityIdentifier: string): Promise { - return await DbCommunity.findOneOrFail({ - where: [{ foreign: false }], - }) -} - /** * TODO: Check if it is needed, because currently it isn't used at all * Retrieves the URL of the community with the given identifier. diff --git a/backend/src/graphql/resolver/util/compareGmsRelevantUserSettings.ts b/backend/src/graphql/resolver/util/compareGmsRelevantUserSettings.ts index cc53ff5a1..1928374a4 100644 --- a/backend/src/graphql/resolver/util/compareGmsRelevantUserSettings.ts +++ b/backend/src/graphql/resolver/util/compareGmsRelevantUserSettings.ts @@ -5,10 +5,15 @@ import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs' import { GmsPublishLocationType } from '@/graphql/enum/GmsPublishLocationType' import { PublishNameType } from '@/graphql/enum/PublishNameType' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { Point2Location } from './Location2Point' +const logger = getLogger( + `${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.compareGmsRelevantUserSettings`, +) + export function compareGmsRelevantUserSettings( orgUser: DbUser, updateUserInfosArgs: UpdateUserInfosArgs, diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 9dadaa8e3..a0c930579 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -4,12 +4,14 @@ import { Decimal } from 'decimal.js-light' import { OpenCreation } from '@model/OpenCreation' import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '@/graphql/resolver/const/const' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { getFirstDayOfPreviousNMonth } from '@/util/utilities' import { AppDatabase } from 'database' +import { getLogger } from 'log4js' const db = AppDatabase.getInstance() +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.creations`) interface CreationMap { id: number diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts deleted file mode 100644 index 1bdb186ce..000000000 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { isURL } from 'class-validator' -import { Community, User as DbUser, UserContact as DbUserContact } from 'database' -import { FindOptionsWhere } from 'typeorm' -import { validate, version } from 'uuid' - -import { LogError } from '@/server/LogError' -import { isEMail, isUUID4 } from '@/util/validate' - -import { VALID_ALIAS_REGEX } from './validateAlias' - -/** - * - * @param identifier could be gradidoID, alias or email of user - * @param communityIdentifier could be uuid or name of community - * @returns - */ -export const findUserByIdentifier = async ( - identifier: string, - communityIdentifier: string, -): Promise => { - let user: DbUser | null - const communityWhere: FindOptionsWhere = isURL(communityIdentifier) - ? { url: communityIdentifier } - : isUUID4(communityIdentifier) - ? { communityUuid: communityIdentifier } - : { name: communityIdentifier } - - if (validate(identifier) && version(identifier) === 4) { - user = await DbUser.findOne({ - where: { gradidoID: identifier, community: communityWhere }, - relations: ['emailContact', 'community'], - }) - if (!user) { - throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) - } - } else if (isEMail(identifier)) { - const userContact = await DbUserContact.findOne({ - where: { - email: identifier, - emailChecked: true, - user: { - community: communityWhere, - }, - }, - relations: { user: { community: true } }, - }) - if (!userContact) { - throw new LogError('No user with this credentials', identifier, communityIdentifier) - } - user = userContact.user - user.emailContact = userContact - } else if (VALID_ALIAS_REGEX.exec(identifier)) { - user = await DbUser.findOne({ - where: { alias: identifier, community: communityWhere }, - relations: ['emailContact', 'community'], - }) - if (!user) { - throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) - } - } else { - throw new LogError('Unknown identifier type', identifier) - } - - return user -} diff --git a/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts b/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts deleted file mode 100644 index 94010b846..000000000 --- a/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ApolloServerTestClient } from 'apollo-server-testing' -import { Community as DbCommunity, User as DbUser } from 'database' -import { DataSource } from 'typeorm' - -import { cleanDB, testEnvironment } from '@test/helpers' - -import { writeHomeCommunityEntry } from '@/seeds/community' -import { userFactory } from '@/seeds/factory/user' -import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { bobBaumeister } from '@/seeds/users/bob-baumeister' -import { peterLustig } from '@/seeds/users/peter-lustig' - -import { findUserByIdentifier } from './findUserByIdentifier' - -jest.mock('@/password/EncryptorUtils') - -let con: DataSource -let testEnv: { - mutate: ApolloServerTestClient['mutate'] - query: ApolloServerTestClient['query'] - con: DataSource -} - -beforeAll(async () => { - testEnv = await testEnvironment() - con = testEnv.con - await cleanDB() -}) - -afterAll(async () => { - await cleanDB() - await con.destroy() -}) - -describe('graphql/resolver/util/findUserByIdentifier', () => { - let homeCom: DbCommunity - let communityUuid: string - let communityName: string - let userBibi: DbUser - - beforeAll(async () => { - homeCom = await writeHomeCommunityEntry() - - communityUuid = homeCom.communityUuid! - - communityName = homeCom.communityUuid! - - userBibi = await userFactory(testEnv, bibiBloxberg) - await userFactory(testEnv, peterLustig) - await userFactory(testEnv, bobBaumeister) - }) - - describe('communityIdentifier is community uuid', () => { - it('userIdentifier is gradido id', async () => { - const user = await findUserByIdentifier(userBibi.gradidoID, communityUuid) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - - it('userIdentifier is alias', async () => { - const user = await findUserByIdentifier(userBibi.alias, communityUuid) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - - it('userIdentifier is email', async () => { - const user = await findUserByIdentifier(userBibi.emailContact.email, communityUuid) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - }) - - describe('communityIdentifier is community name', () => { - it('userIdentifier is gradido id', async () => { - const user = await findUserByIdentifier(userBibi.gradidoID, communityName) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - - it('userIdentifier is alias', async () => { - const user = await findUserByIdentifier(userBibi.alias, communityName) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - - it('userIdentifier is email', async () => { - const user = await findUserByIdentifier(userBibi.emailContact.email, communityName) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - }) -}) diff --git a/backend/src/graphql/resolver/util/getKlicktippState.ts b/backend/src/graphql/resolver/util/getKlicktippState.ts index e006ff20a..7cfdb6d51 100644 --- a/backend/src/graphql/resolver/util/getKlicktippState.ts +++ b/backend/src/graphql/resolver/util/getKlicktippState.ts @@ -1,7 +1,10 @@ import { KlickTipp } from '@model/KlickTipp' import { getKlickTippUser } from '@/apis/KlicktippController' -import { klickTippLogger as logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.getKlicktippState`) export const getKlicktippState = async (email: string): Promise => { try { @@ -10,7 +13,7 @@ export const getKlicktippState = async (email: string): Promise => { return new KlickTipp(klickTippUser.status === 'Subscribed') } } catch (err) { - logger.error('There is no klicktipp user for email', email, err) + logger.error('There is no klicktipp user for email', email.substring(0, 3), '...', err) } return new KlickTipp(false) } diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index 67b6d71bd..2d85a6c1a 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -3,6 +3,10 @@ import { FederatedCommunity as DbFederatedCommunity, PendingTransaction as DbPendingTransaction, User as dbUser, + PendingTransactionLoggingView, + CommunityLoggingView, + UserLoggingView, + countOpenPendingTransactions, } from 'database' import { Decimal } from 'decimal.js-light' @@ -12,14 +16,19 @@ import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0 import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs' import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' import { SendCoinsClientFactory } from '@/federation/client/SendCoinsClientFactory' -import { PendingTransactionState } from '@/graphql/enum/PendingTransactionState' +import { PendingTransactionState } from 'shared' import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { calculateSenderBalance } from '@/util/calculateSenderBalance' import { fullName } from '@/util/utilities' +import { getLogger } from 'log4js' import { settlePendingSenderTransaction } from './settlePendingSenderTransaction' +import { SendCoinsArgsLoggingView } from '@/federation/client/1_0/logging/SendCoinsArgsLogging.view' +import { SendCoinsResultLoggingView } from '@/federation/client/1_0/logging/SendCoinsResultLogging.view' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.processXComSendCoins`) export async function processXComPendingSendCoins( receiverCom: DbCommunity, @@ -32,28 +41,20 @@ export async function processXComPendingSendCoins( ): Promise { let voteResult: SendCoinsResult try { - logger.debug( - `XCom: processXComPendingSendCoins...`, - receiverCom, - senderCom, - amount, - memo, - sender, - recipientIdentifier, - ) - const openSenderPendingTx = await DbPendingTransaction.count({ - where: [ - { userGradidoID: sender.gradidoID, state: PendingTransactionState.NEW }, - { linkedUserGradidoID: sender.gradidoID, state: PendingTransactionState.NEW }, - ], - }) - const openReceiverPendingTx = await DbPendingTransaction.count({ - where: [ - { userGradidoID: recipientIdentifier, state: PendingTransactionState.NEW }, - { linkedUserGradidoID: recipientIdentifier, state: PendingTransactionState.NEW }, - ], - }) - if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) { + // even if debug is not enabled, attributes are processed so we skip the entire call for performance reasons + if(logger.isDebugEnabled()) { + logger.debug( + 'XCom: processXComPendingSendCoins...', { + receiverCom: new CommunityLoggingView(receiverCom), + senderCom: new CommunityLoggingView(senderCom), + amount: amount.toString(), + memo: memo.substring(0, 5), + sender: new UserLoggingView(sender), + recipientIdentifier + } + ) + } + if (await countOpenPendingTransactions([sender.gradidoID, recipientIdentifier]) > 0) { throw new LogError( `There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`, ) @@ -63,11 +64,13 @@ export async function processXComPendingSendCoins( if (!senderBalance) { throw new LogError('User has not enough GDD or amount is < 0', senderBalance) } - logger.debug(`X-Com: calculated senderBalance = `, senderBalance) + if(logger.isDebugEnabled()) { + logger.debug(`calculated senderBalance = ${JSON.stringify(senderBalance, null, 2)}`) + } const receiverFCom = await DbFederatedCommunity.findOneOrFail({ where: { - publicKey: receiverCom.publicKey, + publicKey: Buffer.from(receiverCom.publicKey), apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API, }, }) @@ -88,11 +91,15 @@ export async function processXComPendingSendCoins( args.senderUserUuid = sender.gradidoID args.senderUserName = fullName(sender.firstName, sender.lastName) args.senderAlias = sender.alias - logger.debug(`X-Com: ready for voteForSendCoins with args=`, args) + if(logger.isDebugEnabled()) { + logger.debug(`ready for voteForSendCoins with args=${new SendCoinsArgsLoggingView(args)}`) + } voteResult = await client.voteForSendCoins(args) - logger.debug(`X-Com: returned from voteForSendCoins:`, voteResult) + if(logger.isDebugEnabled()) { + logger.debug(`returned from voteForSendCoins: ${new SendCoinsResultLoggingView(voteResult)}`) + } if (voteResult.vote) { - logger.debug(`X-Com: prepare pendingTransaction for sender...`) + logger.debug('prepare pendingTransaction for sender...') // writing the pending transaction on receiver-side was successfull, so now write the sender side try { const pendingTx = DbPendingTransaction.create() @@ -120,38 +127,37 @@ export async function processXComPendingSendCoins( pendingTx.userId = sender.id pendingTx.userGradidoID = sender.gradidoID pendingTx.userName = fullName(sender.firstName, sender.lastName) - logger.debug(`X-Com: initialized sender pendingTX=`, pendingTx) + if(logger.isDebugEnabled()) { + logger.debug(`initialized sender pendingTX=${new PendingTransactionLoggingView(pendingTx)}`) + } await DbPendingTransaction.insert(pendingTx) - logger.debug(`X-Com: sender pendingTx successfully inserted...`) + logger.debug('sender pendingTx successfully inserted...') } catch (err) { - logger.error(`Error in writing sender pending transaction: `, err) + logger.error(`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`) // revert the existing pending transaction on receiver side let revertCount = 0 - logger.debug(`X-Com: first try to revertSendCoins of receiver`) + logger.debug('first try to revertSendCoins of receiver') do { if (await client.revertSendCoins(args)) { - logger.debug(`revertSendCoins()-1_0... successfull after revertCount=`, revertCount) + logger.debug(`revertSendCoins()-1_0... successfull after revertCount=${revertCount}`) // treat revertingSendCoins as an error of the whole sendCoins-process - throw new LogError('Error in writing sender pending transaction: `, err') + throw new LogError('Error in writing sender pending transaction: ', err) } } while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++) throw new LogError( - `Error in reverting receiver pending transaction even after revertCount=`, - revertCount, + `Error in reverting receiver pending transaction even after revertCount=${revertCount}`, + err, ) } - logger.debug(`voteForSendCoins()-1_0... successfull`) + logger.debug('voteForSendCoins()-1_0... successfull') } else { - logger.error( - `X-Com: break with error on writing pendingTransaction for recipient...`, - voteResult, - ) + logger.error(`break with error on writing pendingTransaction for recipient... ${new SendCoinsResultLoggingView(voteResult)}`) } return voteResult } - } catch (err) { - throw new LogError(`Error:`, err) + } catch (err: any) { + throw new LogError(`Error: ${err.message}`, err) } return new SendCoinsResult() } @@ -167,16 +173,19 @@ export async function processXComCommittingSendCoins( ): Promise { const sendCoinsResult = new SendCoinsResult() try { - logger.debug( - `XCom: processXComCommittingSendCoins...`, - receiverCom, - senderCom, - creationDate, - amount, - memo, - sender, - recipient, - ) + if(logger.isDebugEnabled()) { + logger.debug( + 'XCom: processXComCommittingSendCoins...', { + receiverCom: new CommunityLoggingView(receiverCom), + senderCom: new CommunityLoggingView(senderCom), + creationDate: creationDate.toISOString(), + amount: amount.toString(), + memo: memo.substring(0, 5), + sender: new UserLoggingView(sender), + recipient: new SendCoinsResultLoggingView(recipient), + } + ) + } // first find pending Tx with given parameters const pendingTx = await DbPendingTransaction.findOneBy({ userCommunityUuid: senderCom.communityUuid ?? 'homeCom-UUID', @@ -191,10 +200,12 @@ export async function processXComCommittingSendCoins( memo, }) if (pendingTx) { - logger.debug(`X-Com: find pending Tx for settlement:`, pendingTx) + if(logger.isDebugEnabled()) { + logger.debug(`find pending Tx for settlement: ${new PendingTransactionLoggingView(pendingTx)}`) + } const receiverFCom = await DbFederatedCommunity.findOneOrFail({ where: { - publicKey: receiverCom.publicKey, + publicKey: Buffer.from(receiverCom.publicKey), apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API, }, @@ -218,9 +229,11 @@ export async function processXComCommittingSendCoins( args.senderUserName = pendingTx.userName } args.senderAlias = sender.alias - logger.debug(`X-Com: ready for settleSendCoins with args=`, args) + if(logger.isDebugEnabled()) { + logger.debug(`ready for settleSendCoins with args=${new SendCoinsArgsLoggingView(args)}`) + } const acknowledge = await client.settleSendCoins(args) - logger.debug(`X-Com: returnd from settleSendCoins:`, acknowledge) + logger.debug(`returnd from settleSendCoins: ${acknowledge}`) if (acknowledge) { // settle the pending transaction on receiver-side was successfull, so now settle the sender side try { @@ -244,30 +257,29 @@ export async function processXComCommittingSendCoins( sendCoinsResult.recipAlias = recipient.recipAlias } } catch (err) { - logger.error(`Error in writing sender pending transaction: `, err) + logger.error(`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`) // revert the existing pending transaction on receiver side let revertCount = 0 - logger.debug(`X-Com: first try to revertSetteledSendCoins of receiver`) + logger.debug('first try to revertSetteledSendCoins of receiver') do { if (await client.revertSettledSendCoins(args)) { logger.debug( - `revertSettledSendCoins()-1_0... successfull after revertCount=`, - revertCount, + `revertSettledSendCoins()-1_0... successfull after revertCount=${revertCount}`, ) // treat revertingSettledSendCoins as an error of the whole sendCoins-process - throw new LogError('Error in settle sender pending transaction: `, err') + throw new LogError('Error in settle sender pending transaction: ', err) } } while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++) throw new LogError( - `Error in reverting receiver pending transaction even after revertCount=`, - revertCount, + `Error in reverting receiver pending transaction even after revertCount=${revertCount}`, + err, ) } } } } } catch (err) { - logger.error(`Error:`, err) + logger.error(`Error: ${JSON.stringify(err, null, 2)}`) sendCoinsResult.vote = false } return sendCoinsResult diff --git a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts index 72a458642..b5ec0a08a 100644 --- a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts +++ b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts @@ -9,7 +9,7 @@ import { DataSource } from 'typeorm' import { v4 as uuidv4 } from 'uuid' import { cleanDB, testEnvironment } from '@test/helpers' -import { i18n as localization, logger } from '@test/testSetup' +import { i18n as localization } from '@test/testSetup' import { CONFIG } from '@/config' import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' @@ -20,11 +20,17 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { peterLustig } from '@/seeds/users/peter-lustig' import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' +import { getLogger } from 'config-schema/test/testSetup' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector' jest.mock('@/password/EncryptorUtils') +const logger = getLogger( + `${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.sendTransactionsToDltConnector`, +) + /* // Mock the GraphQLClient jest.mock('graphql-request', () => { @@ -417,7 +423,7 @@ describe('create and send Transactions to DltConnector', () => { ]), ) - expect(logger.info).nthCalledWith(3, 'sending to DltConnector currently not configured...') + expect(logger.info).nthCalledWith(2, 'sending to DltConnector currently not configured...') }) }) diff --git a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts index 010188dca..678fcd151 100644 --- a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts +++ b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts @@ -3,8 +3,13 @@ import { IsNull } from 'typeorm' import { DltConnectorClient } from '@dltConnector/DltConnectorClient' -import { backendLogger as logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { Monitor, MonitorNames } from '@/util/Monitor' +import { getLogger } from 'log4js' + +const logger = getLogger( + `${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.sendTransactionsToDltConnector`, +) export async function sendTransactionsToDltConnector(): Promise { logger.info('sendTransactionsToDltConnector...') @@ -35,7 +40,7 @@ export async function sendTransactionsToDltConnector(): Promise { if (result) { dltTx.messageId = 'sended' await DltTransaction.save(dltTx) - logger.info('store messageId=%s in dltTx=%d', dltTx.messageId, dltTx.id) + logger.info(`store messageId=${dltTx.messageId} in dltTx=${dltTx.id}`) } } catch (e) { logger.error( diff --git a/backend/src/graphql/resolver/util/sendUserToGms.ts b/backend/src/graphql/resolver/util/sendUserToGms.ts index 26ac070d5..a0edfd35a 100644 --- a/backend/src/graphql/resolver/util/sendUserToGms.ts +++ b/backend/src/graphql/resolver/util/sendUserToGms.ts @@ -3,8 +3,11 @@ import { Community as DbCommunity, User as DbUser } from 'database' import { createGmsUser, updateGmsUser } from '@/apis/gms/GmsClient' import { GmsUser } from '@/apis/gms/model/GmsUser' import { CONFIG } from '@/config' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.sendUserToGms`) export async function sendUserToGms( user: DbUser, diff --git a/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts b/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts index 82c95ba2e..2d532113c 100644 --- a/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts +++ b/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts @@ -7,15 +7,18 @@ import { } from 'database' import { Decimal } from 'decimal.js-light' -import { PendingTransactionState } from '@/graphql/enum/PendingTransactionState' +import { PendingTransactionState } from 'shared' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { calculateSenderBalance } from '@/util/calculateSenderBalance' - +import { getLogger } from 'log4js' import { getLastTransaction } from './getLastTransaction' const db = AppDatabase.getInstance() +const logger = getLogger( + `${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.settlePendingSenderTransaction`, +) export async function settlePendingSenderTransaction( homeCom: DbCommunity, @@ -31,7 +34,7 @@ export async function settlePendingSenderTransaction( logger.debug(`start Transaction for write-access...`) try { - logger.info('X-Com: settlePendingSenderTransaction:', homeCom, senderUser, pendingTx) + logger.info('settlePendingSenderTransaction:', homeCom, senderUser, pendingTx) // ensure that no other pendingTx with the same sender or recipient exists const openSenderPendingTx = await DbPendingTransaction.count({ diff --git a/backend/src/graphql/resolver/util/storeForeignUser.ts b/backend/src/graphql/resolver/util/storeForeignUser.ts index 929aa9cca..adceb155e 100644 --- a/backend/src/graphql/resolver/util/storeForeignUser.ts +++ b/backend/src/graphql/resolver/util/storeForeignUser.ts @@ -1,7 +1,10 @@ import { Community as DbCommunity, User as DbUser } from 'database' import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' -import { backendLogger as logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.storeForeignUser`) export async function storeForeignUser( recipCom: DbCommunity, @@ -18,7 +21,7 @@ export async function storeForeignUser( }) if (!user) { logger.debug( - 'X-Com: no foreignUser found for:', + 'no foreignUser found for:', recipCom.communityUuid, committingResult.recipGradidoID, ) @@ -36,7 +39,7 @@ export async function storeForeignUser( } foreignUser.gradidoID = committingResult.recipGradidoID foreignUser = await DbUser.save(foreignUser) - logger.debug('X-Com: new foreignUser inserted:', foreignUser) + logger.debug('new foreignUser inserted:', foreignUser) return true } else if ( @@ -45,7 +48,7 @@ export async function storeForeignUser( user.alias !== committingResult.recipAlias ) { logger.warn( - 'X-Com: foreignUser still exists, but with different name or alias:', + 'foreignUser still exists, but with different name or alias:', user, committingResult, ) @@ -62,11 +65,11 @@ export async function storeForeignUser( logger.debug('update recipient successful.', user) return true } else { - logger.debug('X-Com: foreignUser still exists...:', user) + logger.debug('foreignUser still exists...:', user) return true } } catch (err) { - logger.error('X-Com: error in storeForeignUser;', err) + logger.error('error in storeForeignUser;', err) return false } } diff --git a/backend/src/graphql/resolver/util/syncHumhub.test.ts b/backend/src/graphql/resolver/util/syncHumhub.test.ts index 594f5fdfd..5d31f036b 100644 --- a/backend/src/graphql/resolver/util/syncHumhub.test.ts +++ b/backend/src/graphql/resolver/util/syncHumhub.test.ts @@ -4,8 +4,9 @@ import { HumHubClient } from '@/apis/humhub/HumHubClient' import { GetUser } from '@/apis/humhub/model/GetUser' import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs' import { PublishNameType } from '@/graphql/enum/PublishNameType' -import { backendLogger as logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'config-schema/test/testSetup' import { syncHumhub } from './syncHumhub' jest.mock('@/apis/humhub/HumHubClient') @@ -19,6 +20,8 @@ mockUser.humhubPublishName = PublishNameType.PUBLISH_NAME_FULL const mockUpdateUserInfosArg = new UpdateUserInfosArgs() const mockHumHubUser = new GetUser(mockUser, 1) +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.syncHumhub`) + describe('syncHumhub', () => { beforeEach(() => { jest.spyOn(logger, 'debug').mockImplementation() @@ -33,8 +36,7 @@ describe('syncHumhub', () => { it('Should not sync if no relevant changes', async () => { await syncHumhub(mockUpdateUserInfosArg, new User(), 'username') expect(HumHubClient.getInstance).not.toBeCalled() - // language logging from some other place - expect(logger.debug).toBeCalledTimes(5) + expect(logger.debug).toBeCalledTimes(1) expect(logger.info).toBeCalledTimes(0) }) @@ -42,7 +44,7 @@ describe('syncHumhub', () => { mockUpdateUserInfosArg.firstName = 'New' // Relevant changes mockUser.firstName = 'New' await syncHumhub(mockUpdateUserInfosArg, mockUser, 'username') - expect(logger.debug).toHaveBeenCalledTimes(8) // Four language logging calls, two debug calls in function, one for not syncing + expect(logger.debug).toHaveBeenCalledTimes(4) // Four language logging calls, two debug calls in function, one for not syncing expect(logger.info).toHaveBeenLastCalledWith('finished sync user with humhub', { localId: mockUser.id, externId: mockHumHubUser.id, diff --git a/backend/src/graphql/resolver/util/syncHumhub.ts b/backend/src/graphql/resolver/util/syncHumhub.ts index b483af1ce..9cc53d1e9 100644 --- a/backend/src/graphql/resolver/util/syncHumhub.ts +++ b/backend/src/graphql/resolver/util/syncHumhub.ts @@ -7,7 +7,10 @@ import { ExecutedHumhubAction, syncUser } from '@/apis/humhub/syncUser' import { PublishNameLogic } from '@/data/PublishName.logic' import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs' import { PublishNameType } from '@/graphql/enum/PublishNameType' -import { backendLogger as logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' + +const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.syncHumhub`) /** * Syncs the user with humhub @@ -21,6 +24,8 @@ export async function syncHumhub( oldHumhubUsername: string, spaceId?: number | null, ): Promise { + const logger = createLogger() + logger.addContext('user', user.id) // check for humhub relevant changes if ( updateUserInfosArg && diff --git a/backend/src/graphql/resolver/util/validateAlias.test.ts b/backend/src/graphql/resolver/util/validateAlias.test.ts deleted file mode 100644 index c57a6332f..000000000 --- a/backend/src/graphql/resolver/util/validateAlias.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { ApolloServerTestClient } from 'apollo-server-testing' -import { User } from 'database' -import { DataSource } from 'typeorm' - -import { cleanDB, testEnvironment } from '@test/helpers' -import { i18n as localization, logger } from '@test/testSetup' - -import { userFactory } from '@/seeds/factory/user' -import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' - -import { validateAlias } from './validateAlias' - -let con: DataSource -let testEnv: { - mutate: ApolloServerTestClient['mutate'] - query: ApolloServerTestClient['query'] - con: DataSource -} - -beforeAll(async () => { - testEnv = await testEnvironment(logger, localization) - con = testEnv.con - await cleanDB() -}) - -afterAll(async () => { - await cleanDB() - await con.destroy() -}) - -describe('validate alias', () => { - beforeAll(() => { - jest.clearAllMocks() - }) - - describe('alias too short', () => { - it('throws and logs an error', async () => { - await expect(validateAlias('Bi')).rejects.toEqual(new Error('Given alias is too short')) - expect(logger.error).toBeCalledWith('Given alias is too short', 'Bi') - }) - }) - - describe('alias too long', () => { - it('throws and logs an error', async () => { - await expect(validateAlias('BibiBloxbergHexHexHex')).rejects.toEqual( - new Error('Given alias is too long'), - ) - expect(logger.error).toBeCalledWith('Given alias is too long', 'BibiBloxbergHexHexHex') - }) - }) - - describe('alias contains invalid characters', () => { - it('throws and logs an error', async () => { - await expect(validateAlias('Bibi.Bloxberg')).rejects.toEqual( - new Error('Invalid characters in alias'), - ) - expect(logger.error).toBeCalledWith('Invalid characters in alias', 'Bibi.Bloxberg') - }) - }) - - describe('alias is a reserved word', () => { - it('throws and logs an error', async () => { - await expect(validateAlias('admin')).rejects.toEqual(new Error('Alias is not allowed')) - expect(logger.error).toBeCalledWith('Alias is not allowed', 'admin') - }) - }) - - describe('alias is a reserved word with uppercase characters', () => { - it('throws and logs an error', async () => { - await expect(validateAlias('Admin')).rejects.toEqual(new Error('Alias is not allowed')) - expect(logger.error).toBeCalledWith('Alias is not allowed', 'Admin') - }) - }) - - describe('hyphens and underscore', () => { - describe('alias starts with underscore', () => { - it('throws and logs an error', async () => { - await expect(validateAlias('_bibi')).rejects.toEqual( - new Error('Invalid characters in alias'), - ) - expect(logger.error).toBeCalledWith('Invalid characters in alias', '_bibi') - }) - }) - - describe('alias contains two following hyphens', () => { - it('throws and logs an error', async () => { - await expect(validateAlias('bi--bi')).rejects.toEqual( - new Error('Invalid characters in alias'), - ) - expect(logger.error).toBeCalledWith('Invalid characters in alias', 'bi--bi') - }) - }) - }) - - describe('test against existing alias in database', () => { - beforeAll(async () => { - const bibi = await userFactory(testEnv, bibiBloxberg) - const user = await User.findOne({ where: { id: bibi.id } }) - if (user) { - user.alias = 'b-b' - await user.save() - } - }) - - describe('alias exists in database', () => { - it('throws and logs an error', async () => { - await expect(validateAlias('b-b')).rejects.toEqual(new Error('Alias already in use')) - expect(logger.error).toBeCalledWith('Alias already in use', 'b-b') - }) - }) - - describe('alias exists in database with in lower-case', () => { - it('throws and logs an error', async () => { - await expect(validateAlias('b-B')).rejects.toEqual(new Error('Alias already in use')) - expect(logger.error).toBeCalledWith('Alias already in use', 'b-B') - }) - }) - - describe('valid alias', () => { - it('resolves to true', async () => { - await expect(validateAlias('bibi')).resolves.toEqual(true) - }) - }) - }) -}) diff --git a/backend/src/graphql/resolver/util/validateAlias.ts b/backend/src/graphql/resolver/util/validateAlias.ts deleted file mode 100644 index 4eccad63f..000000000 --- a/backend/src/graphql/resolver/util/validateAlias.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { User as DbUser } from 'database' -import { Raw } from 'typeorm' - -import { LogError } from '@/server/LogError' - -export const VALID_ALIAS_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ - -const RESERVED_ALIAS = [ - 'admin', - 'email', - 'gast', - 'gdd', - 'gradido', - 'guest', - 'home', - 'root', - 'support', - 'temp', - 'tmp', - 'tmp', - 'user', - 'usr', - 'var', -] - -export const validateAlias = async (alias: string): Promise => { - if (alias.length < 3) { - throw new LogError('Given alias is too short', alias) - } - if (alias.length > 20) { - throw new LogError('Given alias is too long', alias) - } - if (!alias.match(VALID_ALIAS_REGEX)) { - throw new LogError('Invalid characters in alias', alias) - } - if (RESERVED_ALIAS.includes(alias.toLowerCase())) { - throw new LogError('Alias is not allowed', alias) - } - const aliasInUse = await DbUser.find({ - where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) }, - }) - if (aliasInUse.length !== 0) { - throw new LogError('Alias already in use', alias) - } - return true -} diff --git a/backend/src/index.ts b/backend/src/index.ts index 04ecbc58b..6fe51ba39 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,11 +1,15 @@ import 'reflect-metadata' +import 'source-map-support/register' +import { getLogger } from 'log4js' import { CONFIG } from './config' import { startValidateCommunities } from './federation/validateCommunities' import { createServer } from './server/createServer' import { writeJwtKeyPairInHomeCommunity } from './federation/validateCommunities' +import { initLogging } from './server/logger' async function main() { - const { app } = await createServer() + initLogging() + const { app } = await createServer(getLogger('apollo')) app.listen(CONFIG.PORT, () => { // biome-ignore lint/suspicious/noConsole: no need for logging the start message diff --git a/backend/src/interactions/updateUnconfirmedContribution/AbstractUnconfirmedContribution.role.ts b/backend/src/interactions/updateUnconfirmedContribution/AbstractUnconfirmedContribution.role.ts index 36369987b..dd85e3fb6 100644 --- a/backend/src/interactions/updateUnconfirmedContribution/AbstractUnconfirmedContribution.role.ts +++ b/backend/src/interactions/updateUnconfirmedContribution/AbstractUnconfirmedContribution.role.ts @@ -7,11 +7,14 @@ import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder' import { ContributionStatus } from '@/graphql/enum/ContributionStatus' import { LogError } from '@/server/LogError' import { Context, getClientTimezoneOffset } from '@/server/context' +import { Logger, getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' export abstract class AbstractUnconfirmedContributionRole { private availableCreationSums?: Decimal[] protected changed = true private currentStep = 0 + protected logger: Logger public constructor( protected self: Contribution, @@ -21,6 +24,8 @@ export abstract class AbstractUnconfirmedContributionRole { if (self.confirmedAt || self.deniedAt) { throw new LogError("this contribution isn't unconfirmed!") } + this.logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.interactions.updateUnconfirmedContribution`) + this.logger.addContext('contribution', this.self.id) } public isChanged(): boolean { diff --git a/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionAdmin.role.ts b/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionAdmin.role.ts index a216a8919..f160966d3 100644 --- a/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionAdmin.role.ts +++ b/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionAdmin.role.ts @@ -6,8 +6,6 @@ import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder' import { AdminUpdateContributionArgs } from '@/graphql/arg/AdminUpdateContributionArgs' import { ContributionStatus } from '@/graphql/enum/ContributionStatus' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' - import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role' /** @@ -25,7 +23,7 @@ export class UnconfirmedContributionAdminRole extends AbstractUnconfirmedContrib updateData.amount ?? contribution.amount, updateData.creationDate ? new Date(updateData.creationDate) : contribution.contributionDate, ) - logger.debug('use UnconfirmedContributionAdminRole') + this.logger.debug('use UnconfirmedContributionAdminRole') } /** diff --git a/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionAdminAddMessage.role.ts b/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionAdminAddMessage.role.ts index 42bb75e00..dcc10091f 100644 --- a/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionAdminAddMessage.role.ts +++ b/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionAdminAddMessage.role.ts @@ -7,7 +7,6 @@ import { ContributionMessageArgs } from '@/graphql/arg/ContributionMessageArgs' import { ContributionMessageType } from '@/graphql/enum/ContributionMessageType' import { ContributionStatus } from '@/graphql/enum/ContributionStatus' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role' @@ -21,7 +20,7 @@ export class UnconfirmedContributionAdminAddMessageRole extends AbstractUnconfir private updateData: ContributionMessageArgs, ) { super(contribution, contribution.amount, contribution.contributionDate) - logger.debug('use UnconfirmedContributionAdminAddMessageRole') + this.logger.debug('use UnconfirmedContributionAdminAddMessageRole') } protected update(): void { diff --git a/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionUser.role.ts b/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionUser.role.ts index dedc7a1d1..6955cbfa7 100644 --- a/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionUser.role.ts +++ b/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionUser.role.ts @@ -4,7 +4,6 @@ import { ContributionMessageBuilder } from '@/data/ContributionMessage.builder' import { ContributionArgs } from '@/graphql/arg/ContributionArgs' import { ContributionStatus } from '@/graphql/enum/ContributionStatus' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role' @@ -18,7 +17,7 @@ export class UnconfirmedContributionUserRole extends AbstractUnconfirmedContribu private updateData: ContributionArgs, ) { super(contribution, updateData.amount, new Date(updateData.contributionDate)) - logger.debug('use UnconfirmedContributionUserRole') + this.logger.debug('use UnconfirmedContributionUserRole') } protected update(): void { diff --git a/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionUserAddMessage.role.ts b/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionUserAddMessage.role.ts index 77d89ac2b..0440762f3 100644 --- a/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionUserAddMessage.role.ts +++ b/backend/src/interactions/updateUnconfirmedContribution/UnconfirmedContributionUserAddMessage.role.ts @@ -5,7 +5,6 @@ import { ContributionMessageArgs } from '@/graphql/arg/ContributionMessageArgs' import { ContributionMessageType } from '@/graphql/enum/ContributionMessageType' import { ContributionStatus } from '@/graphql/enum/ContributionStatus' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { AbstractUnconfirmedContributionRole } from './AbstractUnconfirmedContribution.role' @@ -19,7 +18,7 @@ export class UnconfirmedContributionUserAddMessageRole extends AbstractUnconfirm private updateData: ContributionMessageArgs, ) { super(contribution, contribution.amount, contribution.contributionDate) - logger.debug('use UnconfirmedContributionUserAddMessageRole') + this.logger.debug('use UnconfirmedContributionUserAddMessageRole') } protected update(): void { diff --git a/backend/src/logging/DecayLogging.view.ts b/backend/src/logging/DecayLogging.view.ts index f76f97821..538beecd5 100644 --- a/backend/src/logging/DecayLogging.view.ts +++ b/backend/src/logging/DecayLogging.view.ts @@ -1,9 +1,10 @@ import { AbstractLoggingView } from 'database' import { Decay } from '@/graphql/model/Decay' +import type { Decay as DecayInterface } from 'shared' export class DecayLoggingView extends AbstractLoggingView { - public constructor(private self: Decay) { + public constructor(private self: Decay | DecayInterface) { super() } diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 7da8bc9c1..20ff202b7 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -8,7 +8,6 @@ import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' import { crypto_shorthash_KEYBYTES } from 'sodium-native' @@ -40,7 +39,6 @@ export const SecretKeyCryptographyCreateKey = async ( password: string, ): Promise => { try { - logger.trace('call worker for: SecretKeyCryptographyCreateKey') if (configLoginServerKey.length !== crypto_shorthash_KEYBYTES) { throw new LogError( 'ServerKey has an invalid size', diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts index 1131b97ef..9efe4e60e 100644 --- a/backend/src/password/PasswordEncryptor.ts +++ b/backend/src/password/PasswordEncryptor.ts @@ -1,6 +1,5 @@ import { User } from 'database' -// import { logger } from '@test/testSetup' getting error "jest is not defined" import { SecretKeyCryptographyCreateKey, getUserCryptographicSalt } from './EncryptorUtils' export const encryptPassword = async (dbUser: User, password: string): Promise => { diff --git a/backend/src/password/__mocks__/EncryptorUtils.ts b/backend/src/password/__mocks__/EncryptorUtils.ts index 5575f72f7..1bac06239 100644 --- a/backend/src/password/__mocks__/EncryptorUtils.ts +++ b/backend/src/password/__mocks__/EncryptorUtils.ts @@ -4,8 +4,9 @@ import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { crypto_box_SEEDBYTES, crypto_hash_sha512_BYTES, @@ -22,6 +23,8 @@ import { crypto_shorthash_KEYBYTES, } from 'sodium-native' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.password.EncryptorUtils`) + const SecretKeyCryptographyCreateKeyMock = ( salt: string, password: string, diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index 34d399b08..3cae22f71 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -15,11 +15,11 @@ export const userFactory = async ( const { mutate } = client const homeCom = await writeHomeCommunityEntry() - + // console.log('call createUser with', JSON.stringify(user, null, 2)) const response = await mutate({ mutation: createUser, variables: user }) if (!response?.data?.createUser) { // biome-ignore lint/suspicious/noConsole: will be used in tests where logging is mocked - console.log(response) + // console.log(JSON.stringify(response, null, 2)) throw new Error('createUser mutation returned unexpected response') } const { diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts index 8036d49e5..4189f87e3 100644 --- a/backend/src/seeds/index.ts +++ b/backend/src/seeds/index.ts @@ -4,8 +4,9 @@ import { datatype, internet, name } from 'faker' import { CONFIG } from '@/config' import { createServer } from '@/server/createServer' -import { backendLogger as logger } from '@/server/logger' +import { initLogging } from '@/server/logger' +import { getLogger } from 'log4js' import { writeHomeCommunityEntry } from './community' import { contributionLinks } from './contributionLink/index' import { creations } from './creation/index' @@ -17,6 +18,7 @@ import { transactionLinks } from './transactionLink/index' import { users } from './users/index' CONFIG.EMAIL = false +const logger = getLogger('seed') const context = { token: '', @@ -35,7 +37,9 @@ const context = { export const cleanDB = async () => { // this only works as long we do not have foreign key constraints for (const entity of entities) { - await resetEntity(entity) + if (entity.name !== 'Migration') { + await resetEntity(entity) + } } } @@ -48,7 +52,8 @@ const resetEntity = async (entity: any) => { } const run = async () => { - const server = await createServer(context) + initLogging() + const server = await createServer(getLogger('apollo'), context) const seedClient = createTestClient(server.apollo) const { con } = server await cleanDB() diff --git a/backend/src/server/LogError.test.ts b/backend/src/server/LogError.test.ts index 431b60e6e..3b98aea89 100644 --- a/backend/src/server/LogError.test.ts +++ b/backend/src/server/LogError.test.ts @@ -1,7 +1,10 @@ -import { logger } from '@test/testSetup' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'config-schema/test/testSetup' import { LogError } from './LogError' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) + describe('LogError', () => { it('logs an Error when created', () => { new LogError('new LogError') diff --git a/backend/src/server/LogError.ts b/backend/src/server/LogError.ts index 346923019..c455eaa15 100644 --- a/backend/src/server/LogError.ts +++ b/backend/src/server/LogError.ts @@ -1,5 +1,21 @@ -import { backendLogger as logger } from './logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) + +/** + * A custom Error that logs itself immediately upon instantiation. + * + * TODO: Anti-pattern warning: + * Logging inside the constructor introduces side effects during object creation, + * which breaks separation of concerns and can lead to duplicate or unwanted logs. + * It is generally better to log errors where they are caught, not where they are thrown. + * + * @class LogError + * @extends {Error} + * @param {string} msg - The error message. + * @param {...any} details - Additional details passed to the logger. + */ export class LogError extends Error { constructor(msg: string, ...details: any[]) { super(msg) diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 40d0144fa..60f6fc31e 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -6,14 +6,14 @@ import { ApolloServer } from 'apollo-server-express' import express, { Express, json, urlencoded } from 'express' import { slowDown } from 'express-slow-down' import helmet from 'helmet' -import { Logger } from 'log4js' +import { Logger, getLogger } from 'log4js' import { DataSource } from 'typeorm' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { AppDatabase } from 'database' import { context as serverContext } from './context' import { cors } from './cors' import { i18n } from './localization' -import { apolloLogger } from './logger' import { plugins } from './plugins' // TODO implement // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; @@ -25,11 +25,11 @@ interface ServerDef { } export const createServer = async ( + apolloLogger: Logger, context: any = serverContext, - logger: Logger = apolloLogger, localization: i18n.I18n = i18n, ): Promise => { - logger.addContext('user', 'unknown') + const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.createServer`) logger.debug('createServer...') // open mariadb connection, retry connecting with mariadb @@ -91,7 +91,7 @@ export const createServer = async ( introspection: CONFIG.GRAPHIQL, context, plugins, - logger, + logger: apolloLogger, }) apollo.applyMiddleware({ app, path: '/' }) logger.info( diff --git a/backend/src/server/localization.ts b/backend/src/server/localization.ts index 8e533576a..1e587104a 100644 --- a/backend/src/server/localization.ts +++ b/backend/src/server/localization.ts @@ -1,7 +1,9 @@ import path from 'node:path' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import i18n from 'i18n' +import { getLogger } from 'log4js' -import { backendLogger } from './logger' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.localization`) i18n.configure({ locales: ['en', 'de'], @@ -11,9 +13,9 @@ i18n.configure({ // autoReload: true, // if this is activated the seeding hangs at the very end updateFiles: false, objectNotation: true, - logDebugFn: (msg) => backendLogger.debug(msg), - logWarnFn: (msg) => backendLogger.info(msg), - logErrorFn: (msg) => backendLogger.error(msg), + logDebugFn: (msg) => logger.debug(msg), + logWarnFn: (msg) => logger.info(msg), + logErrorFn: (msg) => logger.error(msg), // this api is needed for email-template pug files api: { __: 't', // now req.__ becomes req.t diff --git a/backend/src/server/logger.ts b/backend/src/server/logger.ts index 24ae79c67..be9cababe 100644 --- a/backend/src/server/logger.ts +++ b/backend/src/server/logger.ts @@ -1,21 +1,17 @@ -import { readFileSync } from 'fs' - -import { configure, getLogger } from 'log4js' - import { CONFIG } from '@/config' +import { defaultCategory, initLogger } from 'config-schema' -const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8')) - -options.categories.backend.level = CONFIG.LOG_LEVEL -options.categories.apollo.level = CONFIG.LOG_LEVEL - -configure(options) - -const apolloLogger = getLogger('apollo') -const backendLogger = getLogger('backend') -const klickTippLogger = getLogger('klicktipp') -const gmsLogger = getLogger('gms') - -backendLogger.addContext('user', 'unknown') - -export { apolloLogger, backendLogger, klickTippLogger, gmsLogger } +export function initLogging() { + // init logger + initLogger( + [ + defaultCategory('backend', CONFIG.LOG_LEVEL), + defaultCategory('apollo', CONFIG.LOG_LEVEL), + defaultCategory('klicktipp', CONFIG.LOG_LEVEL), + defaultCategory('gms', CONFIG.LOG_LEVEL), + defaultCategory('seed', CONFIG.LOG_LEVEL), + ], + CONFIG.LOG_FILES_BASE_PATH, + CONFIG.LOG4JS_CONFIG, + ) +} diff --git a/backend/src/util/Monitor.ts b/backend/src/util/Monitor.ts index f592a343d..31d6d8938 100644 --- a/backend/src/util/Monitor.ts +++ b/backend/src/util/Monitor.ts @@ -1,7 +1,8 @@ import { registerEnumType } from 'type-graphql' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' export enum MonitorNames { SEND_DLT_TRANSACTIONS = 1, @@ -15,6 +16,7 @@ registerEnumType(MonitorNames, { /* @typescript-eslint/no-extraneous-class */ export class Monitor { private static locks = new Map() + private static logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.util.Monitor`) private constructor() {} @@ -25,15 +27,15 @@ export class Monitor { public static isLocked(key: MonitorNames): boolean | undefined { if (this.locks.has(key)) { - logger.debug(`Monitor isLocked key=${key} = `, this.locks.get(key)) + this.logger.debug(`Monitor isLocked key=${key} = `, this.locks.get(key)) return this.locks.get(key) } - logger.debug(`Monitor isLocked key=${key} not exists`) + this.logger.debug(`Monitor isLocked key=${key} not exists`) return false } public static lockIt(key: MonitorNames): void { - logger.debug(`Monitor lockIt key=`, key) + this.logger.debug(`Monitor lockIt key=`, key) if (this.locks.has(key)) { throw new LogError('still existing Monitor with key=', key) } @@ -41,7 +43,7 @@ export class Monitor { } public static releaseIt(key: MonitorNames): void { - logger.debug(`Monitor releaseIt key=`, key) + this.logger.debug(`Monitor releaseIt key=`, key) if (this.locks.has(key)) { this.locks.delete(key) } diff --git a/backend/src/util/calculateSenderBalance.ts b/backend/src/util/calculateSenderBalance.ts index d2973c982..a01b1f15e 100644 --- a/backend/src/util/calculateSenderBalance.ts +++ b/backend/src/util/calculateSenderBalance.ts @@ -4,7 +4,7 @@ import { Decay } from '@model/Decay' import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' -import { calculateDecay } from './decay' +import { calculateDecay } from 'shared' export async function calculateSenderBalance( userId: number, @@ -16,7 +16,7 @@ export async function calculateSenderBalance( return null } - const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) + const decay = new Decay(calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)) const balance = decay.balance.add(amount.toString()) return { balance, lastTransactionId: lastTransaction.id, decay } diff --git a/backend/src/util/executeKlicktipp.ts b/backend/src/util/executeKlicktipp.ts index eec525bac..47db2c735 100644 --- a/backend/src/util/executeKlicktipp.ts +++ b/backend/src/util/executeKlicktipp.ts @@ -1,8 +1,10 @@ import { AppDatabase } from 'database' import { exportEventDataToKlickTipp } from './klicktipp' +import { initLogging } from '@/server/logger' async function executeKlicktipp(): Promise { + initLogging() const connection = AppDatabase.getInstance() await connection.init() if (connection.isConnected()) { diff --git a/backend/src/util/time.ts b/backend/src/util/time.ts index 0acaf2501..ccbb91c07 100644 --- a/backend/src/util/time.ts +++ b/backend/src/util/time.ts @@ -1,3 +1,6 @@ +/** + * @param {number} time - in minutes + */ export const getTimeDurationObject = ( time: number, ): { @@ -13,11 +16,27 @@ export const getTimeDurationObject = ( return { minutes: time } } +/** + * @param startDate + * @param endDate + * @returns duration in minutes + */ +export const durationInMinutesFromDates = (startDate: Date, endDate: Date): number => { + const diff = endDate.getTime() - startDate.getTime() + return Math.floor(diff / (1000 * 60)) +} + +/** + * @param duration in minutes + */ export const printTimeDuration = (duration: number): string => { const time = getTimeDurationObject(duration) const result = time.minutes > 0 ? `${time.minutes} minutes` : '' if (time.hours) { return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') } + if (result === '') { + return '0 minutes' + } return result } diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index d0e493ff5..1dd8a4529 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -7,7 +7,7 @@ import { Decay } from '@model/Decay' import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' import { transactionLinkSummary } from '@/graphql/resolver/util/transactionLinkSummary' -import { calculateDecay } from './decay' +import { calculateDecay } from 'shared' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() @@ -36,7 +36,7 @@ async function calculateBalance( return null } - const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) + const decay = new Decay(calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)) const balance = decay.balance.add(amount.toString()) const { sumHoldAvailableAmount } = await transactionLinkSummary(userId, time) diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index b80642dfe..dabaee264 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -6,7 +6,7 @@ import { TransactionTypeId } from '@enum/TransactionTypeId' import { Transaction } from '@model/Transaction' import { User } from '@model/User' -import { calculateDecay } from './decay' +import { calculateDecay } from 'shared' const defaultModelFunctions = { hasId: function (): boolean { diff --git a/backend/src/webhook/elopage.ts b/backend/src/webhook/elopage.ts index b027633a6..64113062f 100644 --- a/backend/src/webhook/elopage.ts +++ b/backend/src/webhook/elopage.ts @@ -25,10 +25,13 @@ I assume that the webhook arrives via POST and transmits a string as shown above */ -import { backendLogger as logger } from '@/server/logger' import { LoginElopageBuys, UserContact as dbUserContact } from 'database' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { UserResolver } from '@/graphql/resolver/UserResolver' +import { getLogger } from 'log4js' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.webhook.elopage`) export const elopageWebhook = async (req: any, res: any): Promise => { logger.info('Elopage Hook received') diff --git a/backend/src/webhook/gms.ts b/backend/src/webhook/gms.ts index 915552390..038804366 100644 --- a/backend/src/webhook/gms.ts +++ b/backend/src/webhook/gms.ts @@ -1,7 +1,10 @@ import { User as DbUser } from 'database' import { decode } from '@/auth/JWT' -import { gmsLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.webhook.gms`) export const gmsWebhook = async (req: any, res: any): Promise => { logger.info('GMS Hook received') diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts index 6d567f029..c7f533931 100644 --- a/backend/test/helpers.ts +++ b/backend/test/helpers.ts @@ -3,7 +3,9 @@ import { entities } from 'database' import { createServer } from '@/server/createServer' -import { i18n, logger } from './testSetup' +import { i18n } from './testSetup' + +import { getLogger } from 'log4js' export const headerPushMock = jest.fn((t) => { context.token = t.value @@ -21,12 +23,14 @@ const context = { export const cleanDB = async () => { // this only works as long we do not have foreign key constraints for (const entity of entities) { - await resetEntity(entity) + if (entity.name !== 'Migration') { + await resetEntity(entity) + } } } -export const testEnvironment = async (testLogger = logger, testI18n = i18n) => { - const server = await createServer(context, testLogger, testI18n) +export const testEnvironment = async (testLogger = getLogger('apollo'), testI18n = i18n) => { + const server = await createServer( testLogger, context, testI18n) const con = server.con const testClient = createTestClient(server.apollo) const mutate = testClient.mutate diff --git a/backend/test/testSetup.ts b/backend/test/testSetup.ts index 02c325794..c010ff705 100644 --- a/backend/test/testSetup.ts +++ b/backend/test/testSetup.ts @@ -1,7 +1,7 @@ import 'openai/shims/node' import { CONFIG } from '@/config' import { i18n } from '@/server/localization' -import { backendLogger as logger } from '@/server/logger' +import { getLogger, printLogs, clearLogs } from 'config-schema/test/testSetup' CONFIG.EMAIL = true CONFIG.EMAIL_TEST_MODUS = false @@ -10,23 +10,6 @@ CONFIG.GMS_ACTIVE = false jest.setTimeout(1000000) -jest.mock('@/server/logger', () => { - const originalModule = jest.requireActual('@/server/logger') - return { - __esModule: true, - ...originalModule, - backendLogger: { - addContext: jest.fn(), - trace: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - error: jest.fn(), - fatal: jest.fn(), - }, - } -}) - jest.mock('@/server/localization', () => { const originalModule = jest.requireActual('@/server/localization') return { @@ -41,4 +24,4 @@ jest.mock('@/server/localization', () => { } }) -export { logger, i18n } +export { i18n, getLogger, printLogs, clearLogs as cleanLogs } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 79bdd2cf3..2152f4b79 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -12,7 +12,7 @@ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./build", /* Redirect output structure to the directory. */ // "rootDir" : ".", diff --git a/backend/turbo.json b/backend/turbo.json index e4dfea9a8..822b2765f 100644 --- a/backend/turbo.json +++ b/backend/turbo.json @@ -2,28 +2,28 @@ "extends": ["//"], "tasks": { "seed": { - "dependsOn": ["database#up", "config-schema#build", "database#build"], + "dependsOn": ["database#up", "^build"], "cache": false }, "locales": {}, "locales:fix": {}, "lint": { - "dependsOn": ["locales", "database#build"] + "dependsOn": ["locales"] }, "lint:fix": { - "dependsOn": ["locales:fix", "database#build"] - }, - "typecheck": { - "dependsOn": ["database#build", "config-schema#build"] + "dependsOn": ["locales:fix"] }, "test": { - "dependsOn": ["database#up:backend_test", "config-schema#build", "database#build"] + "dependsOn": ["database#up:backend_test", "^build"] }, "dev": { - "dependsOn": ["database#up"] + "dependsOn": ["database#up", "^build"] }, "start": { "dependsOn": ["database#up", "build"] + }, + "start:bundle": { + "dependsOn": ["build:bundle"] } } } \ No newline at end of file diff --git a/biome.json b/biome.json index 0f2cc7978..13ccfdbe9 100644 --- a/biome.json +++ b/biome.json @@ -1,16 +1,18 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json", "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, "files": { "ignoreUnknown": false, - "ignore": ["build", "node_modules", "coverage"], - "include": [ - "package.json", - "./src/**/*.js", - "./src/**/*.ts", - "./entity/**/*.ts", - "./logging/**/*.ts", - "./migrations/**/*.ts" + "includes": [ + "**/package.json", + "src/**/*.js", + "src/**/*.ts", + "entity/**/*.ts", + "logging/**/*.ts", + "migrations/**/*.ts", + "!**/build", + "!**/node_modules", + "!**/coverage" ] }, "formatter": { @@ -24,14 +26,13 @@ "attributePosition": "auto", "bracketSpacing": true }, - "organizeImports": { "enabled": true }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, "rules": { "recommended": false, "complexity": { "noExtraBooleanCast": "error", - "noMultipleSpacesInRegularExpressionLiterals": "error", "noUselessCatch": "error", "noUselessConstructor": "error", "noUselessLoneBlockStatements": "error", @@ -39,9 +40,10 @@ "noUselessTernary": "error", "noUselessUndefinedInitialization": "error", "noVoid": "error", - "noWith": "error", "useLiteralKeys": "error", - "useRegexLiterals": "error" + "useRegexLiterals": "error", + "noAdjacentSpacesInRegex": "error", + "noCommaOperator": "error" }, "correctness": { "noConstAssign": "error", @@ -52,7 +54,6 @@ "noInnerDeclarations": "error", "noInvalidConstructorSuper": "error", "noInvalidUseBeforeDeclaration": "error", - "noNewSymbol": "error", "noNodejsModules": "off", "noNonoctalDecimalEscape": "error", "noPrecisionLoss": "error", @@ -66,22 +67,22 @@ "noUnsafeOptionalChaining": "error", "noUnusedLabels": "error", "noUnusedVariables": "error", - "useArrayLiterals": "error", "useIsNan": "error", "useValidForDirection": "error", - "useYield": "error" + "useYield": "error", + "noInvalidBuiltinInstantiation": "error", + "useValidTypeof": "error" }, "security": { "noGlobalEval": "error" }, "style": { - "noCommaOperator": "error", "noDefaultExport": "error", - "noVar": "warn", "noYodaExpression": "error", "useBlockStatements": "error", "useConsistentBuiltinInstantiation": "error", "useConst": "error", "useImportType": "off", - "useSingleVarDeclarator": "error" + "useSingleVarDeclarator": "error", + "useArrayLiterals": "error" }, "suspicious": { "noAsyncPromiseExecutor": "error", @@ -110,10 +111,11 @@ "noUnsafeNegation": "error", "useDefaultSwitchClauseLast": "error", "useGetterReturn": "error", - "useValidTypeof": "error" + "noWith": "error", + "noVar": "warn" } }, - "ignore": ["**/node_modules", "**/*.min.js", "**/build", "**/coverage"] + "includes": ["**", "!**/node_modules", "!**/*.min.js", "!**/build", "!**/coverage"] }, "javascript": { "formatter": { @@ -147,9 +149,9 @@ }, "overrides": [ { - "include": ["*.ts", "*.tsx"], + "includes": ["**/*.ts", "**/*.tsx"], "linter": { "rules": { "complexity": { "noVoid": "error" } } } }, - { "include": ["*.test.ts"], "linter": { "rules": {} } } + { "includes": ["**/*.test.ts"], "linter": { "rules": {} } } ] } diff --git a/bun.lock b/bun.lock index 3b91556b9..7087ef10f 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "uuid": "^8.3.2", }, "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", }, }, "admin": { @@ -93,7 +93,7 @@ }, "devDependencies": { "@anatine/esbuild-decorators": "^0.2.19", - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", "@swc/cli": "^0.7.3", "@swc/core": "^1.11.24", "@swc/helpers": "^0.5.17", @@ -106,22 +106,24 @@ "@types/node": "^17.0.21", "@types/nodemailer": "^6.4.4", "@types/sodium-native": "^2.3.5", + "@types/source-map-support": "^0.5.10", "@types/uuid": "^8.3.4", "apollo-server-express": "^2.25.2", "apollo-server-testing": "^2.25.2", "await-semaphore": "^0.1.3", "axios": "^0.21.1", "class-validator": "^0.13.1", - "config-schema": "*", + "config-schema": "workspace:*", + "core": "workspace:*", "cors": "^2.8.5", - "database": "*", + "database": "workspace:*", "decimal.js-light": "^2.5.1", "dotenv": "^10.0.0", "esbuild": "^0.25.2", "express": "^4.17.21", "express-slow-down": "^2.0.1", "faker": "^5.5.3", - "graphql": "^15.10.1", + "graphql": "15.10.1", "graphql-parse-resolve-info": "^4.13.1", "graphql-request": "5.0.0", "graphql-tag": "^2.12.6", @@ -144,12 +146,14 @@ "random-bigint": "^0.0.1", "reflect-metadata": "^0.1.13", "regenerator-runtime": "^0.14.1", - "ts-jest": "27.0.5", + "shared": "workspace:*", + "source-map-support": "^0.5.21", + "ts-jest": "29.4.0", "ts-node": "^10.9.2", "tsconfig-paths": "^4.1.1", "type-graphql": "^1.1.1", "typed-rest-client": "^1.8.11", - "typeorm": "^0.3.22", + "typeorm": "^0.3.25", "typescript": "^4.9.5", "uuid": "^8.3.2", "workerpool": "^9.2.0", @@ -158,13 +162,33 @@ }, "config-schema": { "name": "config-schema", - "version": "1.0.0", + "version": "2.6.0", "dependencies": { "esbuild": "^0.25.2", "joi": "^17.13.3", + "log4js": "^6.9.1", + "source-map-support": "^0.5.21", + "yoctocolors-cjs": "^2.1.2", + "zod": "^3.25.61", }, "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", + "@types/node": "^17.0.21", + "jest": "27.2.4", + "typescript": "^4.9.5", + }, + }, + "core": { + "name": "core", + "version": "2.6.0", + "dependencies": { + "database": "*", + "esbuild": "^0.25.2", + "log4js": "^6.9.1", + "zod": "^3.25.61", + }, + "devDependencies": { + "@biomejs/biome": "2.0.0", "@types/node": "^17.0.21", "typescript": "^4.9.5", }, @@ -183,18 +207,30 @@ "log4js": "^6.9.1", "mysql2": "^2.3.0", "reflect-metadata": "^0.1.13", + "shared": "*", + "source-map-support": "^0.5.21", "ts-mysql-migrate": "^1.0.2", - "tsx": "^4.19.4", - "typeorm": "^0.3.22", + "tsx": "^4.20.3", + "typeorm": "^0.3.25", "uuid": "^8.3.2", "wkx": "^0.5.0", }, "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", + "@swc-node/register": "^1.10.10", + "@swc/cli": "^0.7.3", + "@swc/core": "^1.11.24", + "@swc/helpers": "^0.5.17", "@types/faker": "^5.5.9", "@types/geojson": "^7946.0.13", - "@types/node": "^17.0.21", + "@types/jest": "27.0.2", + "@types/node": "^18.7.14", + "crypto-random-bigint": "^2.1.1", + "jest": "27.2.4", + "ts-jest": "27.0.5", + "ts-node": "^10.9.2", "typescript": "^4.9.5", + "vitest": "^3.2.4", }, }, "dht-node": { @@ -206,8 +242,12 @@ "sodium-universal": "4.0.1", }, "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", "@hyperswarm/dht": "6.5.1", + "@swc/cli": "^0.7.3", + "@swc/core": "^1.11.24", + "@swc/helpers": "^0.5.17", + "@swc/jest": "^0.2.38", "@types/dotenv": "^8.2.3", "@types/jest": "27.5.1", "@types/joi": "^17.2.3", @@ -220,10 +260,13 @@ "jest": "27.5.1", "joi": "^17.13.3", "log4js": "^6.9.1", + "nodemon": "^2.0.7", "prettier": "^2.8.8", + "source-map-support": "^0.5.21", "ts-jest": "27.1.4", + "tsconfig-paths": "^4.1.1", "tsx": "^4.19.4", - "typeorm": "^0.3.22", + "typeorm": "^0.3.25", "typescript": "^4.9.5", "uuid": "^8.3.2", }, @@ -237,7 +280,7 @@ }, "devDependencies": { "@anatine/esbuild-decorators": "^0.2.19", - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", "@swc/cli": "^0.7.3", "@swc/core": "^1.11.24", "@swc/helpers": "^0.5.17", @@ -270,10 +313,11 @@ "nodemon": "^2.0.7", "prettier": "^3.5.3", "reflect-metadata": "^0.1.13", + "source-map-support": "^0.5.21", "ts-jest": "27.0.5", "tsconfig-paths": "^4.1.1", "type-graphql": "^1.1.1", - "typeorm": "^0.3.22", + "typeorm": "^0.3.25", "typescript": "^4.9.5", "uuid": "8.3.2", }, @@ -373,6 +417,23 @@ "webpack": "^5", }, }, + "shared": { + "name": "shared", + "version": "2.6.0", + "dependencies": { + "decimal.js-light": "^2.5.1", + "esbuild": "^0.25.2", + "log4js": "^6.9.1", + "zod": "^3.25.61", + }, + "devDependencies": { + "@biomejs/biome": "2.0.0", + "@types/node": "^17.0.21", + "@types/uuid": "^10.0.0", + "typescript": "^4.9.5", + "uuid": "^8.3.2", + }, + }, }, "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], @@ -465,23 +526,23 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + "@biomejs/biome": ["@biomejs/biome@2.0.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.0.0", "@biomejs/cli-darwin-x64": "2.0.0", "@biomejs/cli-linux-arm64": "2.0.0", "@biomejs/cli-linux-arm64-musl": "2.0.0", "@biomejs/cli-linux-x64": "2.0.0", "@biomejs/cli-linux-x64-musl": "2.0.0", "@biomejs/cli-win32-arm64": "2.0.0", "@biomejs/cli-win32-x64": "2.0.0" }, "bin": { "biome": "bin/biome" } }, "sha512-BlUoXEOI/UQTDEj/pVfnkMo8SrZw3oOWBDrXYFT43V7HTkIUDkBRY53IC5Jx1QkZbaB+0ai1wJIfYwp9+qaJTQ=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QvqWYtFFhhxdf8jMAdJzXW+Frc7X8XsnHQLY+TBM1fnT1TfeV/v9vsFI5L2J7GH6qN1+QEEJ19jHibCY2Ypplw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-5JFhls1EfmuIH4QGFPlNpxJQFC6ic3X1ltcoLN+eSRRIPr6H/lUS1ttuD0Fj7rPgPhZqopK/jfH8UVj/1hIsQw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BAH4QVi06TzAbVchXdJPsL0Z/P87jOfes15rI+p3EX9/EGTfIjaQ9lBVlHunxcmoptaA5y1Hdb9UYojIhmnjIw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bxsz8ki8+b3PytMnS5SgrGV+mbAWwIxI3ydChb/d1rURlJTMdxTTq5LTebUnlsUWAX6OvJuFeiVq9Gjn1YbCyA=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-09PcOGYTtkopWRm6mZ/B6Mr6UHdkniUgIG/jLBv+2J8Z61ezRE+xQmpi3yNgUrFIAU4lPA9atg7mhvE/5Bo7Wg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tiQ0ABxMJb9I6GlfNp0ulrTiQSFacJRJO8245FFwE3ty3bfsfxlU/miblzDIi+qNrgGsLq5wIZcVYGp4c+HXZA=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vrTtuGu91xNTEQ5ZcMJBZuDlqr32DWU1r14UfePIGndF//s2WUAmer4FmgoPgruo76rprk37e8S2A2c0psXdxw=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2USVQ0hklNsph/KIR72ZdeptyXNnQ3JdzPn3NbjI4Sna34CnxeiYAaZcZzXPDl5PYNFBivV4xmvT3Z3rTmyDBg=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -501,6 +562,12 @@ "@dual-bundle/import-meta-resolve": ["@dual-bundle/import-meta-resolve@4.1.0", "", {}, "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg=="], + "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], @@ -627,6 +694,8 @@ "@jest/core": ["@jest/core@27.5.1", "", { "dependencies": { "@jest/console": "^27.5.1", "@jest/reporters": "^27.5.1", "@jest/test-result": "^27.5.1", "@jest/transform": "^27.5.1", "@jest/types": "^27.5.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.8.1", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^27.5.1", "jest-config": "^27.5.1", "jest-haste-map": "^27.5.1", "jest-message-util": "^27.5.1", "jest-regex-util": "^27.5.1", "jest-resolve": "^27.5.1", "jest-resolve-dependencies": "^27.5.1", "jest-runner": "^27.5.1", "jest-runtime": "^27.5.1", "jest-snapshot": "^27.5.1", "jest-util": "^27.5.1", "jest-validate": "^27.5.1", "jest-watcher": "^27.5.1", "micromatch": "^4.0.4", "rimraf": "^3.0.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ=="], + "@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="], + "@jest/environment": ["@jest/environment@27.5.1", "", { "dependencies": { "@jest/fake-timers": "^27.5.1", "@jest/types": "^27.5.1", "@types/node": "*", "jest-mock": "^27.5.1" } }, "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA=="], "@jest/fake-timers": ["@jest/fake-timers@27.5.1", "", { "dependencies": { "@jest/types": "^27.5.1", "@sinonjs/fake-timers": "^8.0.1", "@types/node": "*", "jest-message-util": "^27.5.1", "jest-mock": "^27.5.1", "jest-util": "^27.5.1" } }, "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ=="], @@ -715,6 +784,8 @@ "@napi-rs/nice-win32-x64-msvc": ["@napi-rs/nice-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -725,6 +796,32 @@ "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="], + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@5.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hXem5ZAguS7IlSiHg/LK0tEfLj4eUo+9U6DaFwwBEGd0L0VIF9LmuiHydRyOrdnnmi9iAAFMAn/wl2cUoiuruA=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@5.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-wgSwfsZkRbuYCIBLxeg1bYrtKnirAy+IJF0lwfz4z08clgdNBDbfGECJe/cd0csIZPpRcvPFe8317yf31sWhtA=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@5.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kzeE2WHgcRMmWjB071RdwEV5Pwke4o0WWslCKoh8if1puvxIxfzu3o7g6P2+v77BP5qop4cri+uvLABSO0WZjg=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@5.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-I8np34yZP/XfIkZNDbw3rweqVgfjmHYpNX3xnJZWg+f4mgO9/UNWBwetSaqXeDZqvIch/aHak+q4HVrQhQKCqg=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@5.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-u2ndfeEUrW898eXM+qPxIN8TvTPjI90NDQBRgaxxkOfNw3xaotloeiZGz5+Yzlfxgvxr9DY9FdYkqhUhSnGhOw=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@5.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TzbjmFkcnESGuVItQ2diKacX8vu5G0bH3BHmIlmY4OSRLyoAlrJFwGKAHmh6C9+Amfcjo2rx8vdm7swzmsGC6Q=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@5.3.0", "", { "os": "linux", "cpu": "none" }, "sha512-NH3pjAqh8nuN29iRuRfTY42Vn03ctoR9VE8llfoUKUfhHUjFHYOXK5VSkhjj1usG8AeuesvqrQnLptCRQVTi/Q=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@5.3.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-tuZtkK9sJYh2MC2uhol1M/8IMTB6ZQ5jmqP2+k5XNXnOb/im94Y5uV/u2lXwVyIuKHZZHtr+0d1HrOiNahoKpw=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@5.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-VzhPYmZCtoES/ThcPdGSmMop7JlwgqtSvlgtKCW15ByV2JKyl8kHAHnPSBfpIooXb0ehFnRdxFtL9qtAEWy01g=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@5.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Hi39cWzul24rGljN4Vf1lxjXzQdCrdxO5oCT7KJP4ndSlqIUODJnfnMAP1YhcnIRvNvk+5E6sZtnEmFUd/4d8Q=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@5.3.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.9" }, "cpu": "none" }, "sha512-ddujvHhP3chmHnSXRlkPVUeYj4/B7eLZwL4yUid+df3WCbVh6DgoT9RmllZn21AhxgKtMdekDdyVJYKFd8tl4A=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@5.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-j1YYPLvUkMVNKmIFQZZJ7q6Do4cI3htUnyxNLwDSBVhSohvPIK2VG+IdtOAlWZGa7v+phEZsHfNbXVwB0oPYFQ=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@5.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LT9eOPPUqfZscQRd5mc08RBeDWOQf+dnOrKnanMallTGPe6g7+rcAlFTA8SWoJbcD45PV8yArFtCmSQSpzHZmg=="], + "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], @@ -843,35 +940,43 @@ "@sqltools/formatter": ["@sqltools/formatter@1.2.5", "", {}, "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="], - "@swc/cli": ["@swc/cli@0.7.3", "", { "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", "commander": "^8.3.0", "fast-glob": "^3.2.5", "minimatch": "^9.0.3", "piscina": "^4.3.1", "semver": "^7.3.8", "slash": "3.0.0", "source-map": "^0.7.3" }, "peerDependencies": { "@swc/core": "^1.2.66", "chokidar": "^4.0.1" }, "optionalPeers": ["chokidar"], "bin": { "swc": "bin/swc.js", "swcx": "bin/swcx.js", "spack": "bin/spack.js" } }, "sha512-rnVXNnlURjdOuPaBIwZ3TmBA44BF/eP0j154LanlgPEYfau74ige7cpKlKkZr1IBqMOG99lAnYNxQipDWA3hdg=="], + "@swc-node/core": ["@swc-node/core@1.13.3", "", { "peerDependencies": { "@swc/core": ">= 1.4.13", "@swc/types": ">= 0.1" } }, "sha512-OGsvXIid2Go21kiNqeTIn79jcaX4l0G93X2rAnas4LFoDyA9wAwVK7xZdm+QsKoMn5Mus2yFLCc4OtX2dD/PWA=="], - "@swc/core": ["@swc/core@1.11.24", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.24", "@swc/core-darwin-x64": "1.11.24", "@swc/core-linux-arm-gnueabihf": "1.11.24", "@swc/core-linux-arm64-gnu": "1.11.24", "@swc/core-linux-arm64-musl": "1.11.24", "@swc/core-linux-x64-gnu": "1.11.24", "@swc/core-linux-x64-musl": "1.11.24", "@swc/core-win32-arm64-msvc": "1.11.24", "@swc/core-win32-ia32-msvc": "1.11.24", "@swc/core-win32-x64-msvc": "1.11.24" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg=="], + "@swc-node/register": ["@swc-node/register@1.10.10", "", { "dependencies": { "@swc-node/core": "^1.13.3", "@swc-node/sourcemap-support": "^0.5.1", "colorette": "^2.0.20", "debug": "^4.3.5", "oxc-resolver": "^5.0.0", "pirates": "^4.0.6", "tslib": "^2.6.3" }, "peerDependencies": { "@swc/core": ">= 1.4.13", "typescript": ">= 4.3" } }, "sha512-jYWaI2WNEKz8KZL3sExd2KVL1JMma1/J7z+9iTpv0+fRN7LGMF8VTGGuHI2bug/ztpdZU1G44FG/Kk6ElXL9CQ=="], - "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.24", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA=="], + "@swc-node/sourcemap-support": ["@swc-node/sourcemap-support@0.5.1", "", { "dependencies": { "source-map-support": "^0.5.21", "tslib": "^2.6.3" } }, "sha512-JxIvIo/Hrpv0JCHSyRpetAdQ6lB27oFYhv0PKCNf1g2gUXOjpeR1exrXccRxLMuAV5WAmGFBwRnNOJqN38+qtg=="], - "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.24", "", { "os": "darwin", "cpu": "x64" }, "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ=="], + "@swc/cli": ["@swc/cli@0.7.7", "", { "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", "commander": "^8.3.0", "fast-glob": "^3.2.5", "minimatch": "^9.0.3", "piscina": "^4.3.1", "semver": "^7.3.8", "slash": "3.0.0", "source-map": "^0.7.3" }, "peerDependencies": { "@swc/core": "^1.2.66", "chokidar": "^4.0.1" }, "optionalPeers": ["chokidar"], "bin": { "swc": "bin/swc.js", "swcx": "bin/swcx.js", "spack": "bin/spack.js" } }, "sha512-j4yYm9bx3pxWofaJKX1BFwj/3ngUDynN4UIQ2Xd2h0h/7Gt7zkReBTpDN7g5S13mgAYxacaTHTOUsz18097E8w=="], - "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.24", "", { "os": "linux", "cpu": "arm" }, "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw=="], + "@swc/core": ["@swc/core@1.12.4", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.12.4", "@swc/core-darwin-x64": "1.12.4", "@swc/core-linux-arm-gnueabihf": "1.12.4", "@swc/core-linux-arm64-gnu": "1.12.4", "@swc/core-linux-arm64-musl": "1.12.4", "@swc/core-linux-x64-gnu": "1.12.4", "@swc/core-linux-x64-musl": "1.12.4", "@swc/core-win32-arm64-msvc": "1.12.4", "@swc/core-win32-ia32-msvc": "1.12.4", "@swc/core-win32-x64-msvc": "1.12.4" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-hn30ebV4njAn0NAUM+3a0qCF+MJgqTNSrfA/hUAbC6TVjOQy2OYGQwkUvCu/V7S2+rZxrUsTpKOnZ7qqECZV9Q=="], - "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg=="], + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.12.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HihKfeitjZU2ab94Zf893sxzFryLKX0TweGsNXXOLNtkSMLw50auuYfpRM0BOL9/uXXtuCWgRIF6P030SAX5xQ=="], - "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw=="], + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.12.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-meYCXHyYb6RDdu2N5PNAf0EelyxPBFhRcVo4kBFLuvuNb0m6EUg///VWy8MUMXq9/s9uzGS9kJVXXdRdr/d6FA=="], - "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.24", "", { "os": "linux", "cpu": "x64" }, "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg=="], + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.12.4", "", { "os": "linux", "cpu": "arm" }, "sha512-szfDbf7mE8V64of0q/LSqbk+em+T+TD3uqnH40Z7Qu/aL8vi5CHgyLjWG2SLkLLpyjgkAUF6AKrupgnBYcC2NA=="], - "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.24", "", { "os": "linux", "cpu": "x64" }, "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw=="], + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.12.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-n0IY76w+Scx8m3HIVRvLkoResuwsQgjDfAk9bxn99dq4leQO+mE0fkPl0Yw/1BIsPh+kxGfopIJH9zsZ1Z2YrA=="], - "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.24", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ=="], + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.12.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-wE5jmFi5cEQyLy8WmCWmNwfKETrnzy2D8YNi/xpYWpLPWqPhcelpa6tswkfYlbsMmmOh7hQNoTba1QdGu0jvHQ=="], - "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.24", "", { "os": "win32", "cpu": "ia32" }, "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ=="], + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.12.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6S50Xd/7ePjEwrXyHMxpKTZ+KBrgUwMA8hQPbArUOwH4S5vHBr51heL0iXbUkppn1bkSr0J0IbOove5hzn+iqQ=="], - "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.24", "", { "os": "win32", "cpu": "x64" }, "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w=="], + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.12.4", "", { "os": "linux", "cpu": "x64" }, "sha512-hbYRyaHhC13vYKuGG5BrAG5fjjWEQFfQetuFp/4QKEoXDzdnabJoixxWTQACDL3m0JW32nJ+gUzsYIPtFYkwXg=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.12.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-e6EbfjPL8GA/bb1lc9Omtxjlz+1ThTsAuBsy4Q3Kpbuh6B3jclg8KzxU/6t91v23wG593mieTyR5f3Pr7X3AWw=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.12.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-RG2FzmllBTUf4EksANlIvLckcBrLZEA0t13LIa6L213UZKQfEuDNHezqESgoVhJMg2S/tWauitATOCFgZNSmjg=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.12.4", "", { "os": "win32", "cpu": "x64" }, "sha512-oRHKnZlR83zaMeVUCmHENa4j5uNRAWbmEpjYbzRcfC45LPFNWKGWGAGERLx0u87XMUtTGqnVYxnBTHN/rzDHOw=="], "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - "@swc/types": ["@swc/types@0.1.21", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ=="], + "@swc/jest": ["@swc/jest@0.2.38", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@swc/counter": "^0.1.3", "jsonc-parser": "^3.2.0" }, "peerDependencies": { "@swc/core": "*" } }, "sha512-HMoZgXWMqChJwffdDjvplH53g9G2ALQes3HKXDEdliB/b85OQ0CTSbxG8VSeCwiAn7cOaDVEt4mwmZvbHcS52w=="], + + "@swc/types": ["@swc/types@0.1.23", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw=="], "@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], @@ -887,6 +992,8 @@ "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + "@types/accepts": ["@types/accepts@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -899,6 +1006,8 @@ "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], + "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], "@types/content-disposition": ["@types/content-disposition@0.5.8", "", {}, "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg=="], @@ -907,6 +1016,8 @@ "@types/cors": ["@types/cors@2.8.10", "", {}, "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/dotenv": ["@types/dotenv@8.2.3", "", { "dependencies": { "dotenv": "*" } }, "sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw=="], "@types/email-templates": ["@types/email-templates@10.0.4", "", { "dependencies": { "@types/html-to-text": "*", "@types/nodemailer": "*", "juice": "^8.0.0" } }, "sha512-8O2bdGPO6RYgH2DrnFAcuV++s+8KNA5e2Erjl6UxgKRVsBH9zXu2YLrLyOBRMn2VyEYmzgF+6QQUslpVhj0y/g=="], @@ -997,6 +1108,8 @@ "@types/sodium-native": ["@types/sodium-native@2.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-jZIg5ltGH1okmnH3FrLQsgwjcjOVozMSHwSiEm1/LpMekhOMHbQqp21P4H24mizh1BjwI6Q8qmphmD/HJuAqWg=="], + "@types/source-map-support": ["@types/source-map-support@0.5.10", "", { "dependencies": { "source-map": "^0.6.0" } }, "sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA=="], + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], "@types/uuid": ["@types/uuid@8.3.4", "", {}, "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="], @@ -1479,6 +1592,8 @@ "copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="], + "core": ["core@workspace:core"], + "core-js-pure": ["core-js-pure@3.42.0", "", {}, "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -1495,6 +1610,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "crypto-random-bigint": ["crypto-random-bigint@2.1.1", "", { "dependencies": { "uint-rng": "^1.2.1" } }, "sha512-96+FDrenXybkpnLML/60S8NcG32KgJ5Y8yvNNCYPW02r+ssoXFR5XKenuIQcHLWumnGj8UPqUUHBzXNrDGkDmQ=="], + "css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="], "css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], @@ -1543,7 +1660,7 @@ "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], + "dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], @@ -2229,6 +2346,8 @@ "jsonc-eslint-parser": ["jsonc-eslint-parser@2.4.0", "", { "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^3.0.0", "espree": "^9.0.0", "semver": "^7.3.5" } }, "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg=="], + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], "jstransformer": ["jstransformer@1.0.0", "", { "dependencies": { "is-promise": "^2.0.0", "promise": "^7.0.1" } }, "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A=="], @@ -2515,6 +2634,8 @@ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "oxc-resolver": ["oxc-resolver@5.3.0", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "5.3.0", "@oxc-resolver/binding-darwin-x64": "5.3.0", "@oxc-resolver/binding-freebsd-x64": "5.3.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "5.3.0", "@oxc-resolver/binding-linux-arm64-gnu": "5.3.0", "@oxc-resolver/binding-linux-arm64-musl": "5.3.0", "@oxc-resolver/binding-linux-riscv64-gnu": "5.3.0", "@oxc-resolver/binding-linux-s390x-gnu": "5.3.0", "@oxc-resolver/binding-linux-x64-gnu": "5.3.0", "@oxc-resolver/binding-linux-x64-musl": "5.3.0", "@oxc-resolver/binding-wasm32-wasi": "5.3.0", "@oxc-resolver/binding-win32-arm64-msvc": "5.3.0", "@oxc-resolver/binding-win32-x64-msvc": "5.3.0" } }, "sha512-FHqtZx0idP5QRPSNcI5g2ItmADg7fhR3XIeWg5eRMGfp44xqRpfkdvo+EX4ZceqV9bxvl0Z8vaqMqY0gYaNYNA=="], + "p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], "p-event": ["p-event@4.2.0", "", { "dependencies": { "p-timeout": "^3.1.0" } }, "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ=="], @@ -2581,7 +2702,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], @@ -2819,6 +2940,8 @@ "sha.js": ["sha.js@2.4.11", "", { "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" }, "bin": { "sha.js": "./bin.js" } }, "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ=="], + "shared": ["shared@workspace:shared"], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -2983,11 +3106,13 @@ "tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="], + "tiny-webcrypto": ["tiny-webcrypto@1.0.3", "", {}, "sha512-LQQdNMAgz9BXNT2SKbYh3eCb+fLV0p7JB7MwUjzY6IOlQLGIadfnFqRpshERsS5Dl2OM/hs0+4I/XmSrF+RBbw=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], "tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="], @@ -3029,7 +3154,7 @@ "ts-invariant": ["ts-invariant@0.10.3", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ=="], - "ts-jest": ["ts-jest@27.0.5", "", { "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", "jest-util": "^27.0.0", "json5": "2.x", "lodash": "4.x", "make-error": "1.x", "semver": "7.x", "yargs-parser": "20.x" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@types/jest": "^27.0.0", "babel-jest": ">=27.0.0 <28", "jest": "^27.0.0", "typescript": ">=3.8 <5.0" }, "optionalPeers": ["@babel/core", "@types/jest", "babel-jest"], "bin": { "ts-jest": "cli.js" } }, "sha512-lIJApzfTaSSbtlksfFNHkWOzLJuuSm4faFAfo5kvzOiRAuoN4/eKxVJ2zEAho8aecE04qX6K1pAzfH5QHL1/8w=="], + "ts-jest": ["ts-jest@29.4.0", "", { "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.2", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "optionalPeers": ["@babel/core", "@jest/transform", "@jest/types", "babel-jest", "jest-util"], "bin": { "ts-jest": "cli.js" } }, "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q=="], "ts-mysql-migrate": ["ts-mysql-migrate@1.1.2", "", { "dependencies": { "@types/mysql": "^2.15.8", "mysql": "^2.18.1" }, "bin": { "generate-migration": "dist/generate-migration.js" } }, "sha512-jwhVaMrYBNNhAoZ5XISxzqbnXTHqwazqu/r1UQ6kUaGNPGL43ZFnBiXVj4Gm3pfe3xtCGIaNInehDfdDuigPgw=="], @@ -3039,7 +3164,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.19.4", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q=="], + "tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="], "tua-body-scroll-lock": ["tua-body-scroll-lock@1.5.3", "", {}, "sha512-44W12iqek41kZuTdpEUt3JTUsMx0IxfTajXWfQyMLgzsPaMYUPZLcJkwa4P0x24h5DQ3lYvDuYvphBo4+L0t4w=="], @@ -3081,7 +3206,7 @@ "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="], - "typeorm": ["typeorm@0.3.22", "", { "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", "app-root-path": "^3.1.0", "buffer": "^6.0.3", "dayjs": "^1.11.13", "debug": "^4.4.0", "dotenv": "^16.4.7", "glob": "^10.4.5", "sha.js": "^2.4.11", "sql-highlight": "^6.0.0", "tslib": "^2.8.1", "uuid": "^11.1.0", "yargs": "^17.7.2" }, "peerDependencies": { "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", "@sap/hana-client": "^2.12.25", "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "hdb-pool": "^0.1.6", "ioredis": "^5.0.4", "mongodb": "^5.8.0 || ^6.0.0", "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", "mysql2": "^2.2.5 || ^3.0.1", "oracledb": "^6.3.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", "redis": "^3.1.1 || ^4.0.0", "reflect-metadata": "^0.1.14 || ^0.2.0", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", "ts-node": "^10.7.0", "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" }, "optionalPeers": ["@google-cloud/spanner", "@sap/hana-client", "better-sqlite3", "hdb-pool", "ioredis", "mongodb", "mssql", "mysql2", "oracledb", "pg", "pg-native", "pg-query-stream", "redis", "sql.js", "sqlite3", "ts-node", "typeorm-aurora-data-api-driver"], "bin": { "typeorm": "cli.js", "typeorm-ts-node-esm": "cli-ts-node-esm.js", "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js" } }, "sha512-P/Tsz3UpJ9+K0oryC0twK5PO27zejLYYwMsE8SISfZc1lVHX+ajigiOyWsKbuXpEFMjD9z7UjLzY3+ElVOMMDA=="], + "typeorm": ["typeorm@0.3.25", "", { "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", "app-root-path": "^3.1.0", "buffer": "^6.0.3", "dayjs": "^1.11.13", "debug": "^4.4.0", "dedent": "^1.6.0", "dotenv": "^16.4.7", "glob": "^10.4.5", "sha.js": "^2.4.11", "sql-highlight": "^6.0.0", "tslib": "^2.8.1", "uuid": "^11.1.0", "yargs": "^17.7.2" }, "peerDependencies": { "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", "@sap/hana-client": "^2.12.25", "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "hdb-pool": "^0.1.6", "ioredis": "^5.0.4", "mongodb": "^5.8.0 || ^6.0.0", "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", "mysql2": "^2.2.5 || ^3.0.1", "oracledb": "^6.3.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", "redis": "^3.1.1 || ^4.0.0", "reflect-metadata": "^0.1.14 || ^0.2.0", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", "ts-node": "^10.7.0", "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" }, "optionalPeers": ["@google-cloud/spanner", "@sap/hana-client", "better-sqlite3", "hdb-pool", "ioredis", "mongodb", "mssql", "mysql2", "oracledb", "pg", "pg-native", "pg-query-stream", "redis", "sql.js", "sqlite3", "ts-node", "typeorm-aurora-data-api-driver"], "bin": { "typeorm": "cli.js", "typeorm-ts-node-esm": "cli-ts-node-esm.js", "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js" } }, "sha512-fTKDFzWXKwAaBdEMU4k661seZewbNYET4r1J/z3Jwf+eAvlzMVpTLKAVcAzg75WwQk7GDmtsmkZ5MfkmXCiFWg=="], "typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], @@ -3093,6 +3218,8 @@ "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + "uint-rng": ["uint-rng@1.2.1", "", { "dependencies": { "tiny-webcrypto": "^1.0.2" } }, "sha512-swhDg5H+3DX2sIvnYA7VMBMXV/t8mPxvh49CjCDkwFmj/3OZIDOQwJANBgM1MPSUBrUHNIlXmU7/GcL7m4907g=="], + "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], @@ -3279,7 +3406,7 @@ "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yauzl": ["yauzl@3.2.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w=="], @@ -3287,13 +3414,15 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], + "yup": ["yup@1.6.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA=="], "zen-observable": ["zen-observable@0.8.15", "", {}, "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="], "zen-observable-ts": ["zen-observable-ts@1.2.5", "", { "dependencies": { "zen-observable": "0.8.15" } }, "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg=="], - "zod": ["zod@3.25.55", "", {}, "sha512-219huNnkSLQnLsQ3uaRjXsxMrVm5C9W3OOpEVt2k5tvMKuA8nBSu38e0B//a+he9Iq2dvmk2VyYVlHqiHa4YBA=="], + "zod": ["zod@3.25.61", "", {}, "sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ=="], "@apollo/protobufjs/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="], @@ -3361,6 +3490,8 @@ "@jest/core/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], + "@jest/create-cache-key-function/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + "@jest/environment/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], "@jest/fake-timers/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], @@ -3397,9 +3528,9 @@ "@nuxt/kit/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="], - "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + "@nuxt/kit/tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], - "@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], "@selderee/plugin-htmlparser2/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], @@ -3435,6 +3566,8 @@ "@types/sodium-native/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], + "@types/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "@types/ws/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -3465,6 +3598,8 @@ "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "apollo-boost/ts-invariant": ["ts-invariant@0.4.4", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA=="], "apollo-boost/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -3553,6 +3688,12 @@ "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + "database/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], + + "database/ts-jest": ["ts-jest@27.0.5", "", { "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", "jest-util": "^27.0.0", "json5": "2.x", "lodash": "4.x", "make-error": "1.x", "semver": "7.x", "yargs-parser": "20.x" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@types/jest": "^27.0.0", "babel-jest": ">=27.0.0 <28", "jest": "^27.0.0", "typescript": ">=3.8 <5.0" }, "optionalPeers": ["@babel/core", "@types/jest", "babel-jest"], "bin": { "ts-jest": "cli.js" } }, "sha512-lIJApzfTaSSbtlksfFNHkWOzLJuuSm4faFAfo5kvzOiRAuoN4/eKxVJ2zEAho8aecE04qX6K1pAzfH5QHL1/8w=="], + + "database/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "dht-node/@types/jest": ["@types/jest@27.5.1", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ=="], @@ -3605,6 +3746,8 @@ "federation/helmet": ["helmet@7.2.0", "", {}, "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw=="], + "federation/ts-jest": ["ts-jest@27.0.5", "", { "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", "jest-util": "^27.0.0", "json5": "2.x", "lodash": "4.x", "make-error": "1.x", "semver": "7.x", "yargs-parser": "20.x" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@types/jest": "^27.0.0", "babel-jest": ">=27.0.0 <28", "jest": "^27.0.0", "typescript": ">=3.8 <5.0" }, "optionalPeers": ["@babel/core", "@types/jest", "babel-jest"], "bin": { "ts-jest": "cli.js" } }, "sha512-lIJApzfTaSSbtlksfFNHkWOzLJuuSm4faFAfo5kvzOiRAuoN4/eKxVJ2zEAho8aecE04qX6K1pAzfH5QHL1/8w=="], + "file-type/get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], @@ -3649,6 +3792,8 @@ "jest-circus/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], + "jest-circus/dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], + "jest-cli/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], "jest-environment-jsdom/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], @@ -3675,6 +3820,8 @@ "jest-util/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], + "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "jest-watcher/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], "jest-worker/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], @@ -3697,6 +3844,8 @@ "mailparser/tlds": ["tlds@1.255.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "multimatch/@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="], @@ -3759,6 +3908,8 @@ "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + "shared/@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], + "simple-update-notifier/semver": ["semver@7.0.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A=="], "sodium-secretstream/sodium-universal": ["sodium-universal@5.0.1", "", { "dependencies": { "sodium-native": "^5.0.1" }, "peerDependencies": { "sodium-javascript": "~0.8.0" }, "optionalPeers": ["sodium-javascript"] }, "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q=="], @@ -3811,10 +3962,16 @@ "test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "ts-jest/jest": ["jest@27.5.1", "", { "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", "jest-cli": "^27.5.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ=="], + + "ts-jest/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "ts-jest/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "typed-rest-client/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "typeorm/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "typeorm/dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="], "typeorm/glob": ["glob@10.4.5", "", { "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" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], @@ -3835,16 +3992,14 @@ "unimport/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "unimport/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - "unimport/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="], + "unimport/tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], + "unimport/unplugin": ["unplugin@2.3.2", "", { "dependencies": { "acorn": "^8.14.1", "picomatch": "^4.0.2", "webpack-virtual-modules": "^0.6.2" } }, "sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w=="], "unplugin-utils/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "unplugin-utils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - "unplugin-vue-components/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "unplugin-vue-components/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -3881,8 +4036,6 @@ "xss/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "@apollographql/graphql-upload-8-fork/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], "@apollographql/graphql-upload-8-fork/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], @@ -3911,6 +4064,10 @@ "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "@jest/create-cache-key-function/@jest/types/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="], + + "@jest/create-cache-key-function/@jest/types/@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], + "@jest/reporters/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "@jest/transform/write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -3963,6 +4120,34 @@ "css-select/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + "database/ts-jest/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + + "database/vitest/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "database/vitest/@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "database/vitest/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "database/vitest/@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "database/vitest/@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "database/vitest/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "database/vitest/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "database/vitest/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "database/vitest/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "database/vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "database/vitest/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "database/vitest/vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "dht-node/ts-jest/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "dht-rpc/sodium-universal/sodium-native": ["sodium-native@5.0.1", "", { "dependencies": { "require-addon": "^1.1.0", "which-runtime": "^1.2.1" } }, "sha512-Q305aUXc0OzK7VVRvWkeEQJQIHs6slhFwWpyqLB5iJqhpyt2lYIVu96Y6PQ7TABIlWXVF3YiWDU3xS2Snkus+g=="], "editorconfig/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], @@ -3973,6 +4158,8 @@ "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "federation/ts-jest/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "file-type/get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], @@ -3991,6 +4178,8 @@ "jest-cli/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + "jest-cli/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "jest-environment-jsdom/jsdom/cssstyle": ["cssstyle@2.3.0", "", { "dependencies": { "cssom": "~0.3.6" } }, "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A=="], "jest-environment-jsdom/jsdom/data-urls": ["data-urls@2.0.0", "", { "dependencies": { "abab": "^2.0.3", "whatwg-mimetype": "^2.3.0", "whatwg-url": "^8.0.0" } }, "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ=="], @@ -4027,6 +4216,8 @@ "jest-worker/jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + "jest-worker/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "js-beautify/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "jsdom/parse5/entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="], @@ -4083,8 +4274,6 @@ "typeorm/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "unctx/unplugin/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - "unimport/pkg-types/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], "unplugin-vue-components/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -4093,6 +4282,8 @@ "unplugin-vue-components/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "vite-plugin-html/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], @@ -4169,6 +4360,8 @@ "cheerio-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + "chokidar-cli/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "chokidar-cli/yargs/cliui/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], "chokidar-cli/yargs/cliui/wrap-ansi": ["wrap-ansi@5.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", "strip-ansi": "^5.0.0" } }, "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q=="], @@ -4185,6 +4378,12 @@ "css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + "database/vitest/@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "database/vitest/@vitest/spy/tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], + + "database/vitest/@vitest/utils/loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], + "editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -4209,6 +4408,8 @@ "mailparser/html-to-text/selderee/parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="], + "nodemon/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "run-applescript/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], @@ -4225,6 +4426,8 @@ "typeorm/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "unplugin-vue-components/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "unplugin-vue-components/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "vue-apollo/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], diff --git a/config-schema/biome.json b/config-schema/biome.json deleted file mode 100644 index 52b921260..000000000 --- a/config-schema/biome.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, - "files": { - "ignoreUnknown": false, - "ignore": ["build", "node_modules"], - "include": ["./src/**/*.js", "./src/**/*.ts"] - }, - "formatter": { - "enabled": true, - "useEditorconfig": true, - "formatWithErrors": false, - "indentStyle": "space", - "indentWidth": 2, - "lineEnding": "lf", - "lineWidth": 100, - "attributePosition": "auto", - "bracketSpacing": true - }, - "organizeImports": { "enabled": true }, - "linter": { - "enabled": true, - "rules": { - "recommended": false, - "complexity": { - "noExtraBooleanCast": "error", - "noMultipleSpacesInRegularExpressionLiterals": "error", - "noUselessCatch": "error", - "noUselessConstructor": "error", - "noUselessLoneBlockStatements": "error", - "noUselessRename": "error", - "noUselessTernary": "error", - "noUselessUndefinedInitialization": "error", - "noVoid": "error", - "noWith": "error", - "useLiteralKeys": "error", - "useRegexLiterals": "error" - }, - "correctness": { - "noConstAssign": "error", - "noConstantCondition": "error", - "noEmptyCharacterClassInRegex": "error", - "noEmptyPattern": "error", - "noGlobalObjectCalls": "error", - "noInnerDeclarations": "error", - "noInvalidConstructorSuper": "error", - "noInvalidUseBeforeDeclaration": "error", - "noNewSymbol": "error", - "noNodejsModules": "off", - "noNonoctalDecimalEscape": "error", - "noPrecisionLoss": "error", - "noSelfAssign": "error", - "noSetterReturn": "error", - "noSwitchDeclarations": "error", - "noUndeclaredVariables": "error", - "noUnreachable": "error", - "noUnreachableSuper": "error", - "noUnsafeFinally": "error", - "noUnsafeOptionalChaining": "error", - "noUnusedLabels": "error", - "noUnusedVariables": "error", - "useArrayLiterals": "error", - "useIsNan": "error", - "useValidForDirection": "error", - "useYield": "error" - }, - "security": { "noGlobalEval": "error" }, - "style": { - "noCommaOperator": "error", - "noDefaultExport": "error", - "noVar": "warn", - "noYodaExpression": "error", - "useBlockStatements": "error", - "useConsistentBuiltinInstantiation": "error", - "useConst": "error", - "useSingleVarDeclarator": "error" - }, - "suspicious": { - "noAsyncPromiseExecutor": "error", - "noCatchAssign": "error", - "noClassAssign": "error", - "noCompareNegZero": "error", - "noConsole": "error", - "noControlCharactersInRegex": "error", - "noDebugger": "error", - "noDoubleEquals": "error", - "noDuplicateCase": "error", - "noDuplicateClassMembers": "error", - "noDuplicateObjectKeys": "error", - "noDuplicateParameters": "error", - "noEmptyBlockStatements": "error", - "noFallthroughSwitchClause": "error", - "noFunctionAssign": "error", - "noGlobalAssign": "error", - "noImportAssign": "error", - "noMisleadingCharacterClass": "error", - "noPrototypeBuiltins": "error", - "noRedeclare": "error", - "noSelfCompare": "error", - "noShadowRestrictedNames": "error", - "noSparseArray": "error", - "noUnsafeNegation": "error", - "useDefaultSwitchClauseLast": "error", - "useGetterReturn": "error", - "useValidTypeof": "error" - } - }, - "ignore": ["**/node_modules", "**/*.min.js", "**/build", "**/coverage"] - }, - "javascript": { - "formatter": { - "jsxQuoteStyle": "single", - "quoteProperties": "asNeeded", - "trailingCommas": "all", - "semicolons": "asNeeded", - "arrowParentheses": "always", - "bracketSameLine": false, - "quoteStyle": "single", - "attributePosition": "auto", - "bracketSpacing": true - }, - "globals": ["document", "navigator", "window"] - }, - "overrides": [ - { - "include": ["*.ts", "*.tsx"], - "linter": { "rules": { "complexity": { "noVoid": "error" } } } - }, - { "include": ["*.test.ts"], "linter": { "rules": {} } } - ] -} diff --git a/config-schema/package.json b/config-schema/package.json index 097cb329e..c3a6a2b1d 100644 --- a/config-schema/package.json +++ b/config-schema/package.json @@ -1,6 +1,6 @@ { "name": "config-schema", - "version": "1.0.0", + "version": "2.6.0", "description": "Gradido Config for validate config", "main": "./build/index.js", "types": "./src/index.ts", @@ -15,20 +15,27 @@ "license": "Apache-2.0", "private": true, "scripts": { - "build": "esbuild src/index.ts --outdir=build --platform=node --target=node18.20.7 --bundle --packages=external", + "build": "esbuild src/index.ts --outdir=build --sourcemap --platform=node --target=node18.20.7 --bundle --packages=external", "build:bun": "bun build src/index.ts --outdir=build --target=bun --packages=external", "typecheck": "tsc --noEmit", + "test": "bun test", + "test:debug": "bun test --inspect-brk", "lint": "biome check --error-on-warnings .", "lint:fix": "biome check --error-on-warnings . --write" }, "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", "@types/node": "^17.0.21", + "jest": "27.2.4", "typescript": "^4.9.5" }, "dependencies": { "esbuild": "^0.25.2", - "joi": "^17.13.3" + "joi": "^17.13.3", + "log4js": "^6.9.1", + "source-map-support": "^0.5.21", + "yoctocolors-cjs": "^2.1.2", + "zod": "^3.25.61" }, "engines": { "node": ">=18" diff --git a/config-schema/src/commonSchema.ts b/config-schema/src/commonSchema.ts index cd5716392..12946fdae 100644 --- a/config-schema/src/commonSchema.ts +++ b/config-schema/src/commonSchema.ts @@ -21,12 +21,6 @@ export const browserUrls = Joi.array() .required() .description('All URLs need to have same protocol to prevent mixed block errors') -export const DECAY_START_TIME = Joi.date() - .iso() // ISO 8601 format for date validation - .description('The start time for decay, expected in ISO 8601 format (e.g. 2021-05-13T17:46:31Z)') - .default(new Date('2021-05-13T17:46:31Z')) // default to the specified date if not provided - .required() - export const COMMUNITY_URL = Joi.string() .uri({ scheme: ['http', 'https'] }) .custom((value: string, helpers: Joi.CustomHelpers) => { @@ -131,6 +125,22 @@ export const LOG4JS_CONFIG = Joi.string() .default('log4js-config.json') .required() +export const LOG4JS_CONFIG_PLACEHOLDER = Joi.string() + .pattern(/^[a-zA-Z0-9-_]+(%v)?\.json$/) + .message( + 'LOG4JS_CONFIG_PLACEHOLDER must be a valid filename ending with .json can contain %v as API Version placeholder before ending', + ) + .description('config file name for log4js config file') + .default('log4js-config.json') + .required() + +export const LOG_FILES_BASE_PATH = Joi.string() + .pattern(/^[a-zA-Z0-9-_\/\.]+$/) + .message('LOG_FILES_BASE_PATH must be a valid folder name, relative or absolute') + .description('log folder name for module log files') + .default('../logs/backend') + .optional() + export const LOGIN_APP_SECRET = Joi.string() .pattern(/^[a-fA-F0-9]+$/) .message('need to be valid hex') diff --git a/config-schema/src/const.ts b/config-schema/src/const.ts new file mode 100644 index 000000000..1a593f101 --- /dev/null +++ b/config-schema/src/const.ts @@ -0,0 +1 @@ +export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z') diff --git a/config-schema/src/index.ts b/config-schema/src/index.ts index f9e136a34..9c7c32653 100644 --- a/config-schema/src/index.ts +++ b/config-schema/src/index.ts @@ -1,3 +1,7 @@ +import 'source-map-support/register' export * from './commonSchema' export { DatabaseConfigSchema } from './DatabaseConfigSchema' export { validate } from './validate' +export type { LogLevel, Category } from './log4js-config' +export { createLog4jsConfig, initLogger, defaultCategory } from './log4js-config' +export { DECAY_START_TIME } from './const' diff --git a/config-schema/src/log4js-config/appenders.ts b/config-schema/src/log4js-config/appenders.ts new file mode 100644 index 000000000..19f74dccd --- /dev/null +++ b/config-schema/src/log4js-config/appenders.ts @@ -0,0 +1,76 @@ +import type { + Appender, + DateFileAppender, + LogLevelFilterAppender, + StandardOutputAppender, +} from 'log4js' +import { CustomFileAppender } from './types' + +const fileAppenderTemplate = { + type: 'dateFile' as const, + pattern: 'yyyy-MM-dd', + compress: true, + keepFileExt: true, + fileNameSep: '_', + numBackups: 30, +} + +const defaultAppenders = { + errorFile: { + type: 'dateFile' as const, + filename: 'errors.log', + pattern: 'yyyy-MM-dd', + layout: { type: 'coloredContext' as const, withStack: true }, + compress: true, + keepFileExt: true, + fileNameSep: '_', + numBackups: 30, + } as DateFileAppender, + errors: { + type: 'logLevelFilter' as const, + level: 'error' as const, + appender: 'errorFile' as const, + } as LogLevelFilterAppender, + out: { + type: 'stdout' as const, + layout: { type: 'coloredContext' as const, withStack: 'error' }, + } as StandardOutputAppender, +} + +/** + * Creates the appender configuration for log4js. + * + * @param {CustomFileAppender[]} fileAppenders + * the list of custom file appenders to add to the standard + * appenders. + * @param {string} [basePath] + * the base path for all log files. + * @param {boolean} [stacktraceOnStdout=false] + * whether to show the stacktrace on the standard output + * appender. + * @returns {Object} + * the appender configuration as a map + */ +export function createAppenderConfig( + fileAppenders: CustomFileAppender[], + basePath?: string, +): { [name: string]: Appender } { + if (basePath) { + defaultAppenders.errorFile.filename = `${basePath}/errors.log` + } + const customAppender: { [name: string]: Appender } = { ...defaultAppenders } + + fileAppenders.forEach((appender) => { + const filename = appender.filename ?? `${appender.name}.log` + const dateFile: DateFileAppender = { + ...fileAppenderTemplate, + filename: basePath ? `${basePath}/${filename}` : filename, + } + dateFile.layout = { + type: 'coloredContext', + ...appender.layout, + } + customAppender[appender.name] = dateFile + }) + return customAppender +} diff --git a/config-schema/src/log4js-config/coloredContext.test.ts b/config-schema/src/log4js-config/coloredContext.test.ts new file mode 100644 index 000000000..56a7b7b64 --- /dev/null +++ b/config-schema/src/log4js-config/coloredContext.test.ts @@ -0,0 +1,135 @@ +import { LoggingEvent, levels } from 'log4js' +import colors from 'yoctocolors-cjs' +import { createColoredContextLayout } from './coloredContext' + +let defaultLogEvent: LoggingEvent +let colorFn: (input: string) => string +const startTime = new Date() +const startTimeString = startTime.toISOString() + +describe('createColoredContextLayout', () => { + beforeEach(() => { + defaultLogEvent = { + level: levels.INFO, + categoryName: 'config', + data: ['message'], + context: { user: 1 }, + startTime, + fileName: 'file', + lineNumber: 1, + callStack: 'stack', + pid: 1, + serialise: () => { + throw new Error('Function not implemented.') + }, + } + }) + it('returns a function', () => { + expect(typeof createColoredContextLayout({})).toBe('function') + }) + describe('level:info, color:green', () => { + beforeEach(() => { + defaultLogEvent.level = levels.INFO + colorFn = colors.green + }) + it('format with all undefined', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.INFO}] config -`) + expect(createColoredContextLayout({})(defaultLogEvent)).toBe( + `${coloredString} user=1 message`, + ) + }) + it('format with stack', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.INFO}] config -`) + expect(createColoredContextLayout({ withStack: true })(defaultLogEvent)).toBe( + `${coloredString} user=1 message \nstack`, + ) + }) + it('format with file', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.INFO}] config -`) + expect(createColoredContextLayout({ withFile: true })(defaultLogEvent)).toBe( + `${coloredString} user=1 message \n at file:1`, + ) + }) + it('format with file only if it where level:warn', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.INFO}] config -`) + expect(createColoredContextLayout({ withFile: 'warn' })(defaultLogEvent)).toBe( + `${coloredString} user=1 message`, + ) + }) + it('format with line', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.INFO}] config:1 -`) + expect(createColoredContextLayout({ withLine: true })(defaultLogEvent)).toBe( + `${coloredString} user=1 message`, + ) + }) + it('format with line only if it where level:warn', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.INFO}] config -`) + expect(createColoredContextLayout({ withLine: 'warn' })(defaultLogEvent)).toBe( + `${coloredString} user=1 message`, + ) + }) + it('format with file and line', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.INFO}] config -`) + expect(createColoredContextLayout({ withFile: true, withLine: true })(defaultLogEvent)).toBe( + `${coloredString} user=1 message \n at file:1`, + ) + }) + it('format withStack: error, withLine: true, withFile: warn', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.INFO}] config:1 -`) + expect( + createColoredContextLayout({ + withStack: 'error', + withFile: 'warn', + withLine: true, + })(defaultLogEvent), + ).toBe(`${coloredString} user=1 message`) + }) + }) + + describe('level:error, color:red', () => { + beforeEach(() => { + defaultLogEvent.level = levels.ERROR + colorFn = colors.redBright + }) + it('format with all undefined', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.ERROR}] config -`) + expect(createColoredContextLayout({})(defaultLogEvent)).toBe( + `${coloredString} user=1 message`, + ) + }) + it('format with stack', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.ERROR}] config -`) + expect(createColoredContextLayout({ withStack: true })(defaultLogEvent)).toBe( + `${coloredString} user=1 message \nstack`, + ) + }) + it('format with file', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.ERROR}] config -`) + expect(createColoredContextLayout({ withFile: true })(defaultLogEvent)).toBe( + `${coloredString} user=1 message \n at file:1`, + ) + }) + it('format with line', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.ERROR}] config:1 -`) + expect(createColoredContextLayout({ withLine: true })(defaultLogEvent)).toBe( + `${coloredString} user=1 message`, + ) + }) + it('format with file and line', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.ERROR}] config -`) + expect(createColoredContextLayout({ withFile: true, withLine: true })(defaultLogEvent)).toBe( + `${coloredString} user=1 message \n at file:1`, + ) + }) + it('format withStack: error, withLine: true, withFile: warn', () => { + const coloredString = colorFn(`[${startTimeString}] [${levels.ERROR}] config -`) + expect( + createColoredContextLayout({ + withStack: 'error', + withFile: 'warn', + withLine: true, + })(defaultLogEvent), + ).toBe(`${coloredString} user=1 message \nstack`) + }) + }) +}) diff --git a/config-schema/src/log4js-config/coloredContext.ts b/config-schema/src/log4js-config/coloredContext.ts new file mode 100644 index 000000000..1ebb6b219 --- /dev/null +++ b/config-schema/src/log4js-config/coloredContext.ts @@ -0,0 +1,115 @@ +import { Level, LoggingEvent } from 'log4js' +import colors from 'yoctocolors-cjs' +import { ColoredContextLayoutConfig, LogLevel } from './types' +import { inspect } from 'node:util' + +function colorize(str: string, level: Level): string { + switch (level.colour) { + case 'white': + return colors.white(str) + case 'grey': + return colors.gray(str) + case 'black': + return colors.black(str) + case 'blue': + return colors.blue(str) + case 'cyan': + return colors.cyan(str) + case 'green': + return colors.green(str) + case 'magenta': + return colors.magenta(str) + case 'red': + return colors.redBright(str) + case 'yellow': + return colors.yellow(str) + default: + return colors.gray(str) + } +} + +// distinguish between objects with valid toString function (for examples classes derived from AbstractLoggingView) and other objects +function composeDataString(data: (string | Object)[]): string { + return data + .map((d) => { + // if it is a object and his toString function return only garbage + if (typeof d === 'object' && d.toString() === '[object Object]') { + return inspect(d, ) + } + if (d) { + return d.toString() + } + }) + .join(' ') +} + +// automatic detect context objects and list them in logfmt style +function composeContextString(data: Object): string { + return Object.entries(data) + .map(([key, value]) => { + return `${key}=${value} ` + }) + .join(' ') + .trimEnd() +} + +// check if option is enabled, either if option is them self a boolean or a valid log level and <= eventLogLevel +function isEnabledByLogLevel(eventLogLevel: Level, targetLogLevel?: LogLevel | boolean): boolean { + if (!targetLogLevel) { + return false + } + if (typeof targetLogLevel === 'boolean') { + return targetLogLevel + } + return eventLogLevel.isGreaterThanOrEqualTo(targetLogLevel) +} + +enum DetailKind { + Callstack = 'callstack', + File = 'file', + Line = 'line', +} +function resolveDetailKind( + logEvent: LoggingEvent, + config: ColoredContextLayoutConfig, +): DetailKind | undefined { + if (logEvent.callStack && isEnabledByLogLevel(logEvent.level, config.withStack)) { + return DetailKind.Callstack + } + if (isEnabledByLogLevel(logEvent.level, config.withFile)) { + return DetailKind.File + } + if (isEnabledByLogLevel(logEvent.level, config.withLine)) { + return DetailKind.Line + } + return undefined +} + +export function createColoredContextLayout(config: ColoredContextLayoutConfig) { + return (logEvent: LoggingEvent) => { + const result: string[] = [] + const detailKind = resolveDetailKind(logEvent, config) + let categoryName = logEvent.categoryName + if (detailKind === DetailKind.Line) { + categoryName += `:${logEvent.lineNumber}` + } + result.push( + colorize( + `[${logEvent.startTime.toISOString()}] [${logEvent.level}] ${categoryName} -`, + logEvent.level, + ), + ) + if (Object.keys(logEvent.context).length > 0) { + result.push(composeContextString(logEvent.context)) + } + result.push(composeDataString(logEvent.data)) + + if (detailKind === DetailKind.File) { + result.push(`\n at ${logEvent.fileName}:${logEvent.lineNumber}`) + } + if (detailKind === DetailKind.Callstack) { + result.push(`\n${logEvent.callStack}`) + } + return result.join(' ') + } +} diff --git a/config-schema/src/log4js-config/index.test.ts b/config-schema/src/log4js-config/index.test.ts new file mode 100644 index 000000000..fbc722c0a --- /dev/null +++ b/config-schema/src/log4js-config/index.test.ts @@ -0,0 +1,26 @@ +import { createLog4jsConfig, defaultCategory } from '.' + +describe('createLog4jsConfig', () => { + it('should create a log4js config', () => { + const config = createLog4jsConfig([defaultCategory('test', 'debug')]) + expect(config).toBeDefined() + expect(config.appenders).toBeDefined() + expect(config.categories).toBeDefined() + expect(config.appenders).toHaveProperty('test') + expect(config.categories).toHaveProperty('test') + expect(config.appenders.test).toMatchObject({ + type: 'dateFile', + pattern: 'yyyy-MM-dd', + compress: true, + keepFileExt: true, + fileNameSep: '_', + numBackups: 30, + filename: 'test.log', + layout: { + type: 'coloredContext', + withStack: 'error', + withLine: true, + }, + }) + }) +}) diff --git a/config-schema/src/log4js-config/index.ts b/config-schema/src/log4js-config/index.ts new file mode 100644 index 000000000..94721d384 --- /dev/null +++ b/config-schema/src/log4js-config/index.ts @@ -0,0 +1,81 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { Configuration, LoggingEvent, addLayout, configure } from 'log4js' +import { createAppenderConfig } from './appenders' +import { createColoredContextLayout } from './coloredContext' +import type { Category, CustomFileAppender, LogLevel } from './types' +import { defaultCategory } from './types' + +export type { Category, LogLevel } +export { defaultCategory } + +/** + * Creates the log4js configuration. + * + * @param {Category[]} categories - the categories to add to the configuration + * @param {string} [basePath] - the base path for log files + * @returns {Configuration} the log4js configuration + */ + +addLayout('json', function () { + return function (logEvent: LoggingEvent) { + return JSON.stringify(logEvent) + } +}) + +addLayout('coloredContext', createColoredContextLayout) + +export function createLog4jsConfig(categories: Category[], basePath?: string): Configuration { + const customFileAppenders: CustomFileAppender[] = [] + const result: Configuration = { + appenders: {}, + categories: {}, + } + + categories.forEach((category: Category) => { + customFileAppenders.push({ + name: category.name, + filename: category.filename, + layout: category.layout, + }) + // needed by log4js, show all error message accidentally without (proper) Category + result.categories.default = { + level: 'debug', + appenders: ['out', 'errors'], + enableCallStack: true, + } + const appenders = [category.name, 'out'] + if (category.additionalErrorsFile) { + appenders.push('errors') + } + result.categories[category.name] = { + level: category.level, + appenders, + enableCallStack: true, + } + }) + + result.appenders = createAppenderConfig(customFileAppenders, basePath) + return result +} + +/** + * Initializes the logger. + * + * @param {Category[]} categories - the categories to add to the configuration + * @param {string} logFilesPath - the base path for log files + * @param {string} [log4jsConfigFileName] - the name of the log4js config file + */ +export function initLogger( + categories: Category[], + logFilesPath: string, + log4jsConfigFileName: string = 'log4js-config.json', +): void { + // if not log4js config file exists, create a default one + try { + configure(JSON.parse(readFileSync(log4jsConfigFileName, 'utf-8'))) + } catch (_e) { + const options = createLog4jsConfig(categories, logFilesPath) + writeFileSync(log4jsConfigFileName, JSON.stringify(options, null, 2), { encoding: 'utf-8' }) + configure(options) + } +} diff --git a/config-schema/src/log4js-config/types/Category.ts b/config-schema/src/log4js-config/types/Category.ts new file mode 100644 index 000000000..3be4e24b3 --- /dev/null +++ b/config-schema/src/log4js-config/types/Category.ts @@ -0,0 +1,35 @@ +import { ColoredContextLayoutConfig } from './ColoredContextLayoutConfig' +import { LogLevel } from './LogLevel' + +/** + * Configuration for a log4js category. + * + * @property {string} name - The name of the category. + * @property {string} [filename] - The filename for the category, use name if not set. + * @property {boolean} [stdout] - Whether to log to stdout. + * @property {boolean} [additionalErrorsFile] - Whether to log errors additional to the default error file. + * @property {LogLevel} level - The logging level. + * @property {ColoredContextLayoutConfig} [layout] - The layout for the category. + */ +export type Category = { + name: string + filename?: string + stdout?: boolean + additionalErrorsFile?: boolean + level: LogLevel + layout?: ColoredContextLayoutConfig +} + +export function defaultCategory(name: string, level: LogLevel): Category { + return { + name, + level, + stdout: true, + additionalErrorsFile: true, + layout: { + withStack: 'error', + withFile: 'warn', + withLine: true, + }, + } +} diff --git a/config-schema/src/log4js-config/types/ColoredContextLayoutConfig.ts b/config-schema/src/log4js-config/types/ColoredContextLayoutConfig.ts new file mode 100644 index 000000000..1a01f666f --- /dev/null +++ b/config-schema/src/log4js-config/types/ColoredContextLayoutConfig.ts @@ -0,0 +1,7 @@ +import { LogLevel } from './LogLevel' + +export type ColoredContextLayoutConfig = { + withStack?: LogLevel | boolean + withFile?: LogLevel | boolean + withLine?: LogLevel | boolean +} diff --git a/config-schema/src/log4js-config/types/CustomFileAppender.ts b/config-schema/src/log4js-config/types/CustomFileAppender.ts new file mode 100644 index 000000000..eab3250a3 --- /dev/null +++ b/config-schema/src/log4js-config/types/CustomFileAppender.ts @@ -0,0 +1,34 @@ +import { ColoredContextLayoutConfig } from './ColoredContextLayoutConfig' +import { LogLevel } from './LogLevel' +/** + * use default dateFile Template for custom file appenders + * + * @example use name for key and filename, add .log to name + * ``` + * const appenderConfig = createAppenderConfig([ + * { name: 'info' }, + * ]) + * ``` + * + * @example if log file should contain the stacktrace + * ``` + * const appenderConfig = createAppenderConfig([ + * { name: 'warn', filename: 'warn.log', withStack: true }, + * ]) + * ``` + * + * @example if log file should contain the stacktrace only from log level debug and higher + * ``` + * const appenderConfig = createAppenderConfig([ + * { name: 'warn', filename: 'warn.log', withStack: 'debug' }, + * ]) + * ``` + * if stack is shown, no file and no line is shown, because it is already in the stack trace + * if file:line is shown, no extra line is shown + * line will be shown after category name:line + */ +export type CustomFileAppender = { + name: string + filename?: string + layout?: ColoredContextLayoutConfig +} diff --git a/config-schema/src/log4js-config/types/LogLevel.ts b/config-schema/src/log4js-config/types/LogLevel.ts new file mode 100644 index 000000000..d669c0c96 --- /dev/null +++ b/config-schema/src/log4js-config/types/LogLevel.ts @@ -0,0 +1,15 @@ +import { z } from 'zod' + +export const LOG_LEVEL = z.enum([ + 'all', + 'mark', + 'trace', + 'debug', + 'info', + 'warn', + 'error', + 'fatal', + 'off', +]) + +export type LogLevel = z.infer diff --git a/config-schema/src/log4js-config/types/index.ts b/config-schema/src/log4js-config/types/index.ts new file mode 100644 index 000000000..67279ff57 --- /dev/null +++ b/config-schema/src/log4js-config/types/index.ts @@ -0,0 +1,4 @@ +export * from './Category' +export * from './CustomFileAppender' +export * from './LogLevel' +export * from './ColoredContextLayoutConfig' diff --git a/config-schema/test/testSetup.bun.ts b/config-schema/test/testSetup.bun.ts new file mode 100644 index 000000000..f64b81279 --- /dev/null +++ b/config-schema/test/testSetup.bun.ts @@ -0,0 +1,102 @@ +import { mock, jest } from 'bun:test' +import { inspect } from 'node:util' +/* + * This file is used to mock the log4js logger in the tests. + * It is used to collect all log entries in the logs array. + * If you want to debug your test, you can use `printLogs()` to print all log entries collected through the tests. + * To have only the relevant logs, call `clearLogs()` before your calling the methods you like to test and `printLogs()` after. + * + * This is the bun version + */ + +jest.setTimeout(1000000) + +type LogEntry = { + level: string; + message: string; + logger: string; + context: string; + additional: any[]; +} + +const loggers: { [key: string]: any } = {} +const logs: LogEntry[] = [] + +function addLog(level: string, message: string, logger: string, context: Map, additional: any[]) { + logs.push({ + level, + context: [...context.entries()].map(([key, value]) => `${key}=${value}`).join(' ').trimEnd(), + message, + logger, + additional + }) +} + +export function printLogs() { + for (const log of logs) { + const messages = [] + messages.push(log.message) + // console.log('additionals: ', JSON.stringify(log.additional, null, 2)) + messages.push(log.additional.map((d) => inspect(d)).filter((d) => d)) + process.stdout.write(`${log.logger} [${log.level}] ${log.context} ${messages.join(' ')}\n`) + } +} + +export function clearLogs(): void { + logs.length = 0 +} + +const getLoggerMocked = mock().mockImplementation((param: any) => { + if (loggers[param]) { + // TODO: check if it is working when tests run in parallel + loggers[param].clearContext() + return loggers[param] + } + // console.log('getLogger called with: ', param) + const fakeLogger = { + context: new Map(), + addContext: jest.fn((key: string, value: string) => { + fakeLogger.context.set(key, value) + }), + trace: jest.fn((message: string, ...args: any[]) => { + addLog('trace', message, param, fakeLogger.context, args) + }), + debug: jest.fn((message: string, ...args: any[]) => { + addLog('debug', message, param, fakeLogger.context, args) + }), + warn: jest.fn((message: string, ...args: any[]) => { + addLog('warn', message, param, fakeLogger.context, args) + }), + info: jest.fn((message: string, ...args: any[]) => { + addLog('info', message, param, fakeLogger.context, args) + }), + error: jest.fn((message: string, ...args: any[]) => { + addLog('error', message, param, fakeLogger.context, args) + }), + fatal: jest.fn((message: string, ...args: any[]) => { + addLog('fatal', message, param, fakeLogger.context, args) + }), + removeContext: jest.fn((key: string) => { + fakeLogger.context.delete(key) + }), + clearContext: jest.fn(() => { + fakeLogger.context.clear() + }), + isDebugEnabled: jest.fn(() => { + return true + }) + } + loggers[param] = fakeLogger + return fakeLogger +}) + +mock.module('log4js', () => ({ + getLogger: getLoggerMocked +})) + +export function getLogger(name: string) { + if (!loggers[name]) { + return getLoggerMocked(name) + } + return loggers[name] +} diff --git a/config-schema/test/testSetup.ts b/config-schema/test/testSetup.ts new file mode 100644 index 000000000..530b5cbc8 --- /dev/null +++ b/config-schema/test/testSetup.ts @@ -0,0 +1,109 @@ +/* + * This file is used to mock the log4js logger in the tests. + * It is used to collect all log entries in the logs array. + * If you want to debug your test, you can use `printLogs()` to print all log entries collected through the tests. + * To have only the relevant logs, call `clearLogs()` before your calling the methods you like to test and `printLogs()` after. + */ + +jest.setTimeout(1000000) + +type LogEntry = { + level: string; + message: string; + logger: string; + context: string; + additional: any[]; +} + +const loggers: { [key: string]: any } = {} +const logs: LogEntry[] = [] + +function addLog(level: string, message: string, logger: string, context: Map, additional: any[]) { + logs.push({ + level, + context: [...context.entries()].map(([key, value]) => `${key}=${value}`).join(' ').trimEnd(), + message, + logger, + additional + }) +} + +export function printLogs() { + for (const log of logs) { + const messages = [] + messages.push(log.message) + messages.push(log.additional.map((d) => { + if (typeof d === 'object' && d.toString() === '[object Object]') { + return JSON.stringify(d) + } + if (d) { + return d.toString() + } + }).filter((d) => d)) + process.stdout.write(`${log.logger} [${log.level}] ${log.context} ${messages.join(' ')}\n`) + } +} + +export function clearLogs(): void { + logs.length = 0 +} + +const getLoggerMocked = jest.fn().mockImplementation((param: any) => { + if (loggers[param]) { + // TODO: check if it is working when tests run in parallel + loggers[param].clearContext() + return loggers[param] + } + // console.log('getLogger called with: ', param) + const fakeLogger = { + context: new Map(), + addContext: jest.fn((key: string, value: string) => { + fakeLogger.context.set(key, value) + }), + trace: jest.fn((message: string, ...args: any[]) => { + addLog('trace', message, param, fakeLogger.context, args) + }), + debug: jest.fn((message: string, ...args: any[]) => { + addLog('debug', message, param, fakeLogger.context, args) + }), + warn: jest.fn((message: string, ...args: any[]) => { + addLog('warn', message, param, fakeLogger.context, args) + }), + info: jest.fn((message: string, ...args: any[]) => { + addLog('info', message, param, fakeLogger.context, args) + }), + error: jest.fn((message: string, ...args: any[]) => { + addLog('error', message, param, fakeLogger.context, args) + }), + fatal: jest.fn((message: string, ...args: any[]) => { + addLog('fatal', message, param, fakeLogger.context, args) + }), + removeContext: jest.fn((key: string) => { + fakeLogger.context.delete(key) + }), + clearContext: jest.fn(() => { + fakeLogger.context.clear() + }), + isDebugEnabled: jest.fn(() => { + return true + }) + } + loggers[param] = fakeLogger + return fakeLogger +}) + +jest.mock('log4js', () => { + const originalModule = jest.requireActual('log4js') + return { + __esModule: true, + ...originalModule, + getLogger: getLoggerMocked + } +}) + +export function getLogger(name: string) { + if (!loggers[name]) { + return getLoggerMocked(name) + } + return loggers[name] +} diff --git a/config-schema/test/testSetup.vitest.ts b/config-schema/test/testSetup.vitest.ts new file mode 100644 index 000000000..dac4b0292 --- /dev/null +++ b/config-schema/test/testSetup.vitest.ts @@ -0,0 +1,109 @@ +import { vi } from 'vitest' +/* + * This file is used to mock the log4js logger in the tests. + * It is used to collect all log entries in the logs array. + * If you want to debug your test, you can use `printLogs()` to print all log entries collected through the tests. + * To have only the relevant logs, call `clearLogs()` before your calling the methods you like to test and `printLogs()` after. + */ + + +type LogEntry = { + level: string; + message: string; + logger: string; + context: string; + additional: any[]; +} + +const loggers: { [key: string]: any } = {} +const logs: LogEntry[] = [] + +function addLog(level: string, message: string, logger: string, context: Map, additional: any[]) { + logs.push({ + level, + context: [...context.entries()].map(([key, value]) => `${key}=${value}`).join(' ').trimEnd(), + message, + logger, + additional + }) +} + +export function printLogs() { + for (const log of logs) { + const messages = [] + messages.push(log.message) + messages.push(log.additional.map((d) => { + if (typeof d === 'object' && d.toString() === '[object Object]') { + return JSON.stringify(d) + } + if (d) { + return d.toString() + } + }).filter((d) => d)) + process.stdout.write(`${log.logger} [${log.level}] ${log.context} ${messages.join(' ')}\n`) + } +} + +export function clearLogs(): void { + logs.length = 0 +} + +const getLoggerMocked = vi.fn().mockImplementation((param: any) => { + if (loggers[param]) { + // TODO: check if it is working when tests run in parallel + loggers[param].clearContext() + return loggers[param] + } + // console.log('getLogger called with: ', param) + const fakeLogger = { + context: new Map(), + addContext: vi.fn((key: string, value: string) => { + fakeLogger.context.set(key, value) + }), + trace: vi.fn((message: string, ...args: any[]) => { + addLog('trace', message, param, fakeLogger.context, args) + }), + debug: vi.fn((message: string, ...args: any[]) => { + addLog('debug', message, param, fakeLogger.context, args) + }), + warn: vi.fn((message: string, ...args: any[]) => { + addLog('warn', message, param, fakeLogger.context, args) + }), + info: vi.fn((message: string, ...args: any[]) => { + addLog('info', message, param, fakeLogger.context, args) + }), + error: vi.fn((message: string, ...args: any[]) => { + addLog('error', message, param, fakeLogger.context, args) + }), + fatal: vi.fn((message: string, ...args: any[]) => { + addLog('fatal', message, param, fakeLogger.context, args) + }), + removeContext: vi.fn((key: string) => { + fakeLogger.context.delete(key) + }), + clearContext: vi.fn(() => { + fakeLogger.context.clear() + }), + isDebugEnabled: vi.fn(() => { + return true + }) + } + loggers[param] = fakeLogger + return fakeLogger +}) + +vi.mock('log4js', () => { + const originalModule = vi.importActual('log4js') + return { + __esModule: true, + ...originalModule, + getLogger: getLoggerMocked + } +}) + +export function getLogger(name: string) { + if (!loggers[name]) { + return getLoggerMocked(name) + } + return loggers[name] +} diff --git a/config-schema/tsconfig.json b/config-schema/tsconfig.json index 04306edad..d65460927 100644 --- a/config-schema/tsconfig.json +++ b/config-schema/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ - /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ @@ -69,5 +68,6 @@ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, - "references": [] /* Any project that is referenced must itself have a `references` array (which may be empty). */ + "references": [], /* Any project that is referenced must itself have a `references` array (which may be empty). */ + "exclude": ["**/*.test.ts", "**/*.spec.ts", "test/*"], } diff --git a/core/README.md b/core/README.md new file mode 100644 index 000000000..4a361b0b8 --- /dev/null +++ b/core/README.md @@ -0,0 +1,10 @@ +# core +Gradido Core Code, High-Level Shared Code, with dependencies on other modules + +## Bun-Compatibility +Full bun compatible + +## Validation +All validation logic used across more than one module +Anything more complex than simple zod schemas is implemented here +Tests written for bun \ No newline at end of file diff --git a/core/bunfig.toml b/core/bunfig.toml new file mode 100644 index 000000000..6e74bf7ca --- /dev/null +++ b/core/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["../config-schema/test/testSetup.bun.ts"] diff --git a/core/package.json b/core/package.json new file mode 100644 index 000000000..98863bb60 --- /dev/null +++ b/core/package.json @@ -0,0 +1,40 @@ +{ + "name": "core", + "version": "2.6.0", + "description": "Gradido Core Code, High-Level Shared Code, with dependencies on other modules", + "main": "./build/index.js", + "types": "./src/index.ts", + "exports": { + ".": { + "import": "./build/index.js", + "require": "./build/index.js" + } + }, + "repository": "https://github.com/gradido/gradido/core", + "author": "Gradido Academy - https://www.gradido.net", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "esbuild src/index.ts --outdir=build --platform=node --target=node18.20.7 --bundle --packages=external", + "build:bun": "bun build src/index.ts --outdir=build --target=bun --packages=external", + "test": "bun test", + "test:debug": "bun test --inspect-brk", + "typecheck": "tsc --noEmit", + "lint": "biome check --error-on-warnings .", + "lint:fix": "biome check --error-on-warnings . --write" + }, + "devDependencies": { + "@biomejs/biome": "2.0.0", + "@types/node": "^17.0.21", + "typescript": "^4.9.5" + }, + "dependencies": { + "database": "*", + "esbuild": "^0.25.2", + "log4js": "^6.9.1", + "zod": "^3.25.61" + }, + "engines": { + "node": ">=18" + } +} diff --git a/core/src/config/const.ts b/core/src/config/const.ts new file mode 100644 index 000000000..1c19265d0 --- /dev/null +++ b/core/src/config/const.ts @@ -0,0 +1 @@ +export const LOG4JS_BASE_CATEGORY_NAME = 'core' \ No newline at end of file diff --git a/core/src/index.ts b/core/src/index.ts new file mode 100644 index 000000000..0984ebefc --- /dev/null +++ b/core/src/index.ts @@ -0,0 +1 @@ +export * from './validation/user' \ No newline at end of file diff --git a/core/src/validation/user.test.ts b/core/src/validation/user.test.ts new file mode 100644 index 000000000..cd3bd1925 --- /dev/null +++ b/core/src/validation/user.test.ts @@ -0,0 +1,68 @@ +import { validateAlias } from './user' +import { getLogger } from '../../../config-schema/test/testSetup.bun' +import { describe, it, expect, beforeEach, mock, jest } from 'bun:test' +import { aliasExists } from 'database' +import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.validation.user`) + +mock.module('database', () => ({ + aliasExists: jest.fn(), +})) +mock.module('shared/src/schema/user.schema', () => ({ + aliasSchema: { + parse: jest.fn(), + }, +})) + +describe('validate alias', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('zod throw an validation error', () => { + it('throws and logs an error', () => { + expect(validateAlias('Bi')).rejects.toThrowError(new Error('Given alias is too short')) + expect(logger.warn.mock.calls[0]).toEqual([ + 'invalid alias', + 'Bi', + expect.arrayContaining([ + // error vor zod v4 + /*expect.objectContaining({ + code: 'too_small', + minimum: 3, + origin: 'string', + message: 'Given alias is too short', + }), */ + expect.objectContaining({ + code: 'too_small', + exact: false, + inclusive: true, + minimum: 3, + type: 'string', + message: 'Given alias is too short', + path: [], + }), + ]), + ]) + }) + }) + + + describe('test against existing alias in database', () => { + describe('alias exists in database', () => { + it('throws and logs an error', () => { + (aliasExists as jest.Mock).mockResolvedValue(true) + expect(validateAlias('b-b')).rejects.toEqual(new Error('Given alias is already in use')) + expect(logger.warn.mock.calls[0]).toEqual(['alias already in use', 'b-b']) + }) + }) + + describe('valid alias', () => { + it('resolves to true', async () => { + (aliasExists as jest.Mock).mockResolvedValue(false) + expect(validateAlias('bibi')).resolves.toEqual(true) + }) + }) + }) +}) diff --git a/core/src/validation/user.ts b/core/src/validation/user.ts new file mode 100644 index 000000000..eebc34442 --- /dev/null +++ b/core/src/validation/user.ts @@ -0,0 +1,27 @@ +import { ZodError } from 'zod' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const' +import { aliasExists } from 'database' +import { aliasSchema } from 'shared' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.validation.user`) + +export async function validateAlias(alias: string): Promise { + try { + aliasSchema.parse(alias) + } catch (err) { + if (err instanceof ZodError || (err as Error).name === 'ZodError') { + // throw only first error, but log all errors + logger.warn('invalid alias', alias, (err as ZodError).errors) + throw new Error((err as ZodError).errors[0].message) + } + throw err + } + + if (await aliasExists(alias)) { + logger.warn('alias already in use', alias) + throw new Error('Given alias is already in use') + } + + return true +} diff --git a/core/tsconfig.json b/core/tsconfig.json new file mode 100644 index 000000000..75b686340 --- /dev/null +++ b/core/tsconfig.json @@ -0,0 +1,73 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./build/outfile.js", /* Concatenate and emit output to single file. */ + "outDir": "./build", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [".", "../database"], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": ["bun-types"], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + }, + "references": [], /* Any project that is referenced must itself have a `references` array (which may be empty). */ + "exclude": ["**/*.test.ts", "**/*.spec.ts", "test/*", "**/bun.d.ts"], +} diff --git a/database/Dockerfile b/database/Dockerfile index 5c4e144e5..5413b891b 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -55,8 +55,8 @@ ENV PATH="/root/.bun/bin:${PATH}" ################################################################################## FROM bun-base as installer -COPY --chown=app:app ./database . -RUN bun install --production --no-cache --frozen-lockfile +COPY --chown=app:app . . +RUN bun install --filter database --production --no-cache --frozen-lockfile ################################################################################## # Build ########################################################################## @@ -64,7 +64,8 @@ RUN bun install --production --no-cache --frozen-lockfile FROM installer as build RUN bun install --no-cache --frozen-lockfile \ - yarn build && yarn typecheck + && cd shared && yarn build \ + && cd ../database && yarn build && yarn typecheck ################################################################################## # PRODUCTION IMAGE ############################################################### @@ -72,8 +73,7 @@ RUN bun install --no-cache --frozen-lockfile \ FROM base as production COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/src ./src -COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/migrations ./migrations -COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/entity ./entity +COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/migration ./migration COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/node_modules ./node_modules COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/package.json ./package.json COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json @@ -83,7 +83,6 @@ COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig ################################################################################## FROM production as up - # Run command CMD /bin/sh -c "yarn up" diff --git a/database/README.md b/database/README.md index 780545381..fe7ed3631 100644 --- a/database/README.md +++ b/database/README.md @@ -49,3 +49,5 @@ yarn clear ``` +## Tests +Currently written for vitest, but can be transformed into bun test after switching out TypeORM with DrizzleORM \ No newline at end of file diff --git a/database/biome.json b/database/biome.json index 786069e9b..d27c2426d 100644 --- a/database/biome.json +++ b/database/biome.json @@ -1,10 +1,18 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "root": false, + "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json", "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, "files": { "ignoreUnknown": false, - "ignore": ["build", "node_modules", "coverage"], - "include": ["./src/**/*.ts", "./entity/**/*.ts", "./logging/**/*.ts", "./migrations/**/*.ts"] + "includes": [ + "src/**/*.ts", + "entity/**/*.ts", + "logging/**/*.ts", + "migrations/**/*.ts", + "!**/build", + "!**/node_modules", + "!**/coverage" + ] }, "formatter": { "enabled": true, @@ -17,14 +25,13 @@ "attributePosition": "auto", "bracketSpacing": true }, - "organizeImports": { "enabled": true }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, "rules": { "recommended": false, "complexity": { "noExtraBooleanCast": "error", - "noMultipleSpacesInRegularExpressionLiterals": "error", "noUselessCatch": "error", "noUselessConstructor": "error", "noUselessLoneBlockStatements": "error", @@ -32,10 +39,11 @@ "noUselessTernary": "error", "noUselessUndefinedInitialization": "error", "noVoid": "error", - "noWith": "error", "useArrowFunction": "off", "useLiteralKeys": "error", - "useRegexLiterals": "error" + "useRegexLiterals": "error", + "noAdjacentSpacesInRegex": "error", + "noCommaOperator": "error" }, "correctness": { "noConstAssign": "error", @@ -46,7 +54,6 @@ "noInnerDeclarations": "error", "noInvalidConstructorSuper": "error", "noInvalidUseBeforeDeclaration": "error", - "noNewSymbol": "error", "noNodejsModules": "off", "noNonoctalDecimalEscape": "error", "noPrecisionLoss": "error", @@ -60,22 +67,22 @@ "noUnsafeOptionalChaining": "error", "noUnusedLabels": "error", "noUnusedVariables": "error", - "useArrayLiterals": "error", "useIsNan": "error", "useValidForDirection": "error", - "useYield": "error" + "useYield": "error", + "noInvalidBuiltinInstantiation": "error", + "useValidTypeof": "error" }, "security": { "noGlobalEval": "error" }, "style": { - "noCommaOperator": "error", "noDefaultExport": "error", - "noVar": "warn", "noYodaExpression": "error", "useBlockStatements": "error", "useConsistentBuiltinInstantiation": "error", "useConst": "error", "useSingleVarDeclarator": "error", - "useThrowOnlyError": "error" + "useThrowOnlyError": "error", + "useArrayLiterals": "error" }, "suspicious": { "noAssignInExpressions": "error", @@ -106,10 +113,11 @@ "noUnsafeNegation": "error", "useDefaultSwitchClauseLast": "error", "useGetterReturn": "error", - "useValidTypeof": "error" + "noWith": "error", + "noVar": "warn" } }, - "ignore": ["**/node_modules", "**/*.min.js", "**/build"] + "includes": ["**", "!**/node_modules", "!**/*.min.js", "!**/build"] }, "javascript": { "formatter": { @@ -127,7 +135,7 @@ }, "overrides": [ { - "include": ["*.ts", "*.tsx"], + "includes": ["**/*.ts", "**/*.tsx"], "linter": { "rules": { "complexity": { "noVoid": "error" } } } } ] diff --git a/database/esbuild.config.ts b/database/esbuild.config.ts index 98cb7543f..078228f7d 100644 --- a/database/esbuild.config.ts +++ b/database/esbuild.config.ts @@ -9,6 +9,7 @@ build({ platform: 'node', packages: 'external', outdir: './build', + sourcemap: true, plugins: [ { // hardcode last db version string into index.ts, before parsing diff --git a/database/package.json b/database/package.json index fd146fbc5..21d027427 100644 --- a/database/package.json +++ b/database/package.json @@ -13,29 +13,42 @@ "repository": "https://github.com/gradido/gradido/database", "author": "Gradido Academy - https://www.gradido.net", "license": "Apache-2.0", - "private": false, + "private": true, "scripts": { "build": "tsx ./esbuild.config.ts", "typecheck": "tsc --noEmit", "lint": "biome check --error-on-warnings .", "lint:fix": "biome check --error-on-warnings . --write", "clear": "cross-env TZ=UTC tsx migration/index.ts clear", + "test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test vitest --reporter verbose --no-file-parallelism run", + "test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles", "up": "cross-env TZ=UTC tsx migration/index.ts up", "down": "cross-env TZ=UTC tsx migration/index.ts down", "reset": "cross-env TZ=UTC tsx migration/index.ts reset", + "up:test": "cross-env TZ=UTC DB_DATABASE=gradido_test tsx migration/index.ts up", "up:backend_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_backend tsx migration/index.ts up", "up:federation_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_federation tsx migration/index.ts up", "up:dht_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_dht tsx migration/index.ts up" }, "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", + "@swc-node/register": "^1.10.10", + "@swc/cli": "^0.7.3", + "@swc/core": "^1.11.24", + "@swc/helpers": "^0.5.17", "@types/faker": "^5.5.9", "@types/geojson": "^7946.0.13", - "@types/node": "^17.0.21", - "typescript": "^4.9.5" + "@types/jest": "27.0.2", + "@types/node": "^18.7.14", + "crypto-random-bigint": "^2.1.1", + "jest": "27.2.4", + "ts-jest": "27.0.5", + "ts-node": "^10.9.2", + "typescript": "^4.9.5", + "vitest": "^3.2.4" }, "dependencies": { - "@types/uuid": "^8.3.4", + "@types/uuid": "^8.3.4", "cross-env": "^7.0.3", "decimal.js-light": "^2.5.1", "dotenv": "^10.0.0", @@ -45,9 +58,11 @@ "log4js": "^6.9.1", "mysql2": "^2.3.0", "reflect-metadata": "^0.1.13", + "shared": "*", + "source-map-support": "^0.5.21", "ts-mysql-migrate": "^1.0.2", - "tsx": "^4.19.4", - "typeorm": "^0.3.22", + "tsx": "^4.20.3", + "typeorm": "^0.3.25", "uuid": "^8.3.2", "wkx": "^0.5.0" }, diff --git a/database/src/AppDatabase.ts b/database/src/AppDatabase.ts index 2a6181a26..f995a176a 100644 --- a/database/src/AppDatabase.ts +++ b/database/src/AppDatabase.ts @@ -93,6 +93,7 @@ export class AppDatabase { public async destroy(): Promise { await this.dataSource?.destroy() } + // ###################################### // private methods // ###################################### diff --git a/database/src/config/index.ts b/database/src/config/index.ts index 3eb93ad66..fc6a4bb04 100644 --- a/database/src/config/index.ts +++ b/database/src/config/index.ts @@ -2,6 +2,10 @@ import dotenv from 'dotenv' dotenv.config() +const defaults = { + DEFAULT_LANGUAGE: process.env.DEFAULT_LANGUAGE ?? 'en', +} + const database = { DB_CONNECT_RETRY_COUNT: process.env.DB_CONNECT_RETRY_COUNT ? Number.parseInt(process.env.DB_CONNECT_RETRY_COUNT) @@ -21,4 +25,4 @@ const database = { const PRODUCTION = process.env.NODE_ENV === 'production' || false const nodeEnv = process.env.NODE_ENV || 'development' -export const CONFIG = { ...database, NODE_ENV: nodeEnv, PRODUCTION } +export const CONFIG = { ...database, NODE_ENV: nodeEnv, PRODUCTION, ...defaults } diff --git a/database/src/index.ts b/database/src/index.ts index 1dd0a84d6..3ff4efa17 100644 --- a/database/src/index.ts +++ b/database/src/index.ts @@ -59,4 +59,5 @@ export const entities = [ export { latestDbVersion } export * from './logging' +export * from './queries' export { AppDatabase } from './AppDatabase' diff --git a/database/src/logging/AbstractLogging.view.ts b/database/src/logging/AbstractLogging.view.ts index 4d8824cc3..00fdd4703 100644 --- a/database/src/logging/AbstractLogging.view.ts +++ b/database/src/logging/AbstractLogging.view.ts @@ -23,7 +23,11 @@ export abstract class AbstractLoggingView { public dateToString(date: Date | undefined | null): string | undefined { if (date) { - return date.toISOString() + if (date instanceof Date) { + return date.toISOString() + } else { + return new Date(date).toISOString() + } } return undefined } diff --git a/database/src/logging/PendingTransactionLogging.view.ts b/database/src/logging/PendingTransactionLogging.view.ts index 9cf27be88..fad2d8f56 100644 --- a/database/src/logging/PendingTransactionLogging.view.ts +++ b/database/src/logging/PendingTransactionLogging.view.ts @@ -1,14 +1,7 @@ import { PendingTransaction, Transaction } from '../entity' import { AbstractLoggingView } from './AbstractLogging.view' import { TransactionLoggingView } from './TransactionLogging.view' - -// TODO: move enum into database, maybe rename database -enum PendingTransactionState { - NEW = 1, - PENDING = 2, - SETTLED = 3, - REVERTED = 4, -} +import { PendingTransactionState } from 'shared' export class PendingTransactionLoggingView extends AbstractLoggingView { public constructor(private self: PendingTransaction) { diff --git a/database/src/queries/communities.test.ts b/database/src/queries/communities.test.ts new file mode 100644 index 000000000..9cfb210db --- /dev/null +++ b/database/src/queries/communities.test.ts @@ -0,0 +1,40 @@ +import { Community as DbCommunity } from '..' +import { AppDatabase } from '../AppDatabase' +import { getHomeCommunity } from './communities' +import { describe, expect, it, beforeAll, afterAll } from 'vitest' +import { createCommunity } from '../seeds/homeCommunity' + +const db = AppDatabase.getInstance() + +beforeAll(async () => { + await db.init() +}) +afterAll(async () => { + await db.destroy() +}) + +describe('community.queries', () => { + beforeAll(async () => { + await DbCommunity.clear() + }) + describe('getHomeCommunity', () => { + it('should return null if no home community exists', async () => { + await createCommunity(true) + expect(await getHomeCommunity()).toBeNull() + }) + it('should return the home community', async () => { + const homeCom = await createCommunity(false) + const community = await getHomeCommunity() + expect(community).toBeDefined() + expect(community?.name).toBe(homeCom.name) + expect(community?.description).toBe(homeCom.description) + expect(community?.url).toBe(homeCom.url) + expect(community?.creationDate).toStrictEqual(homeCom.creationDate) + expect(community?.communityUuid).toBe(homeCom.communityUuid) + expect(community?.authenticatedAt).toStrictEqual(homeCom.authenticatedAt) + expect(community?.foreign).toBe(homeCom.foreign) + expect(community?.publicKey).toStrictEqual(homeCom.publicKey) + expect(community?.privateKey).toStrictEqual(homeCom.privateKey) + }) + }) +}) \ No newline at end of file diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts new file mode 100644 index 000000000..868db42e8 --- /dev/null +++ b/database/src/queries/communities.ts @@ -0,0 +1,11 @@ +import { Community as DbCommunity } from '../entity/Community' + +/** + * Retrieves the home community, i.e., a community that is not foreign. + * @returns A promise that resolves to the home community, or null if no home community was found + */ +export async function getHomeCommunity(): Promise { + return await DbCommunity.findOne({ + where: { foreign: false }, + }) +} \ No newline at end of file diff --git a/database/src/queries/index.ts b/database/src/queries/index.ts new file mode 100644 index 000000000..9acf80871 --- /dev/null +++ b/database/src/queries/index.ts @@ -0,0 +1,7 @@ +import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const' + +export * from './user' +export * from './communities' +export * from './pendingTransactions' + +export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries` diff --git a/database/src/queries/pendingTransactions.test.ts b/database/src/queries/pendingTransactions.test.ts new file mode 100644 index 000000000..72deaac71 --- /dev/null +++ b/database/src/queries/pendingTransactions.test.ts @@ -0,0 +1,124 @@ +import { + PendingTransaction as DbPendingTransaction, + User as DbUser, + UserContact as DbUserContact, + Community as DbCommunity +} from '..' +import { countOpenPendingTransactions } from './pendingTransactions' +import { PendingTransactionState } from 'shared' +import { AppDatabase } from '../AppDatabase' +import { userFactory } from '../seeds/factory/user' +import { pendingTransactionFactory } from '../seeds/factory/pendingTransaction' +import { bibiBloxberg } from '../seeds/users/bibi-bloxberg' +import { peterLustig } from '../seeds/users/peter-lustig' +import { bobBaumeister } from '../seeds/users/bob-baumeister' +import { garrickOllivander } from '../seeds/users/garrick-ollivander' +import { describe, expect, it, beforeAll, afterAll } from 'vitest' +import { createCommunity } from '../seeds/homeCommunity' +import { v4 as uuidv4 } from 'uuid' +import Decimal from 'decimal.js-light' + +const db = AppDatabase.getInstance() + +beforeAll(async () => { + await db.init() +}) +afterAll(async () => { + await db.destroy() +}) + + +describe('countOpenPendingTransactions', () => { + let bibi: DbUser + let peter: DbUser + let bob: DbUser + let garrick: DbUser + beforeAll(async () => { + await DbPendingTransaction.clear() + await DbUser.clear() + await DbUserContact.clear() + await DbCommunity.clear() + + await createCommunity(false) + + bibi = await userFactory(bibiBloxberg) + peter = await userFactory(peterLustig) + bob = await userFactory(bobBaumeister) + garrick = await userFactory(garrickOllivander) + + // Bibi -> Peter + await pendingTransactionFactory( + bibi, + peter, + new Decimal(10), + 'Bibi -> Peter new', + PendingTransactionState.NEW + ) + await pendingTransactionFactory( + bibi, + peter, + new Decimal(100.01), + 'Bibi -> Peter settled', + PendingTransactionState.SETTLED + ) + + // Peter -> Bibi + await pendingTransactionFactory( + peter, + bibi, + new Decimal(12), + 'Peter -> Bibi new', + PendingTransactionState.NEW + ) + + // Bob -> Peter + await pendingTransactionFactory( + bob, + peter, + new Decimal(17.1), + 'Bob -> Peter new', + PendingTransactionState.NEW + ) + + }) + it('should return 0 if called with empty array', async () => { + const count = await countOpenPendingTransactions([]) + expect(count).toBe(0) + }) + + it('should return 0 if called with unknown gradido id', async () => { + const count = await countOpenPendingTransactions([uuidv4()]) + expect(count).toBe(0) + }) + + it('should return 0 if there are no pending transactions for the given user', async () => { + const count = await countOpenPendingTransactions([garrick.gradidoID]) + expect(count).toBe(0) + }) + + it('bibi and peter have two transactions together and peter one additional, should return 3', async () => { + const count = await countOpenPendingTransactions([bibi.gradidoID, peter.gradidoID]) + expect(count).toBe(3) + }) + + it('peter and bob have one transaction together, peter two additional, should return 3', async () => { + const count = await countOpenPendingTransactions([peter.gradidoID, bob.gradidoID]) + expect(count).toBe(3) + }) + + it('peter has three transactions, should return 3', async () => { + const count = await countOpenPendingTransactions([peter.gradidoID]) + expect(count).toBe(3) + }) + + + it('bibi has two transactions, should return 2', async () => { + const count = await countOpenPendingTransactions([bibi.gradidoID]) + expect(count).toBe(2) + }) + + it('bob has one transaction, should return 1', async () => { + const count = await countOpenPendingTransactions([bob.gradidoID]) + expect(count).toBe(1) + }) +}) diff --git a/database/src/queries/pendingTransactions.ts b/database/src/queries/pendingTransactions.ts new file mode 100644 index 000000000..44bf63f82 --- /dev/null +++ b/database/src/queries/pendingTransactions.ts @@ -0,0 +1,18 @@ +import { PendingTransaction as DbPendingTransaction } from '../entity' +import { In } from 'typeorm' +import { PendingTransactionState } from 'shared' + +/** + * Counts the number of open pending transactions for the given users. + * @param users The users gradidoID to count the pending transactions for + * @returns The number of open pending transactions + */ +export async function countOpenPendingTransactions(users: string[]): Promise { + const count = await DbPendingTransaction.count({ + where: [ + { userGradidoID: In(users), state: PendingTransactionState.NEW }, + { linkedUserGradidoID: In(users), state: PendingTransactionState.NEW }, + ], + }) + return count +} \ No newline at end of file diff --git a/database/src/queries/user.test.ts b/database/src/queries/user.test.ts new file mode 100644 index 000000000..8cb95e0ac --- /dev/null +++ b/database/src/queries/user.test.ts @@ -0,0 +1,140 @@ +import { User as DbUser, UserContact as DbUserContact, Community as DbCommunity } from '..' +import { AppDatabase } from '../AppDatabase' +import { aliasExists, findUserByIdentifier } from './user' +import { userFactory } from '../seeds/factory/user' +import { bibiBloxberg } from '../seeds/users/bibi-bloxberg' +import { describe, expect, it, beforeAll, afterAll, beforeEach, } from 'vitest' +import { createCommunity } from '../seeds/homeCommunity' +import { peterLustig } from '../seeds/users/peter-lustig' +import { bobBaumeister } from '../seeds/users/bob-baumeister' +import { getLogger, printLogs, clearLogs } from '../../../config-schema/test/testSetup.vitest' +import { LOG4JS_QUERIES_CATEGORY_NAME } from '.' + +const db = AppDatabase.getInstance() +const userIdentifierLoggerName = `${LOG4JS_QUERIES_CATEGORY_NAME}.user.findUserByIdentifier` + +beforeAll(async () => { + await db.init() +}) +afterAll(async () => { + await db.destroy() +}) + +describe('user.queries', () => { + describe('aliasExists', () => { + beforeAll(async () => { + await DbUser.clear() + await DbUserContact.clear() + + const bibi = bibiBloxberg + bibi.alias = 'b-b' + await userFactory(bibi) + }) + + it('should return true if alias exists', async () => { + expect(await aliasExists('b-b')).toBe(true) + }) + + it('should return true if alias exists even with deviating casing', async () => { + expect(await aliasExists('b-B')).toBe(true) + }) + + it('should return false if alias does not exist', async () => { + expect(await aliasExists('bibi')).toBe(false) + }) + }) + + describe('findUserByIdentifier', () => { + let homeCom: DbCommunity + let communityUuid: string + let communityName: string + let userBibi: DbUser + + beforeAll(async () => { + await DbUser.clear() + await DbUserContact.clear() + await DbCommunity.clear() + + homeCom = await createCommunity(false) + communityUuid = homeCom.communityUuid! + communityName = homeCom.name! + userBibi = await userFactory(bibiBloxberg) + await userFactory(peterLustig) + await userFactory(bobBaumeister) + }) + beforeEach(() => { + clearLogs() + }) + describe('communityIdentifier is community uuid', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID, communityUuid) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is alias', async () => { + const user = await findUserByIdentifier(userBibi.alias, communityUuid) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is email', async () => { + const user = await findUserByIdentifier(userBibi.emailContact.email, communityUuid) + expect(user).toMatchObject(userBibi) + }) + it('userIdentifier is unknown', async () => { + const user = await findUserByIdentifier('unknown', communityUuid) + expect(user).toBeNull() + }) + }) + + describe('communityIdentifier is community name', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID, communityName) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is alias', async () => { + const user = await findUserByIdentifier(userBibi.alias, communityName) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is email', async () => { + const user = await findUserByIdentifier(userBibi.emailContact.email, communityName) + expect(user).toMatchObject(userBibi) + }) + }) + describe('communityIdentifier is unknown', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID, 'unknown') + expect(user).toBeNull() + }) + it('userIdentifier is unknown', async () => { + const user = await findUserByIdentifier('unknown', communityUuid) + expect(user).toBeNull() + }) + }) + describe('communityIdentifier is empty', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is alias', async () => { + const user = await findUserByIdentifier(userBibi.alias) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is email', async () => { + const user = await findUserByIdentifier(userBibi.emailContact.email) + expect(user).toMatchObject(userBibi) + }) + it('userIdentifier is unknown type', async () => { + const user = await findUserByIdentifier('sa') + printLogs() + expect(getLogger(userIdentifierLoggerName).warn).toHaveBeenCalledWith('Unknown identifier type', 'sa') + expect(user).toBeNull() + }) + }) + }) +}) + + diff --git a/database/src/queries/user.ts b/database/src/queries/user.ts new file mode 100644 index 000000000..b968e9433 --- /dev/null +++ b/database/src/queries/user.ts @@ -0,0 +1,62 @@ +import { Raw } from 'typeorm' +import { Community, User as DbUser, UserContact as DbUserContact } from '../entity' +import { FindOptionsWhere } from 'typeorm' +import { aliasSchema, emailSchema, uuidv4Schema, urlSchema } from 'shared' +import { getLogger } from 'log4js' +import { LOG4JS_QUERIES_CATEGORY_NAME } from './index' + +export async function aliasExists(alias: string): Promise { + const user = await DbUser.findOne({ + where: { alias: Raw((a) => `LOWER(${a}) = LOWER(:alias)`, { alias }) }, + }) + return user !== null +} + +/** + * + * @param identifier could be gradidoID, alias or email of user + * @param communityIdentifier could be uuid or name of community + * @returns + */ +export const findUserByIdentifier = async ( + identifier: string, + communityIdentifier?: string, +): Promise => { + const communityWhere: FindOptionsWhere = urlSchema.safeParse(communityIdentifier).success + ? { url: communityIdentifier } + : uuidv4Schema.safeParse(communityIdentifier).success + ? { communityUuid: communityIdentifier } + : { name: communityIdentifier } + + if (uuidv4Schema.safeParse(identifier).success) { + return DbUser.findOne({ + where: { gradidoID: identifier, community: communityWhere }, + relations: ['emailContact', 'community'], + }) + } else if (emailSchema.safeParse(identifier).success) { + const userContact = await DbUserContact.findOne({ + where: { + email: identifier, + emailChecked: true, + user: { + community: communityWhere, + }, + }, + relations: { user: { community: true } }, + }) + if (userContact) { + // TODO: remove circular reference + const user = userContact.user + user.emailContact = userContact + return user + } + } else if (aliasSchema.safeParse(identifier).success) { + return await DbUser.findOne({ + where: { alias: identifier, community: communityWhere }, + relations: ['emailContact', 'community'], + }) + } + // should don't happen often, so we create only in the rare case a logger for it + getLogger(`${LOG4JS_QUERIES_CATEGORY_NAME}.user.findUserByIdentifier`).warn('Unknown identifier type', identifier) + return null +} diff --git a/database/src/seeds/factory/pendingTransaction.ts b/database/src/seeds/factory/pendingTransaction.ts new file mode 100644 index 000000000..2e8c7d256 --- /dev/null +++ b/database/src/seeds/factory/pendingTransaction.ts @@ -0,0 +1,23 @@ +import { User as DbUser, PendingTransaction as DbPendingTransaction } from '../..' +import { PendingTransactionState } from 'shared' +import { Decimal } from 'decimal.js-light' + +export async function pendingTransactionFactory( + sender: DbUser, + receiver: DbUser, + amount: Decimal, + memo: string, + state: PendingTransactionState, +) { + const pendingTransaction = new DbPendingTransaction() + pendingTransaction.state = state + pendingTransaction.memo = memo + pendingTransaction.amount = amount + pendingTransaction.userId = sender.id + pendingTransaction.userGradidoID = sender.gradidoID + pendingTransaction.userCommunityUuid = sender.communityUuid! + pendingTransaction.linkedUserId = receiver.id + pendingTransaction.linkedUserGradidoID = receiver.gradidoID + pendingTransaction.linkedUserCommunityUuid = receiver.communityUuid! + await pendingTransaction.save() +} diff --git a/database/src/seeds/factory/user.ts b/database/src/seeds/factory/user.ts new file mode 100644 index 000000000..3772fe66d --- /dev/null +++ b/database/src/seeds/factory/user.ts @@ -0,0 +1,45 @@ +import { UserInterface } from '../users/UserInterface' +import { User, UserContact } from '../../entity' +import { v4 } from 'uuid' +import { UserContactType, OptInType, PasswordEncryptionType } from 'shared' +import { getHomeCommunity } from '../../queries/communities' +import random from 'crypto-random-bigint' + +export const userFactory = async (user: UserInterface): Promise => { + let dbUserContact = new UserContact() + + dbUserContact.email = user.email ?? '' + dbUserContact.type = UserContactType.USER_CONTACT_EMAIL + + let dbUser = new User() + dbUser.firstName = user.firstName ?? '' + dbUser.lastName = user.lastName ?? '' + if (user.alias) { + dbUser.alias = user.alias + } + dbUser.language = user.language ?? 'en' + dbUser.createdAt = user.createdAt ?? new Date() + dbUser.deletedAt = user.deletedAt ?? null + dbUser.publisherId = user.publisherId ?? 0 + dbUser.gradidoID = v4() + + if (user.emailChecked) { + dbUserContact.emailVerificationCode = random(64).toString() + dbUserContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER + dbUserContact.emailChecked = true + dbUser.password = random(64) + dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID + } + const homeCommunity = await getHomeCommunity() + if (homeCommunity) { + dbUser.community = homeCommunity + dbUser.communityUuid = homeCommunity.communityUuid! + } + // TODO: improve with cascade + dbUser = await dbUser.save() + dbUserContact.userId = dbUser.id + dbUserContact = await dbUserContact.save() + dbUser.emailId = dbUserContact.id + dbUser.emailContact = dbUserContact + return dbUser.save() +} \ No newline at end of file diff --git a/database/src/seeds/homeCommunity.ts b/database/src/seeds/homeCommunity.ts new file mode 100644 index 000000000..d76ee0b98 --- /dev/null +++ b/database/src/seeds/homeCommunity.ts @@ -0,0 +1,26 @@ +import { Community } from '../entity' +import { randomBytes } from 'node:crypto' +import { v4 as uuidv4 } from 'uuid' + +export async function createCommunity(foreign: boolean): Promise { + const homeCom = new Community() + homeCom.publicKey = randomBytes(32) + homeCom.communityUuid = uuidv4() + homeCom.authenticatedAt = new Date() + homeCom.name = 'HomeCommunity-name' + homeCom.creationDate = new Date() + + if(foreign) { + homeCom.foreign = true + homeCom.name = 'ForeignCommunity-name' + homeCom.description = 'ForeignCommunity-description' + homeCom.url = 'http://foreign/api' + } else { + homeCom.foreign = false + homeCom.privateKey = randomBytes(64) + homeCom.name = 'HomeCommunity-name' + homeCom.description = 'HomeCommunity-description' + homeCom.url = 'http://localhost/api' + } + return await homeCom.save() +} diff --git a/database/src/seeds/users/UserInterface.ts b/database/src/seeds/users/UserInterface.ts new file mode 100644 index 000000000..1fb1b128a --- /dev/null +++ b/database/src/seeds/users/UserInterface.ts @@ -0,0 +1,13 @@ +export interface UserInterface { + alias?: string + email?: string + firstName?: string + lastName?: string + // description?: string + createdAt?: Date + emailChecked?: boolean + language?: string + deletedAt?: Date + publisherId?: number + role?: string +} diff --git a/database/src/seeds/users/bibi-bloxberg.ts b/database/src/seeds/users/bibi-bloxberg.ts new file mode 100644 index 000000000..9a40e922b --- /dev/null +++ b/database/src/seeds/users/bibi-bloxberg.ts @@ -0,0 +1,12 @@ +import { UserInterface } from './UserInterface' + +export const bibiBloxberg: UserInterface = { + email: 'bibi@bloxberg.de', + firstName: 'Bibi', + lastName: 'Bloxberg', + alias: 'BBB', + // description: 'Hex Hex', + emailChecked: true, + language: 'de', + publisherId: 1234, +} diff --git a/database/src/seeds/users/bob-baumeister.ts b/database/src/seeds/users/bob-baumeister.ts new file mode 100644 index 000000000..b7902f7bc --- /dev/null +++ b/database/src/seeds/users/bob-baumeister.ts @@ -0,0 +1,11 @@ +import { UserInterface } from './UserInterface' + +export const bobBaumeister: UserInterface = { + alias: 'MeisterBob', + email: 'bob@baumeister.de', + firstName: 'Bob', + lastName: 'der Baumeister', + // description: 'Können wir das schaffen? Ja, wir schaffen das!', + emailChecked: true, + language: 'de', +} diff --git a/database/src/seeds/users/garrick-ollivander.ts b/database/src/seeds/users/garrick-ollivander.ts new file mode 100644 index 000000000..264a866bd --- /dev/null +++ b/database/src/seeds/users/garrick-ollivander.ts @@ -0,0 +1,12 @@ +import { UserInterface } from './UserInterface' + +export const garrickOllivander: UserInterface = { + email: 'garrick@ollivander.com', + firstName: 'Garrick', + lastName: 'Ollivander', + // description: `Curious ... curious ... + // Renowned wandmaker Mr Ollivander owns the wand shop Ollivanders: Makers of Fine Wands Since 382 BC in Diagon Alley. His shop is widely considered the best place to purchase a wand.`, + createdAt: new Date('2022-01-10T10:23:17'), + emailChecked: false, + language: 'en', +} diff --git a/database/src/seeds/users/index.ts b/database/src/seeds/users/index.ts new file mode 100644 index 000000000..beb6c6f25 --- /dev/null +++ b/database/src/seeds/users/index.ts @@ -0,0 +1,15 @@ +import { bibiBloxberg } from './bibi-bloxberg' +import { bobBaumeister } from './bob-baumeister' +import { garrickOllivander } from './garrick-ollivander' +import { peterLustig } from './peter-lustig' +import { raeuberHotzenplotz } from './raeuber-hotzenplotz' +import { stephenHawking } from './stephen-hawking' + +export const users = [ + peterLustig, + bibiBloxberg, + bobBaumeister, + raeuberHotzenplotz, + stephenHawking, + garrickOllivander, +] diff --git a/database/src/seeds/users/peter-lustig.ts b/database/src/seeds/users/peter-lustig.ts new file mode 100644 index 000000000..58b07fe99 --- /dev/null +++ b/database/src/seeds/users/peter-lustig.ts @@ -0,0 +1,11 @@ +import { UserInterface } from './UserInterface' + +export const peterLustig: UserInterface = { + email: 'peter@lustig.de', + firstName: 'Peter', + lastName: 'Lustig', + // description: 'Latzhose und Nickelbrille', + createdAt: new Date('2020-11-25T10:48:43'), + emailChecked: true, + language: 'de' +} diff --git a/database/src/seeds/users/raeuber-hotzenplotz.ts b/database/src/seeds/users/raeuber-hotzenplotz.ts new file mode 100644 index 000000000..62601efff --- /dev/null +++ b/database/src/seeds/users/raeuber-hotzenplotz.ts @@ -0,0 +1,10 @@ +import { UserInterface } from './UserInterface' + +export const raeuberHotzenplotz: UserInterface = { + email: 'raeuber@hotzenplotz.de', + firstName: 'Räuber', + lastName: 'Hotzenplotz', + // description: 'Pfefferpistole', + emailChecked: true, + language: 'de', +} diff --git a/database/src/seeds/users/stephen-hawking.ts b/database/src/seeds/users/stephen-hawking.ts new file mode 100644 index 000000000..a683b7579 --- /dev/null +++ b/database/src/seeds/users/stephen-hawking.ts @@ -0,0 +1,12 @@ +import { UserInterface } from './UserInterface' + +export const stephenHawking: UserInterface = { + email: 'stephen@hawking.uk', + firstName: 'Stephen', + lastName: 'Hawking', + // description: 'A Brief History of Time', + emailChecked: true, + createdAt: new Date('1942-01-08T09:17:52'), + deletedAt: new Date('2018-03-14T09:17:52'), + language: 'en', +} diff --git a/database/tsconfig.json b/database/tsconfig.json index fc32bbbab..ec6769fa2 100644 --- a/database/tsconfig.json +++ b/database/tsconfig.json @@ -50,7 +50,7 @@ //"@/*": ["src/*"], /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ //}, // "rootDirs": [".", "../database"], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ + "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ // "types": ["node"], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ @@ -71,5 +71,9 @@ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, - "references": [] /* Any project that is referenced must itself have a `references` array (which may be empty). */ + "references": [], /* Any project that is referenced must itself have a `references` array (which may be empty). */ + "ts-node": { + "swc": true + }, + "exclude": ["**/*.test.ts", "**/*.spec.ts", "test/*"] } diff --git a/database/turbo.json b/database/turbo.json index 8339cc11f..8ea6e46f5 100644 --- a/database/turbo.json +++ b/database/turbo.json @@ -13,10 +13,16 @@ "up:dht_test": { "cache": false }, + "up:test": { + "cache": false + }, "up": { "cache": false, "dependsOn": ["build"] }, + "test": { + "dependsOn": ["up:test", "^build"] + }, "down": { "cache": false, "dependsOn": ["build"] diff --git a/database/vitest.config.js b/database/vitest.config.js new file mode 100644 index 000000000..b0e269a1a --- /dev/null +++ b/database/vitest.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + setupFiles: '../config-schema/test/testSetup.vitest.ts', + }, +}) \ No newline at end of file diff --git a/database/yarn.lock b/database/yarn.lock deleted file mode 100644 index 03a87b110..000000000 --- a/database/yarn.lock +++ /dev/null @@ -1,1060 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/runtime@^7.21.0": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec" - integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA== - dependencies: - regenerator-runtime "^0.13.11" - -"@biomejs/biome@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.9.4.tgz#89766281cbc3a0aae865a7ff13d6aaffea2842bf" - integrity sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog== - optionalDependencies: - "@biomejs/cli-darwin-arm64" "1.9.4" - "@biomejs/cli-darwin-x64" "1.9.4" - "@biomejs/cli-linux-arm64" "1.9.4" - "@biomejs/cli-linux-arm64-musl" "1.9.4" - "@biomejs/cli-linux-x64" "1.9.4" - "@biomejs/cli-linux-x64-musl" "1.9.4" - "@biomejs/cli-win32-arm64" "1.9.4" - "@biomejs/cli-win32-x64" "1.9.4" - -"@biomejs/cli-darwin-arm64@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz#dfa376d23a54a2d8f17133c92f23c1bf2e62509f" - integrity sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw== - -"@biomejs/cli-darwin-x64@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz#eafc2ce3849d385fc02238aad1ca4a73395a64d9" - integrity sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg== - -"@biomejs/cli-linux-arm64-musl@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz#d780c3e01758fc90f3268357e3f19163d1f84fca" - integrity sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA== - -"@biomejs/cli-linux-arm64@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz#8ed1dd0e89419a4b66a47f95aefb8c46ae6041c9" - integrity sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g== - -"@biomejs/cli-linux-x64-musl@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz#f36982b966bd671a36671e1de4417963d7db15fb" - integrity sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg== - -"@biomejs/cli-linux-x64@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz#a0a7f56680c76b8034ddc149dbf398bdd3a462e8" - integrity sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg== - -"@biomejs/cli-win32-arm64@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz#e2ef4e0084e76b7e26f0fc887c5ef1265ea56200" - integrity sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg== - -"@biomejs/cli-win32-x64@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz#4c7afa90e3970213599b4095e62f87e5972b2340" - integrity sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA== - -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@esbuild/aix-ppc64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz#014180d9a149cffd95aaeead37179433f5ea5437" - integrity sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ== - -"@esbuild/android-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz#649e47e04ddb24a27dc05c395724bc5f4c55cbfe" - integrity sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ== - -"@esbuild/android-arm@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.3.tgz#8a0f719c8dc28a4a6567ef7328c36ea85f568ff4" - integrity sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A== - -"@esbuild/android-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.3.tgz#e2ab182d1fd06da9bef0784a13c28a7602d78009" - integrity sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ== - -"@esbuild/darwin-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz#c7f3166fcece4d158a73dcfe71b2672ca0b1668b" - integrity sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w== - -"@esbuild/darwin-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz#d8c5342ec1a4bf4b1915643dfe031ba4b173a87a" - integrity sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A== - -"@esbuild/freebsd-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz#9f7d789e2eb7747d4868817417cc968ffa84f35b" - integrity sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw== - -"@esbuild/freebsd-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz#8ad35c51d084184a8e9e76bb4356e95350a64709" - integrity sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q== - -"@esbuild/linux-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz#3af0da3d9186092a9edd4e28fa342f57d9e3cd30" - integrity sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A== - -"@esbuild/linux-arm@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz#e91cafa95e4474b3ae3d54da12e006b782e57225" - integrity sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ== - -"@esbuild/linux-ia32@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz#81025732d85b68ee510161b94acdf7e3007ea177" - integrity sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw== - -"@esbuild/linux-loong64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz#3c744e4c8d5e1148cbe60a71a11b58ed8ee5deb8" - integrity sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g== - -"@esbuild/linux-mips64el@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz#1dfe2a5d63702db9034cc6b10b3087cc0424ec26" - integrity sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag== - -"@esbuild/linux-ppc64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz#2e85d9764c04a1ebb346dc0813ea05952c9a5c56" - integrity sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg== - -"@esbuild/linux-riscv64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz#a9ea3334556b09f85ccbfead58c803d305092415" - integrity sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA== - -"@esbuild/linux-s390x@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz#f6a7cb67969222b200974de58f105dfe8e99448d" - integrity sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ== - -"@esbuild/linux-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz#a237d3578ecdd184a3066b1f425e314ade0f8033" - integrity sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA== - -"@esbuild/netbsd-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz#4c15c68d8149614ddb6a56f9c85ae62ccca08259" - integrity sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA== - -"@esbuild/netbsd-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz#12f6856f8c54c2d7d0a8a64a9711c01a743878d5" - integrity sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g== - -"@esbuild/openbsd-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz#ca078dad4a34df192c60233b058db2ca3d94bc5c" - integrity sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ== - -"@esbuild/openbsd-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz#c9178adb60e140e03a881d0791248489c79f95b2" - integrity sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w== - -"@esbuild/sunos-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz#03765eb6d4214ff27e5230af779e80790d1ee09f" - integrity sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA== - -"@esbuild/win32-arm64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz#f1c867bd1730a9b8dfc461785ec6462e349411ea" - integrity sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ== - -"@esbuild/win32-ia32@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz#77491f59ef6c9ddf41df70670d5678beb3acc322" - integrity sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew== - -"@esbuild/win32-x64@0.25.3": - version "0.25.3" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz#b17a2171f9074df9e91bfb07ef99a892ac06412a" - integrity sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg== - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@sqltools/formatter@^1.2.5": - version "1.2.5" - resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12" - integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw== - -"@tsconfig/node10@^1.0.7": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" - integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== - -"@tsconfig/node12@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" - integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== - -"@tsconfig/node14@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" - integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== - -"@tsconfig/node16@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" - integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== - -"@types/faker@^5.5.9": - version "5.5.9" - resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.5.9.tgz#588ede92186dc557bff8341d294335d50d255f0c" - integrity sha512-uCx6mP3UY5SIO14XlspxsGjgaemrxpssJI0Ol+GfhxtcKpv9pgRZYsS4eeKeHVLje6Qtc8lGszuBI461+gVZBA== - -"@types/geojson@^7946.0.13": - version "7946.0.13" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.13.tgz#e6e77ea9ecf36564980a861e24e62a095988775e" - integrity sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ== - -"@types/mysql@^2.15.8": - version "2.15.19" - resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.19.tgz#d158927bb7c1a78f77e56de861a3b15cae0e7aed" - integrity sha512-wSRg2QZv14CWcZXkgdvHbbV2ACufNy5EgI8mBBxnJIptchv7DBy/h53VMa2jDhyo0C9MO4iowE6z9vF8Ja1DkQ== - dependencies: - "@types/node" "*" - -"@types/node@*": - version "16.7.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.1.tgz#c6b9198178da504dfca1fd0be9b2e1002f1586f0" - integrity sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A== - -"@types/node@^17.0.21": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== - -"@types/uuid@^8.3.4": - version "8.3.4" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" - integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== - -acorn-walk@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.1.1.tgz#3ddab7f84e4a7e2313f6c414c5b7dac85f4e3ebc" - integrity sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w== - -acorn@^8.4.1: - version "8.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" - integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - -app-root-path@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86" - integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA== - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bignumber.js@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" - integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -chalk@^4.0.0, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -cli-highlight@^2.1.11: - version "2.1.11" - resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.11.tgz#49736fa452f0aaf4fae580e30acb26828d2dc1bf" - integrity sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg== - dependencies: - chalk "^4.0.0" - highlight.js "^10.7.1" - mz "^2.4.0" - parse5 "^5.1.1" - parse5-htmlparser2-tree-adapter "^6.0.0" - yargs "^16.0.0" - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== - dependencies: - cross-spawn "^7.0.1" - -cross-spawn@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" - integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== - -date-fns@^2.29.3: - version "2.30.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" - integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== - dependencies: - "@babel/runtime" "^7.21.0" - -debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -decimal.js-light@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" - integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== - -denque@^1.4.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -dotenv@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" - integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== - -dotenv@^16.0.3: - version "16.3.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" - integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -esbuild@~0.25.0: - version "0.25.3" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.3.tgz#371f7cb41283e5b2191a96047a7a89562965a285" - integrity sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q== - optionalDependencies: - "@esbuild/aix-ppc64" "0.25.3" - "@esbuild/android-arm" "0.25.3" - "@esbuild/android-arm64" "0.25.3" - "@esbuild/android-x64" "0.25.3" - "@esbuild/darwin-arm64" "0.25.3" - "@esbuild/darwin-x64" "0.25.3" - "@esbuild/freebsd-arm64" "0.25.3" - "@esbuild/freebsd-x64" "0.25.3" - "@esbuild/linux-arm" "0.25.3" - "@esbuild/linux-arm64" "0.25.3" - "@esbuild/linux-ia32" "0.25.3" - "@esbuild/linux-loong64" "0.25.3" - "@esbuild/linux-mips64el" "0.25.3" - "@esbuild/linux-ppc64" "0.25.3" - "@esbuild/linux-riscv64" "0.25.3" - "@esbuild/linux-s390x" "0.25.3" - "@esbuild/linux-x64" "0.25.3" - "@esbuild/netbsd-arm64" "0.25.3" - "@esbuild/netbsd-x64" "0.25.3" - "@esbuild/openbsd-arm64" "0.25.3" - "@esbuild/openbsd-x64" "0.25.3" - "@esbuild/sunos-x64" "0.25.3" - "@esbuild/win32-arm64" "0.25.3" - "@esbuild/win32-ia32" "0.25.3" - "@esbuild/win32-x64" "0.25.3" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -generate-function@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" - integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== - dependencies: - is-property "^1.0.2" - -geojson@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0" - integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-tsconfig@^4.7.5: - version "4.10.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz#403a682b373a823612475a4c2928c7326fc0f6bb" - integrity sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A== - dependencies: - resolve-pkg-maps "^1.0.0" - -glob@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -highlight.js@^10.7.1: - version "10.7.3" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" - integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-property@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -long@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== - -lru-cache@^4.1.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -mkdirp@^2.1.3: - version "2.1.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19" - integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A== - -mkdirp@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" - integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -mysql2@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.3.0.tgz#600f5cc27e397dfb77b59eac93666434f88e8079" - integrity sha512-0t5Ivps5Tdy5YHk5NdKwQhe/4Qyn2pload+S+UooDBvsqngtzujG1BaTWBihQLfeKO3t3122/GtusBtmHEHqww== - dependencies: - denque "^1.4.1" - generate-function "^2.3.1" - iconv-lite "^0.6.2" - long "^4.0.0" - lru-cache "^6.0.0" - named-placeholders "^1.1.2" - seq-queue "^0.0.5" - sqlstring "^2.3.2" - -mysql@^2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" - integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== - dependencies: - bignumber.js "9.0.0" - readable-stream "2.3.7" - safe-buffer "5.1.2" - sqlstring "2.3.1" - -mz@^2.4.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - -named-placeholders@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.2.tgz#ceb1fbff50b6b33492b5cf214ccf5e39cef3d0e8" - integrity sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA== - dependencies: - lru-cache "^4.1.3" - -ncp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" - integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA== - -object-assign@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -parse5-htmlparser2-tree-adapter@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" - integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== - dependencies: - parse5 "^6.0.1" - -parse5@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" - integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== - -parse5@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -prettier@^2.8.7: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -readable-stream@2.3.7: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -reflect-metadata@^0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" - integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== - -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@^5.0.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -seq-queue@^0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" - integrity sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4= - -sha.js@^2.4.11: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -sqlstring@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" - integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= - -sqlstring@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.2.tgz#cdae7169389a1375b18e885f2e60b3e460809514" - integrity sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg== - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.1" - resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" - integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== - dependencies: - any-promise "^1.0.0" - -ts-mysql-migrate@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ts-mysql-migrate/-/ts-mysql-migrate-1.0.2.tgz#736d37c3aa3fef92f226b869098e939950d0e18c" - integrity sha512-zDW6iQsfPCJfQ3JMhfUGjhy8aK+VNTvPrXmJH66PB2EGEvyn4m7x2nBdhDNhKuwYU9LMxW1p+l39Ei+btXNpxA== - dependencies: - "@types/mysql" "^2.15.8" - mysql "^2.18.1" - -ts-node@^10.9.2: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tslib@^2.5.0: - version "2.5.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" - integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== - -tsx@^4.19.3: - version "4.19.3" - resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.19.3.tgz#2bdbcb87089374d933596f8645615142ed727666" - integrity sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ== - dependencies: - esbuild "~0.25.0" - get-tsconfig "^4.7.5" - optionalDependencies: - fsevents "~2.3.3" - -typeorm@^0.3.16: - version "0.3.17" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.17.tgz#a73c121a52e4fbe419b596b244777be4e4b57949" - integrity sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig== - dependencies: - "@sqltools/formatter" "^1.2.5" - app-root-path "^3.1.0" - buffer "^6.0.3" - chalk "^4.1.2" - cli-highlight "^2.1.11" - date-fns "^2.29.3" - debug "^4.3.4" - dotenv "^16.0.3" - glob "^8.1.0" - mkdirp "^2.1.3" - reflect-metadata "^0.1.13" - sha.js "^2.4.11" - tslib "^2.5.0" - uuid "^9.0.0" - yargs "^17.6.2" - -typescript@^4.9.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -uuid@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== - -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wkx@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" - integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== - dependencies: - "@types/node" "*" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^16.0.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yargs@^17.6.2: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/dht-node/Dockerfile b/dht-node/Dockerfile index bd397fea0..c19888a5d 100644 --- a/dht-node/Dockerfile +++ b/dht-node/Dockerfile @@ -116,8 +116,5 @@ COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/dht-node/build/index.js ./in # add node_modules from production_node_modules COPY --chown=app:app --from=production-node-modules ${DOCKER_WORKDIR}/node_modules ./node_modules -# Copy log4js-config.json to provide log configuration -COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/dht-node/log4js-config.json ./log4js-config.json - # Run command CMD ["node", "index.js"] diff --git a/dht-node/esbuild.config.ts b/dht-node/esbuild.config.ts index f38039c43..42d9c329d 100644 --- a/dht-node/esbuild.config.ts +++ b/dht-node/esbuild.config.ts @@ -10,5 +10,5 @@ build({ // legalComments: 'inline', external: ['dht-rpc', 'sodium-universal'], minify: true, - sourcemap: false, + sourcemap: true, }) diff --git a/dht-node/jest.config.js b/dht-node/jest.config.js index 18170ac48..fe5fb923d 100644 --- a/dht-node/jest.config.js +++ b/dht-node/jest.config.js @@ -1,19 +1,22 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { - verbose: true, + verbose: false, preset: 'ts-jest', - collectCoverage: true, - collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], + collectCoverage: false, + collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!build/**'], coverageThreshold: { global: { lines: 82, }, }, - setupFiles: ['/test/testSetup.ts'], + setupFiles: ['config-schema/test/testSetup.ts'], setupFilesAfterEnv: [], modulePathIgnorePatterns: ['/build/'], moduleNameMapper: { '@/(.*)': '/src/$1', '@test/(.*)': '/test/$1', }, + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, } diff --git a/dht-node/log4js-config.json b/dht-node/log4js-config.json deleted file mode 100644 index ee5207550..000000000 --- a/dht-node/log4js-config.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "appenders": - { - "dht": - { - "type": "dateFile", - "filename": "../logs/dht-node/apiversion-%v.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "errorFile": - { - "type": "dateFile", - "filename": "../logs/dht-node/errors.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "errors": - { - "type": "logLevelFilter", - "level": "error", - "appender": "errorFile" - }, - "out": - { - "type": "stdout", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - } - } - }, - "categories": - { - "default": - { - "appenders": - [ - "out", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "dht": - { - "appenders": - [ - "dht", - "out", - "errors" - ], - "level": "debug", - "enableCallStack": true - } - } -} diff --git a/dht-node/package.json b/dht-node/package.json index 5a9d6a8e8..8a5cd4462 100644 --- a/dht-node/package.json +++ b/dht-node/package.json @@ -10,11 +10,14 @@ "scripts": { "build": "tsx esbuild.config.ts", "start": "cross-env TZ=UTC NODE_ENV=production node build/index.js", - "dev": "cross-env TZ=UTC tsx watch src/index.ts", + "dev": "cross-env TZ=UTC nodemon -w src --ext ts,json -r tsconfig-paths/register src/index.ts", + "devFast": "cross-env TZ=UTC tsx src/index.ts", "typecheck": "tsc --noEmit", "lint": "biome check --error-on-warnings .", "lint:fix": "biome check --error-on-warnings . --write", - "test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht jest --runInBand --forceExit --detectOpenHandles" + "test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht jest --verbose --runInBand --forceExit --detectOpenHandles", + "test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles", + "test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht jest --coverage --runInBand --forceExit --detectOpenHandles" }, "dependencies": { "cross-env": "^7.0.3", @@ -22,8 +25,12 @@ "sodium-universal": "4.0.1" }, "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", "@hyperswarm/dht": "6.5.1", + "@swc/cli": "^0.7.3", + "@swc/core": "^1.11.24", + "@swc/helpers": "^0.5.17", + "@swc/jest": "^0.2.38", "@types/dotenv": "^8.2.3", "@types/jest": "27.5.1", "@types/joi": "^17.2.3", @@ -36,10 +43,13 @@ "jest": "27.5.1", "joi": "^17.13.3", "log4js": "^6.9.1", + "nodemon": "^2.0.7", "prettier": "^2.8.8", + "source-map-support": "^0.5.21", "ts-jest": "27.1.4", + "tsconfig-paths": "^4.1.1", "tsx": "^4.19.4", - "typeorm": "^0.3.22", + "typeorm": "^0.3.25", "typescript": "^4.9.5", "uuid": "^8.3.2" }, diff --git a/dht-node/src/config/const.ts b/dht-node/src/config/const.ts new file mode 100644 index 000000000..47350c075 --- /dev/null +++ b/dht-node/src/config/const.ts @@ -0,0 +1 @@ +export const LOG4JS_BASE_CATEGORY_NAME = 'dht' diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 018f074aa..7f403ef26 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -1,14 +1,16 @@ import { validate } from 'config-schema' +import type { LogLevel } from 'config-schema' import dotenv from 'dotenv' - import { schema } from './schema' dotenv.config() -const constants = { - LOG4JS_CONFIG: 'log4js-config.json', +const logging = { + LOG4JS_CONFIG: process.env.LOG4JS_CONFIG ?? 'log4js-config.json', // default log level on production should be info - LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', + // log level for default log4js-config.json, don't change existing log4js-config.json + LOG_LEVEL: (process.env.LOG_LEVEL ?? 'info') as LogLevel, + LOG_FILES_BASE_PATH: process.env.LOG_FILES_BASE_PATH ?? '../logs/dht-node', } const server = { @@ -33,7 +35,7 @@ const federation = { } export const CONFIG = { - ...constants, + ...logging, ...server, ...community, ...federation, diff --git a/dht-node/src/config/schema.ts b/dht-node/src/config/schema.ts index b65525ac7..a205a51f3 100644 --- a/dht-node/src/config/schema.ts +++ b/dht-node/src/config/schema.ts @@ -2,6 +2,7 @@ import { COMMUNITY_DESCRIPTION, COMMUNITY_NAME, LOG4JS_CONFIG, + LOG_FILES_BASE_PATH, LOG_LEVEL, NODE_ENV, PRODUCTION, @@ -12,6 +13,7 @@ export const schema = Joi.object({ COMMUNITY_NAME, COMMUNITY_DESCRIPTION, LOG4JS_CONFIG, + LOG_FILES_BASE_PATH, LOG_LEVEL, NODE_ENV, PRODUCTION, diff --git a/dht-node/src/dht_node/index.test.ts b/dht-node/src/dht_node/index.test.ts index 5ffc3cb5f..25ccd4638 100644 --- a/dht-node/src/dht_node/index.test.ts +++ b/dht-node/src/dht_node/index.test.ts @@ -3,8 +3,8 @@ import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } import { validate as validateUUID, version as versionUUID } from 'uuid' import { cleanDB, testEnvironment } from '@test/helpers' -import { logger } from '@test/testSetup' - +import { getLogger } from 'config-schema/test/testSetup' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { CONFIG } from '@/config' import { startDHT } from './index' @@ -21,6 +21,8 @@ const keyPairMock = { secretKey: Buffer.from('secretKey'), } +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.dht_node`) + const serverListenSpy = jest.fn() const serverEventMocks: { [key: string]: any } = {} @@ -112,6 +114,13 @@ describe('federation', () => { jest.useFakeTimers() }) + afterEach(() => { + // print logs which where captured during test + // printLogs() + // clean logs after, else they will be printed in next test again + // cleanLogs() + }) + describe('call startDHT', () => { const hashSpy = jest.spyOn(DHT, 'hash') const keyPairSpy = jest.spyOn(DHT, 'keyPair') diff --git a/dht-node/src/dht_node/index.ts b/dht-node/src/dht_node/index.ts index 69cf86681..cbcf415e7 100644 --- a/dht-node/src/dht_node/index.ts +++ b/dht-node/src/dht_node/index.ts @@ -3,12 +3,14 @@ import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, + getHomeCommunity, } from 'database' import { v4 as uuidv4 } from 'uuid' import { CONFIG } from '@/config' -import { logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' import { ApiVersionType } from './ApiVersionType' const KEY_SECRET_SEEDBYTES = 32 @@ -17,6 +19,7 @@ const POLLTIME = 20000 const SUCCESSTIME = 120000 const ERRORTIME = 240000 const ANNOUNCETIME = 30000 +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.dht_node`) type CommunityApi = { api: string @@ -57,7 +60,6 @@ export const startDHT = async (topic: string): Promise => { server.on('connection', function (socket: any) { logger.info(`server on... with Remote public key: ${socket.remotePublicKey.toString('hex')}`) - socket.on('data', async (data: Buffer) => { try { if (data.length > 1141) { @@ -82,7 +84,6 @@ export const startDHT = async (topic: string): Promise => { ) return } - for (const recApiVersion of recApiVersions) { if ( !recApiVersion.api || @@ -111,7 +112,6 @@ export const startDHT = async (topic: string): Promise => { publicKey: socket.remotePublicKey, lastAnnouncedAt: new Date(), } - logger.debug(`upsert with variables=${JSON.stringify(variables)}`) // this will NOT update the updatedAt column, to distingue between a normal update and the last announcement await DbFederatedCommunity.createQueryBuilder() .insert() @@ -234,7 +234,7 @@ async function writeFederatedHomeCommunityEntries(pubKey: string): Promise { try { // check for existing homeCommunity entry - let homeCom = await DbCommunity.findOne({ where: { foreign: false } }) + let homeCom = await getHomeCommunity() if (homeCom) { // simply update the existing entry, but it MUST keep the ID and UUID because of possible relations homeCom.publicKey = keyPair.publicKey diff --git a/dht-node/src/index.ts b/dht-node/src/index.ts index ec2d30554..36c866eaa 100644 --- a/dht-node/src/index.ts +++ b/dht-node/src/index.ts @@ -1,10 +1,20 @@ +import 'source-map-support/register' import { startDHT } from '@/dht_node/index' import { CONFIG } from '@/config' -import { logger } from '@/server/logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { defaultCategory, initLogger } from 'config-schema' import { AppDatabase } from 'database' +import { getLogger } from 'log4js' async function main() { + // init logger + initLogger( + [defaultCategory(LOG4JS_BASE_CATEGORY_NAME, CONFIG.LOG_LEVEL)], + CONFIG.LOG_FILES_BASE_PATH, + CONFIG.LOG4JS_CONFIG, + ) + const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}`) // open mysql connection await AppDatabase.getInstance().init() logger.debug(`dhtseed set by CONFIG.FEDERATION_DHT_SEED=${CONFIG.FEDERATION_DHT_SEED}`) diff --git a/dht-node/src/server/logger.ts b/dht-node/src/server/logger.ts deleted file mode 100644 index 99ea8518c..000000000 --- a/dht-node/src/server/logger.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { readFileSync } from 'fs' - -import { configure, getLogger } from 'log4js' - -import { CONFIG } from '@/config' - -const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8')) - -options.categories.dht.level = CONFIG.LOG_LEVEL -let filename: string = options.appenders.dht.filename -options.appenders.dht.filename = filename.replace( - 'apiversion-%v', - 'dht-' + CONFIG.FEDERATION_DHT_TOPIC, -) -filename = options.appenders.errorFile.filename - -configure(options) - -const logger = getLogger('dht') - -export { logger } diff --git a/dht-node/test/helpers.test.ts b/dht-node/test/helpers.test.ts index 69d8f3fa4..37810e878 100644 --- a/dht-node/test/helpers.test.ts +++ b/dht-node/test/helpers.test.ts @@ -1,4 +1,4 @@ -import { contributionDateFormatter } from '@test/helpers' +import { contributionDateFormatter } from './helpers' describe('contributionDateFormatter', () => { it('formats the date correctly', () => { diff --git a/dht-node/test/helpers.ts b/dht-node/test/helpers.ts index 9d1829a88..5978f7bb9 100644 --- a/dht-node/test/helpers.ts +++ b/dht-node/test/helpers.ts @@ -1,5 +1,4 @@ import { AppDatabase, entities } from 'database' -import { CONFIG } from '@/config' export const headerPushMock = jest.fn((t) => { context.token = t.value @@ -17,7 +16,9 @@ const context = { export const cleanDB = async () => { // this only works as long we do not have foreign key constraints for (const entity of entities) { - await resetEntity(entity) + if (entity.name !== 'Migration') { + await resetEntity(entity) + } } } diff --git a/dht-node/test/testSetup.ts b/dht-node/test/testSetup.ts deleted file mode 100644 index ff619e95d..000000000 --- a/dht-node/test/testSetup.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { logger } from '@/server/logger' - -jest.setTimeout(1000000) - -jest.mock('@/server/logger', () => { - const originalModule = jest.requireActual('@/server/logger') - return { - __esModule: true, - ...originalModule, - logger: { - addContext: jest.fn(), - trace: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - error: jest.fn(), - fatal: jest.fn(), - }, - } -}) - -export { logger } diff --git a/dht-node/tsconfig.json b/dht-node/tsconfig.json index 757d61e02..9628b7c41 100644 --- a/dht-node/tsconfig.json +++ b/dht-node/tsconfig.json @@ -12,7 +12,7 @@ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./build", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ @@ -74,5 +74,8 @@ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + }, + "ts-node": { + "swc": true } } diff --git a/dht-node/turbo.json b/dht-node/turbo.json index f6211afa1..27d8044a8 100644 --- a/dht-node/turbo.json +++ b/dht-node/turbo.json @@ -2,10 +2,10 @@ "extends": ["//"], "tasks": { "test": { - "dependsOn": ["database#build", "config-schema#build", "database#up:dht_test"] + "dependsOn": ["database#up:dht_test", "^build"] }, "dev": { - "dependsOn": ["database#up"] + "dependsOn": ["database#up", "^build"] }, "start": { "dependsOn": ["database#up", "build"] diff --git a/federation/Dockerfile b/federation/Dockerfile index 070fe3e2a..a8fcfd769 100644 --- a/federation/Dockerfile +++ b/federation/Dockerfile @@ -115,8 +115,5 @@ COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/federation/build/index.js ./ # add node_modules from production_node_modules COPY --chown=app:app --from=production-node-modules ${DOCKER_WORKDIR}/node_modules ./node_modules -# Copy log4js-config.json to provide log configuration -COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/federation/log4js-config.json ./log4js-config.json - # Run command CMD ["node", "index.js"] \ No newline at end of file diff --git a/federation/esbuild.config.ts b/federation/esbuild.config.ts index b4154f008..2ef5f88e5 100644 --- a/federation/esbuild.config.ts +++ b/federation/esbuild.config.ts @@ -12,4 +12,5 @@ build({ external: ['sodium-native'], plugins: [esbuildDecorators()], minify: true, + sourcemap: true, }) \ No newline at end of file diff --git a/federation/jest.config.js b/federation/jest.config.js index 44ddf9bf5..9766c8848 100644 --- a/federation/jest.config.js +++ b/federation/jest.config.js @@ -2,14 +2,14 @@ module.exports = { verbose: true, preset: 'ts-jest', - collectCoverage: true, + collectCoverage: false, collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { lines: 68, }, }, - setupFiles: ['/test/testSetup.ts'], + setupFiles: ['config-schema/test/testSetup.ts'], setupFilesAfterEnv: [], modulePathIgnorePatterns: ['/build/'], moduleNameMapper: { diff --git a/federation/log4js-config.json b/federation/log4js-config.json deleted file mode 100644 index e9336f83c..000000000 --- a/federation/log4js-config.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "appenders": - { - "access": - { - "type": "dateFile", - "filename": "../logs/federation/access-%p.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "apollo": - { - "type": "dateFile", - "filename": "../logs/federation/apollo-%p.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "backend": - { - "type": "dateFile", - "filename": "../logs/federation/backend-%p.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "federation": - { - "type": "dateFile", - "filename": "../logs/federation/apiversion-%v-%p.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "errorFile": - { - "type": "dateFile", - "filename": "../logs/federation/errors-%p.log", - "pattern": "yyyy-MM-dd", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - }, - "compress": true, - "keepFileExt" : true, - "fileNameSep" : "_", - "numBackups" : 30 - }, - "errors": - { - "type": "logLevelFilter", - "level": "error", - "appender": "errorFile" - }, - "out": - { - "type": "stdout", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - } - }, - "apolloOut": - { - "type": "stdout", - "layout": - { - "type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m" - } - } - }, - "categories": - { - "default": - { - "appenders": - [ - "out", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "apollo": - { - "appenders": - [ - "apollo", - "apolloOut", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "backend": - { - "appenders": - [ - "backend", - "out", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "federation": - { - "appenders": - [ - "federation", - "out", - "errors" - ], - "level": "debug", - "enableCallStack": true - }, - "http": - { - "appenders": - [ - "access" - ], - "level": "info" - } - } -} diff --git a/federation/package.json b/federation/package.json index f98b6adf7..f2bae7d73 100644 --- a/federation/package.json +++ b/federation/package.json @@ -15,6 +15,8 @@ "dev:bun": "cross-env TZ=UTC bun --hot src/index.ts", "typecheck": "tsc --noEmit", "test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_federation jest --runInBand --forceExit --detectOpenHandles", + "test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_federation node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles", + "test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_federation jest --coverage --runInBand --forceExit --detectOpenHandles", "lint": "biome check --error-on-warnings .", "lint:fix": "biome check --error-on-warnings . --write" }, @@ -24,7 +26,7 @@ }, "devDependencies": { "@anatine/esbuild-decorators": "^0.2.19", - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.0", "@swc/cli": "^0.7.3", "@swc/core": "^1.11.24", "@swc/helpers": "^0.5.17", @@ -57,15 +59,18 @@ "nodemon": "^2.0.7", "prettier": "^3.5.3", "reflect-metadata": "^0.1.13", + "source-map-support": "^0.5.21", "ts-jest": "27.0.5", "tsconfig-paths": "^4.1.1", "type-graphql": "^1.1.1", - "typeorm": "^0.3.22", + "typeorm": "^0.3.25", "typescript": "^4.9.5", "uuid": "8.3.2" }, "nodemonConfig": { - "ignore": ["**/*.test.ts"] + "ignore": [ + "**/*.test.ts" + ] }, "engines": { "node": ">=18" diff --git a/federation/src/client/1_0/AuthenticationClient.ts b/federation/src/client/1_0/AuthenticationClient.ts index 2aa7b55d3..85daa706f 100644 --- a/federation/src/client/1_0/AuthenticationClient.ts +++ b/federation/src/client/1_0/AuthenticationClient.ts @@ -1,12 +1,15 @@ -import { federationLogger as logger } from '@/server/logger' import { FederatedCommunity as DbFederatedCommunity } from 'database' import { GraphQLClient } from 'graphql-request' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { AuthenticationArgs } from '@/graphql/api/1_0/model/AuthenticationArgs' import { OpenConnectionCallbackArgs } from '@/graphql/api/1_0/model/OpenConnectionCallbackArgs' import { authenticate } from './query/authenticate' import { openConnectionCallback } from './query/openConnectionCallback' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.client.1_0.AuthenticationClient`) + export class AuthenticationClient { dbCom: DbFederatedCommunity endpoint: string @@ -27,41 +30,35 @@ export class AuthenticationClient { } async openConnectionCallback(args: OpenConnectionCallbackArgs): Promise { - logger.debug('Authentication: openConnectionCallback with endpoint', this.endpoint, args) + logger.debug('openConnectionCallback with endpoint', this.endpoint, args) try { const { data } = await this.client.rawRequest(openConnectionCallback, { args }) if (data && data.openConnectionCallback) { - logger.warn( - 'Authentication: openConnectionCallback without response data from endpoint', - this.endpoint, - ) + logger.warn('openConnectionCallback without response data from endpoint', this.endpoint) return false } - logger.debug( - 'Authentication: openConnectionCallback successfully started with endpoint', - this.endpoint, - ) + logger.debug('openConnectionCallback successfully started with endpoint', this.endpoint) return true } catch (err) { - logger.error('Authentication: error on openConnectionCallback', err) + logger.error('error on openConnectionCallback', err) } return false } async authenticate(args: AuthenticationArgs): Promise { - logger.debug('Authentication: authenticate with endpoint=', this.endpoint) + logger.debug('authenticate with endpoint=', this.endpoint) try { const { data } = await this.client.rawRequest(authenticate, { args }) - logger.debug('Authentication: after authenticate: data:', data) + logger.debug('after authenticate: data:', data) const authUuid: string = data?.authenticate if (authUuid) { - logger.debug('Authentication: received authenticated uuid', authUuid) + logger.debug('received authenticated uuid', authUuid) return authUuid } } catch (err) { - logger.error('Authentication: authenticate failed', { + logger.error('authenticate failed', { endpoint: this.endpoint, err, }) diff --git a/federation/src/config/const.ts b/federation/src/config/const.ts new file mode 100644 index 000000000..4d0e8c1db --- /dev/null +++ b/federation/src/config/const.ts @@ -0,0 +1 @@ +export const LOG4JS_BASE_CATEGORY_NAME = 'federation' diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 2d0ab8b87..6e9d5437e 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -3,6 +3,7 @@ import { Decimal } from 'decimal.js-light' import dotenv from 'dotenv' import { validate } from 'config-schema' +import type { LogLevel } from 'config-schema' import { schema } from './schema' @@ -13,11 +14,12 @@ Decimal.set({ rounding: Decimal.ROUND_HALF_UP, }) -const constants = { - DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 - LOG4JS_CONFIG: 'log4js-config.json', +const logging = { + LOG4JS_CONFIG_PLACEHOLDER: process.env.LOG4JS_CONFIG_PLACEHOLDER ?? 'log4js-config-%v.json', // default log level on production should be info - LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', + // log level for default log4js-config.json, don't change existing log4js-config.json + LOG_LEVEL: (process.env.LOG_LEVEL ?? 'info') as LogLevel, + LOG_FILES_BASE_PATH: process.env.LOG_FILES_BASE_PATH ?? '../logs/federation', } const server = { @@ -44,10 +46,8 @@ const federation = { } export const CONFIG = { - ...constants, + ...logging, ...server, - // ...community, - // ...eventProtocol, ...federation, } diff --git a/federation/src/config/schema.ts b/federation/src/config/schema.ts index 14e6168cd..07f5ed48f 100644 --- a/federation/src/config/schema.ts +++ b/federation/src/config/schema.ts @@ -1,7 +1,7 @@ import { - DECAY_START_TIME, GRAPHIQL, - LOG4JS_CONFIG, + LOG4JS_CONFIG_PLACEHOLDER, + LOG_FILES_BASE_PATH, LOG_LEVEL, NODE_ENV, PRODUCTION, @@ -9,9 +9,9 @@ import { import Joi from 'joi' export const schema = Joi.object({ - DECAY_START_TIME, GRAPHIQL, - LOG4JS_CONFIG, + LOG4JS_CONFIG_PLACEHOLDER, + LOG_FILES_BASE_PATH, LOG_LEVEL, NODE_ENV, PRODUCTION, diff --git a/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts b/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts index d89b0b0eb..e59f3fd7d 100644 --- a/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts +++ b/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts @@ -1,11 +1,5 @@ import { registerEnumType } from 'type-graphql' - -export enum PendingTransactionState { - NEW = 1, - PENDING = 2, - SETTLED = 3, - REVERTED = 4, -} +import { PendingTransactionState } from 'shared' registerEnumType(PendingTransactionState, { name: 'PendingTransactionState', // this one is mandatory diff --git a/federation/src/graphql/api/1_0/model/Decay.ts b/federation/src/graphql/api/1_0/model/Decay.ts index a32b96c13..f72fade3a 100644 --- a/federation/src/graphql/api/1_0/model/Decay.ts +++ b/federation/src/graphql/api/1_0/model/Decay.ts @@ -1,14 +1,6 @@ import { Decimal } from 'decimal.js-light' import { Field, Int, ObjectType } from 'type-graphql' - -interface DecayInterface { - balance: Decimal - decay: Decimal - roundedDecay: Decimal - start: Date | null - end: Date | null - duration: number | null -} +import { Decay as DecayInterface} from 'shared' @ObjectType() export class Decay { diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 441d4e7b3..d7b17db91 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -1,13 +1,14 @@ import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' -import { federationLogger as logger } from '@/server/logger' import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, } from 'database' +import { getLogger } from 'log4js' import { Arg, Mutation, Resolver } from 'type-graphql' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { AuthenticationArgs } from '../model/AuthenticationArgs' import { OpenConnectionArgs } from '../model/OpenConnectionArgs' import { OpenConnectionCallbackArgs } from '../model/OpenConnectionCallbackArgs' @@ -16,6 +17,8 @@ import { verifyAndDecrypt } from 'backend/src/auth/jwt/JWT' import { OpenConnectionJwtPayloadType } from 'backend/src/auth/jwt/payloadtypes/OpenConnectionJwtPayloadType' import { JwtPayloadType } from 'backend/src/auth/jwt/payloadtypes/JwtPayloadType' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver`) + @Resolver() export class AuthenticationResolver { @Mutation(() => Boolean) @@ -24,7 +27,7 @@ export class AuthenticationResolver { args: OpenConnectionArgs, ): Promise { const pubKeyBuf = Buffer.from(args.publicKey, 'hex') - logger.debug(`Authentication: openConnection() via apiVersion=1_0:`, args) + logger.debug(`openConnection() via apiVersion=1_0:`, args) // first find with args.publicKey the community 'comA', which starts openConnection request const comA = await DbCommunity.findOneBy({ @@ -36,7 +39,7 @@ export class AuthenticationResolver { if (!comA.publicJwtKey) { throw new LogError(`missing publicJwtKey of community with publicKey`, pubKeyBuf.toString('hex')) } - logger.debug(`Authentication: found requestedCom:`, new CommunityLoggingView(comA)) + logger.debug(`found requestedCom:`, new CommunityLoggingView(comA)) // verify the signing of args.jwt with homeCom.privateJwtKey and decrypt args.jwt with comA.publicJwtKey const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) const openConnectionJwtPayload = await verifyAndDecrypt(args.jwt, homeCom.privateJwtKey!, comA.publicJwtKey) as OpenConnectionJwtPayloadType @@ -73,17 +76,17 @@ export class AuthenticationResolver { @Arg('data') args: OpenConnectionCallbackArgs, ): Promise { - logger.debug(`Authentication: openConnectionCallback() via apiVersion=1_0 ...`, args) + logger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args) // TODO decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey const endPoint = args.url.slice(0, args.url.lastIndexOf('/') + 1) const apiVersion = args.url.slice(args.url.lastIndexOf('/') + 1, args.url.length) - logger.debug(`Authentication: search fedComB per:`, endPoint, apiVersion) + logger.debug(`search fedComB per:`, endPoint, apiVersion) const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion }) if (!fedComB) { throw new LogError(`unknown callback community with url`, args.url) } logger.debug( - `Authentication: found fedComB and start authentication:`, + `found fedComB and start authentication:`, new FederatedCommunityLoggingView(fedComB), ) // biome-ignore lint/complexity/noVoid: no await to respond immediately and invoke authenticate-request asynchronously @@ -96,18 +99,15 @@ export class AuthenticationResolver { @Arg('data') args: AuthenticationArgs, ): Promise { - logger.debug(`Authentication: authenticate() via apiVersion=1_0 ...`, args) + logger.debug(`authenticate() via apiVersion=1_0 ...`, args) const authCom = await DbCommunity.findOneByOrFail({ communityUuid: args.oneTimeCode }) - logger.debug('Authentication: found authCom:', new CommunityLoggingView(authCom)) + logger.debug('found authCom:', new CommunityLoggingView(authCom)) if (authCom) { // TODO decrypt args.uuid with authCom.publicKey authCom.communityUuid = args.uuid authCom.authenticatedAt = new Date() await DbCommunity.save(authCom) - logger.debug( - 'Authentication: store authCom.uuid successfully:', - new CommunityLoggingView(authCom), - ) + logger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) // TODO encrypt homeCom.uuid with homeCom.privateKey if (homeCom.communityUuid) { diff --git a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts index b3a094b4f..174c3735f 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts @@ -2,6 +2,7 @@ import { CONFIG } from '@/config' import { createServer } from '@/server/createServer' import { createTestClient } from 'apollo-server-testing' import { Community as DbCommunity } from 'database' +import { getLogger } from 'log4js' import { DataSource } from 'typeorm' let query: any @@ -12,7 +13,7 @@ let con: DataSource CONFIG.FEDERATION_API = '1_0' beforeAll(async () => { - const server = await createServer() + const server = await createServer(getLogger('apollo')) con = server.con query = createTestClient(server.apollo).query DbCommunity.clear() diff --git a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.ts b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.ts index e019f27f6..1cdb43a31 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.ts @@ -1,9 +1,12 @@ -import { federationLogger as logger } from '@/server/logger' import { Community as DbCommunity } from 'database' +import { getLogger } from 'log4js' import { Query, Resolver } from 'type-graphql' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { GetPublicCommunityInfoResultLoggingView } from '../logger/GetPublicCommunityInfoResultLogging.view' import { GetPublicCommunityInfoResult } from '../model/GetPublicCommunityInfoResult' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.PublicCommunityInfoResolver`) + @Resolver() export class PublicCommunityInfoResolver { @Query(() => GetPublicCommunityInfoResult) diff --git a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts index 894570972..023a11a19 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts @@ -1,7 +1,9 @@ import { CONFIG } from '@/config' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { createServer } from '@/server/createServer' import { createTestClient } from 'apollo-server-testing' import { FederatedCommunity as DbFederatedCommunity } from 'database' +import { getLogger } from 'log4js' let query: any @@ -11,7 +13,7 @@ let con: any CONFIG.FEDERATION_API = '1_0' beforeAll(async () => { - const server = await createServer() + const server = await createServer(getLogger('apollo')) con = server.con query = createTestClient(server.apollo).query DbFederatedCommunity.clear() diff --git a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts index 7c4d734c1..4df03096d 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts @@ -1,8 +1,11 @@ -import { federationLogger as logger } from '@/server/logger' import { FederatedCommunity as DbFederatedCommunity } from 'database' +import { getLogger } from 'log4js' import { Query, Resolver } from 'type-graphql' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { GetPublicKeyResult } from '../model/GetPublicKeyResult' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.PublicKeyResolver`) + @Resolver() export class PublicKeyResolver { @Query(() => GetPublicKeyResult) diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts index 44273b4c5..5ba55f88a 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts @@ -1,11 +1,12 @@ import { CONFIG } from '@/config' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { fullName } from '@/graphql/util/fullName' import { cleanDB, testEnvironment } from '@test/helpers' -import { logger } from '@test/testSetup' import { ApolloServerTestClient } from 'apollo-server-testing' import { Community as DbCommunity, User as DbUser, UserContact as DbUserContact } from 'database' import Decimal from 'decimal.js-light' import { GraphQLError } from 'graphql' +import { getLogger } from 'log4js' import { DataSource } from 'typeorm' import { SendCoinsArgs } from '../model/SendCoinsArgs' @@ -28,7 +29,7 @@ let recipUser: DbUser let recipContact: DbUserContact beforeAll(async () => { - testEnv = await testEnvironment(logger) + testEnv = await testEnvironment(getLogger('apollo')) mutate = testEnv.mutate // query = testEnv.query // con = testEnv.con diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index 7f9d1a684..35850fc3e 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -1,15 +1,16 @@ -import { findUserByIdentifier } from '@/graphql/util/findUserByIdentifier' import { fullName } from '@/graphql/util/fullName' import { LogError } from '@/server/LogError' -import { federationLogger as logger } from '@/server/logger' import { Community as DbCommunity, PendingTransaction as DbPendingTransaction, PendingTransactionLoggingView, + findUserByIdentifier } from 'database' import Decimal from 'decimal.js-light' +import { getLogger } from 'log4js' import { Arg, Mutation, Resolver } from 'type-graphql' -import { PendingTransactionState } from '../enum/PendingTransactionState' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { PendingTransactionState } from 'shared' import { TransactionTypeId } from '../enum/TransactionTypeId' import { SendCoinsArgsLoggingView } from '../logger/SendCoinsArgsLogging.view' import { SendCoinsArgs } from '../model/SendCoinsArgs' @@ -19,6 +20,8 @@ import { calculateRecipientBalance } from '../util/calculateRecipientBalance' import { revertSettledReceiveTransaction } from '../util/revertSettledReceiveTransaction' import { settlePendingReceiveTransaction } from '../util/settlePendingReceiveTransaction' import { storeForeignUser } from '../util/storeForeignUser' +import { countOpenPendingTransactions } from 'database' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.SendCoinsResolver`) @Resolver() export class SendCoinsResolver { @@ -40,32 +43,21 @@ export class SendCoinsResolver { ) } let receiverUser - try { - // second check if receiver user exists in this community - receiverUser = await findUserByIdentifier( - args.recipientUserIdentifier, - args.recipientCommunityUuid, - ) - } catch (err) { - logger.error('Error in findUserByIdentifier:', err) + + // second check if receiver user exists in this community + receiverUser = await findUserByIdentifier( + args.recipientUserIdentifier, + args.recipientCommunityUuid, + ) + if (!receiverUser) { + logger.error('Error in findUserByIdentifier:') throw new LogError( `voteForSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, ) } - const openSenderPendingTx = await DbPendingTransaction.count({ - where: [ - { userGradidoID: args.senderUserUuid, state: PendingTransactionState.NEW }, - { linkedUserGradidoID: args.senderUserUuid, state: PendingTransactionState.NEW }, - ], - }) - const openReceiverPendingTx = await DbPendingTransaction.count({ - where: [ - { userGradidoID: receiverUser.gradidoID, state: PendingTransactionState.NEW }, - { linkedUserGradidoID: receiverUser.gradidoID, state: PendingTransactionState.NEW }, - ], - }) - if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) { + + if (await countOpenPendingTransactions([args.senderUserUuid, receiverUser.gradidoID]) > 0) { throw new LogError( `There exist still ongoing 'Pending-Transactions' for the involved users on receiver-side!`, ) @@ -123,11 +115,11 @@ export class SendCoinsResolver { ) } let receiverUser - try { - // second check if receiver user exists in this community - receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) - } catch (err) { - logger.error('Error in findUserByIdentifier:', err) + + // second check if receiver user exists in this community + receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + if (!receiverUser) { + logger.error('Error in findUserByIdentifier') throw new LogError( `revertSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, @@ -190,12 +182,11 @@ export class SendCoinsResolver { args.recipientCommunityUuid, ) } - let receiverUser - try { - // second check if receiver user exists in this community - receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) - } catch (err) { - logger.error('Error in findUserByIdentifier:', err) + + // second check if receiver user exists in this community + const receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + if (!receiverUser) { + logger.error('Error in findUserByIdentifier') throw new LogError( `settleSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, @@ -262,13 +253,12 @@ export class SendCoinsResolver { `revertSettledSendCoins with wrong recipientCommunityUuid`, args.recipientCommunityUuid, ) - } - let receiverUser - try { - // second check if receiver user exists in this community - receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) - } catch (err) { - logger.error('Error in findUserByIdentifier:', err) + } + + // second check if receiver user exists in this community + const receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + if (!receiverUser) { + logger.error('Error in findUserByIdentifier') throw new LogError( `revertSettledSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 2c6ea14a8..3083293d8 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -1,10 +1,10 @@ -import { federationLogger as logger } from '@/server/logger' import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, } from 'database' +import { getLogger } from 'log4js' import { OpenConnectionArgs } from '../model/OpenConnectionArgs' import { OpenConnectionCallbackArgs } from '../model/OpenConnectionCallbackArgs' @@ -12,10 +12,13 @@ import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactor import { randombytes_random } from 'sodium-native' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { AuthenticationArgs } from '../model/AuthenticationArgs' import { encryptAndSign } from 'backend/src/auth/jwt/JWT' import { OpenConnectionCallbackJwtPayloadType } from 'backend/src/auth/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`) + export async function startOpenConnectionCallback( comA: DbCommunity, api: string, @@ -55,13 +58,13 @@ export async function startOpenConnectionCallback( // encrypt callbackArgs with requestedCom.publicKey and sign it with homeCom.privateKey const encryptedCallbackArgs = await encryptAndSign(callbackArgs, homeCom.privateJwtKey!, comA.publicJwtKey!) if (await client.openConnectionCallback(encryptedCallbackArgs)) { - logger.debug('Authentication: startOpenConnectionCallback() successful:', callbackArgs) + logger.debug('startOpenConnectionCallback() successful:', encryptedCallbackArgs) } else { - logger.error('Authentication: startOpenConnectionCallback() failed:', callbackArgs) + logger.error('startOpenConnectionCallback() failed:', encryptedCallbackArgs) } } } catch (err) { - logger.error('Authentication: error in startOpenConnectionCallback:', err) + logger.error('error in startOpenConnectionCallback:', err) } } @@ -69,7 +72,7 @@ export async function startAuthentication( oneTimeCode: string, fedComB: DbFedCommunity, ): Promise { - logger.debug(`Authentication: startAuthentication()...`, { + logger.debug(`startAuthentication()...`, { oneTimeCode, fedComB: new FederatedCommunityLoggingView(fedComB), }) @@ -86,12 +89,12 @@ export async function startAuthentication( if (homeCom.communityUuid) { authenticationArgs.uuid = homeCom.communityUuid } - logger.debug(`Authentication: invoke authenticate() with:`, authenticationArgs) + logger.debug(`invoke authenticate() with:`, authenticationArgs) const fedComUuid = await client.authenticate(authenticationArgs) - logger.debug(`Authentication: response of authenticate():`, fedComUuid) + logger.debug(`response of authenticate():`, fedComUuid) if (fedComUuid !== null) { logger.debug( - `Authentication: received communityUUid for callbackFedCom:`, + `received communityUUid for callbackFedCom:`, fedComUuid, new FederatedCommunityLoggingView(fedComB), ) @@ -103,15 +106,12 @@ export async function startAuthentication( callbackCom.communityUuid = fedComUuid callbackCom.authenticatedAt = new Date() await DbCommunity.save(callbackCom) - logger.debug( - 'Authentication: Community Authentication successful:', - new CommunityLoggingView(callbackCom), - ) + logger.debug('Community Authentication successful:', new CommunityLoggingView(callbackCom)) } else { - logger.error('Authentication: Community Authentication failed:', authenticationArgs) + logger.error('Community Authentication failed:', authenticationArgs) } } } catch (err) { - logger.error('Authentication: error in startOpenConnectionCallback:', err) + logger.error('error in startAuthentication:', err) } } diff --git a/federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts b/federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts index 198c89289..ead2cc641 100644 --- a/federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts +++ b/federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts @@ -1,4 +1,4 @@ -import { calculateDecay } from '@/graphql/util/decay' +import { calculateDecay } from 'shared' import { getLastTransaction } from '@/graphql/util/getLastTransaction' import { Decimal } from 'decimal.js-light' import { Decay } from '../model/Decay' @@ -13,7 +13,7 @@ export async function calculateRecipientBalance( return null } - const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) + const decay = new Decay(calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)) const balance = decay.balance.add(amount.toString()) diff --git a/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts b/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts index 8a2bd7663..9b311be30 100644 --- a/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts +++ b/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts @@ -10,15 +10,17 @@ import { Transaction as dbTransaction, } from 'database' -import { PendingTransactionState } from '../enum/PendingTransactionState' +import { PendingTransactionState } from 'shared' import { LogError } from '@/server/LogError' -import { federationLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { TRANSACTIONS_LOCK } from '@/graphql/util/TRANSACTIONS_LOCK' import { getLastTransaction } from '@/graphql/util/getLastTransaction' const db = AppDatabase.getInstance() +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.revertSettledReceiveTransaction`) export async function revertSettledReceiveTransaction( homeCom: DbCommunity, diff --git a/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts index 306313f82..6396174bc 100644 --- a/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts +++ b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts @@ -9,17 +9,19 @@ import { UserLoggingView, Transaction as dbTransaction, } from 'database' -import { PendingTransactionState } from '../enum/PendingTransactionState' +import { PendingTransactionState } from 'shared' import { LogError } from '@/server/LogError' -import { federationLogger as logger } from '@/server/logger' import { TRANSACTIONS_LOCK } from '@/graphql/util/TRANSACTIONS_LOCK' import { getLastTransaction } from '@/graphql/util/getLastTransaction' import Decimal from 'decimal.js-light' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { calculateRecipientBalance } from './calculateRecipientBalance' const db = AppDatabase.getInstance() +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.settlePendingReceiveTransaction`) export async function settlePendingReceiveTransaction( homeCom: DbCommunity, diff --git a/federation/src/graphql/api/1_0/util/storeForeignUser.ts b/federation/src/graphql/api/1_0/util/storeForeignUser.ts index d0f05ae1e..c45f94eb7 100644 --- a/federation/src/graphql/api/1_0/util/storeForeignUser.ts +++ b/federation/src/graphql/api/1_0/util/storeForeignUser.ts @@ -1,9 +1,12 @@ import { User as DbUser, UserLoggingView } from 'database' -import { federationLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { SendCoinsArgsLoggingView } from '../logger/SendCoinsArgsLogging.view' import { SendCoinsArgs } from '../model/SendCoinsArgs' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.storeForeignUser`) + export async function storeForeignUser(args: SendCoinsArgs): Promise { if (args.senderCommunityUuid !== null && args.senderUserUuid !== null) { try { diff --git a/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.test.ts b/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.test.ts index 2c2b3bb48..40db6db76 100644 --- a/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.test.ts +++ b/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.test.ts @@ -1,7 +1,9 @@ import { CONFIG } from '@/config' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { createServer } from '@/server/createServer' import { createTestClient } from 'apollo-server-testing' import { FederatedCommunity as DbFederatedCommunity } from 'database' +import { getLogger } from 'log4js' let query: any @@ -11,7 +13,7 @@ let con: any CONFIG.FEDERATION_API = '1_1' beforeAll(async () => { - const server = await createServer() + const server = await createServer(getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apollo`)) con = server.con query = createTestClient(server.apollo).query DbFederatedCommunity.clear() diff --git a/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.ts b/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.ts index 6e621eb59..01ac673d5 100644 --- a/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.ts +++ b/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.ts @@ -1,13 +1,16 @@ -import { federationLogger as logger } from '@/server/logger' import { FederatedCommunity as DbFederatedCommunity } from 'database' +import { getLogger } from 'log4js' import { Query, Resolver } from 'type-graphql' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { GetPublicKeyResult } from '../../1_0/model/GetPublicKeyResult' +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_1.resolver.PublicKeyResolver`) + @Resolver() export class PublicKeyResolver { @Query(() => GetPublicKeyResult) async getPublicKey(): Promise { - logger.debug(`getPublicKey() via apiVersion=1_0 ...`) + logger.debug(`getPublicKey()...`) const homeCom = await DbFederatedCommunity.findOneOrFail({ where: { foreign: false, @@ -15,7 +18,7 @@ export class PublicKeyResolver { }, }) const publicKeyHex = homeCom.publicKey.toString('hex') - logger.debug(`getPublicKey()-1_1... return publicKey=${publicKeyHex}`) + logger.debug(`getPublicKey()... return publicKey=${publicKeyHex}`) return new GetPublicKeyResult(publicKeyHex) } } diff --git a/federation/src/graphql/api/schema.ts b/federation/src/graphql/api/schema.ts index 661e203d2..1802d046e 100644 --- a/federation/src/graphql/api/schema.ts +++ b/federation/src/graphql/api/schema.ts @@ -1,12 +1,13 @@ -import { federationLogger as logger } from '@/server/logger' +import { getLogger } from 'log4js' import { NonEmptyArray } from 'type-graphql' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' // config import { CONFIG } from '../../config' import { getApiResolvers as getApiResolvers_1_0 } from './1_0/schema' import { getApiResolvers as getApiResolvers_1_1 } from './1_1/schema' export const getApiResolvers = (): NonEmptyArray => { - logger.info(`getApiResolvers...${CONFIG.FEDERATION_API}`) + getLogger(LOG4JS_BASE_CATEGORY_NAME).info(`getApiResolvers...${CONFIG.FEDERATION_API}`) if (CONFIG.FEDERATION_API === '1_0') { return getApiResolvers_1_0() diff --git a/federation/src/graphql/util/checkTradingLevel.ts b/federation/src/graphql/util/checkTradingLevel.ts index 773e76c03..2a96f34f9 100644 --- a/federation/src/graphql/util/checkTradingLevel.ts +++ b/federation/src/graphql/util/checkTradingLevel.ts @@ -1,9 +1,12 @@ import { CONFIG } from '@/config' -import { federationLogger as logger } from '@/server/logger' import { Community as DbCommunity } from 'database' import { Decimal } from 'decimal.js-light' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' export async function checkTradingLevel(homeCom: DbCommunity, amount: Decimal): Promise { + const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.util.checkTradingLevel`) + const tradingLevel = CONFIG.FEDERATION_TRADING_LEVEL if (homeCom.url !== tradingLevel.RECEIVER_COMMUNITY_URL) { logger.warn( diff --git a/federation/src/graphql/util/decay.test.ts b/federation/src/graphql/util/decay.test.ts deleted file mode 100644 index f419982ac..000000000 --- a/federation/src/graphql/util/decay.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Decimal } from 'decimal.js-light' - -import { calculateDecay, decayFormula } from './decay' - -describe('utils/decay', () => { - describe('decayFormula', () => { - it('has base 0.99999997802044727', () => { - const amount = new Decimal(1.0) - const seconds = 1 - // TODO: toString() was required, we could not compare two decimals - expect(decayFormula(amount, seconds).toString()).toBe('0.999999978035040489732012') - }) - it('has correct backward calculation', () => { - const amount = new Decimal(1.0) - const seconds = -1 - expect(decayFormula(amount, seconds).toString()).toBe('1.000000021964959992727444') - }) - // we get pretty close, but not exact here, skipping - - it.skip('has correct forward calculation', () => { - const amount = new Decimal(1.0).div( - new Decimal('0.99999997803504048973201202316767079413460520837376'), - ) - const seconds = 1 - expect(decayFormula(amount, seconds).toString()).toBe('1.0') - }) - }) - it('has base 0.99999997802044727', () => { - const now = new Date() - now.setSeconds(1) - const oneSecondAgo = new Date(now.getTime()) - oneSecondAgo.setSeconds(0) - expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now).balance.toString()).toBe( - '0.999999978035040489732012', - ) - }) - - it('returns input amount when from and to is the same', () => { - const now = new Date() - expect(calculateDecay(new Decimal(100.0), now, now).balance.toString()).toBe('100') - }) -}) diff --git a/federation/src/graphql/util/decay.ts b/federation/src/graphql/util/decay.ts deleted file mode 100644 index 331d3b5b4..000000000 --- a/federation/src/graphql/util/decay.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Decimal } from 'decimal.js-light' - -import { CONFIG } from '@/config' -import { LogError } from '@/server/LogError' -import { Decay } from '../api/1_0/model/Decay' - -// TODO: externalize all those definitions and functions into an external decay library - -function decayFormula(value: Decimal, seconds: number): Decimal { - // TODO why do we need to convert this here to a stting to work properly? - return value.mul( - new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds).toString(), - ) -} - -function calculateDecay( - amount: Decimal, - from: Date, - to: Date, - startBlock: Date = CONFIG.DECAY_START_TIME, -): Decay { - const fromMs = from.getTime() - const toMs = to.getTime() - const startBlockMs = startBlock.getTime() - - if (toMs < fromMs) { - throw new LogError('calculateDecay: to < from, reverse decay calculation is invalid') - } - - // Initialize with no decay - const decay: Decay = { - balance: amount, - decay: new Decimal(0), - roundedDecay: new Decimal(0), - start: null, - end: null, - duration: null, - } - - // decay started after end date; no decay - if (startBlockMs > toMs) { - return decay - } - // decay started before start date; decay for full duration - if (startBlockMs < fromMs) { - decay.start = from - decay.duration = (toMs - fromMs) / 1000 - } - // decay started between start and end date; decay from decay start till end date - else { - decay.start = startBlock - decay.duration = (toMs - startBlockMs) / 1000 - } - - decay.end = to - decay.balance = decayFormula(amount, decay.duration) - decay.decay = decay.balance.minus(amount) - decay.roundedDecay = amount - .toDecimalPlaces(2, Decimal.ROUND_DOWN) - .minus(decay.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN).toString()) - .mul(-1) - return decay -} - -export { decayFormula, calculateDecay } diff --git a/federation/src/graphql/util/findUserByIdentifier.ts b/federation/src/graphql/util/findUserByIdentifier.ts deleted file mode 100644 index 7f5e8e329..000000000 --- a/federation/src/graphql/util/findUserByIdentifier.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { User as DbUser, UserContact as DbUserContact } from 'database' -import { validate, version } from 'uuid' - -import { LogError } from '@/server/LogError' - -import { VALID_ALIAS_REGEX } from './validateAlias' - -export const findUserByIdentifier = async ( - identifier: string, - communityIdentifier?: string, -): Promise => { - let user: DbUser | null - if (validate(identifier) && version(identifier) === 4) { - user = await DbUser.findOne({ - where: { gradidoID: identifier, communityUuid: communityIdentifier }, - relations: ['emailContact'], - }) - if (!user) { - throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) - } - } else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) { - const userContact = await DbUserContact.findOne({ - where: { - email: identifier, - emailChecked: true, - }, - relations: ['user'], - }) - if (!userContact) { - throw new LogError('No user with this credentials', identifier) - } - if (!userContact.user) { - throw new LogError('No user to given contact', identifier) - } - if (userContact.user.communityUuid !== communityIdentifier) { - throw new LogError( - 'Found user to given contact, but belongs to other community', - identifier, - communityIdentifier, - ) - } - user = userContact.user - user.emailContact = userContact - } else if (VALID_ALIAS_REGEX.exec(identifier)) { - user = await DbUser.findOne({ - where: { alias: identifier, communityUuid: communityIdentifier }, - relations: ['emailContact'], - }) - if (!user) { - throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) - } - } else { - throw new LogError('Unknown identifier type', identifier) - } - - return user -} diff --git a/federation/src/graphql/util/validateAlias.ts b/federation/src/graphql/util/validateAlias.ts deleted file mode 100644 index 4eccad63f..000000000 --- a/federation/src/graphql/util/validateAlias.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { User as DbUser } from 'database' -import { Raw } from 'typeorm' - -import { LogError } from '@/server/LogError' - -export const VALID_ALIAS_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ - -const RESERVED_ALIAS = [ - 'admin', - 'email', - 'gast', - 'gdd', - 'gradido', - 'guest', - 'home', - 'root', - 'support', - 'temp', - 'tmp', - 'tmp', - 'user', - 'usr', - 'var', -] - -export const validateAlias = async (alias: string): Promise => { - if (alias.length < 3) { - throw new LogError('Given alias is too short', alias) - } - if (alias.length > 20) { - throw new LogError('Given alias is too long', alias) - } - if (!alias.match(VALID_ALIAS_REGEX)) { - throw new LogError('Invalid characters in alias', alias) - } - if (RESERVED_ALIAS.includes(alias.toLowerCase())) { - throw new LogError('Alias is not allowed', alias) - } - const aliasInUse = await DbUser.find({ - where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) }, - }) - if (aliasInUse.length !== 0) { - throw new LogError('Alias already in use', alias) - } - return true -} diff --git a/federation/src/index.ts b/federation/src/index.ts index 5c3cc6f89..4492f24fb 100644 --- a/federation/src/index.ts +++ b/federation/src/index.ts @@ -1,21 +1,29 @@ +import 'source-map-support/register' import { createServer } from './server/createServer' +import { defaultCategory, initLogger } from 'config-schema' +import { getLogger } from 'log4js' // config import { CONFIG } from './config' +import { LOG4JS_BASE_CATEGORY_NAME } from './config/const' async function main() { - // biome-ignore lint/suspicious/noConsole: no logger needed fot startup infos - console.log(`FEDERATION_PORT=${CONFIG.FEDERATION_PORT}`) - // biome-ignore lint/suspicious/noConsole: no logger needed fot startup infos - console.log(`FEDERATION_API=${CONFIG.FEDERATION_API}`) - const { app } = await createServer() + // init logger + const log4jsConfigFileName = CONFIG.LOG4JS_CONFIG_PLACEHOLDER.replace('%v', CONFIG.FEDERATION_API) + initLogger( + [defaultCategory('federation', CONFIG.LOG_LEVEL), defaultCategory('apollo', CONFIG.LOG_LEVEL)], + `${CONFIG.LOG_FILES_BASE_PATH}_${CONFIG.FEDERATION_API}`, + log4jsConfigFileName, + ) + + // init server + const { app } = await createServer(getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apollo`)) app.listen(CONFIG.FEDERATION_PORT, () => { - // biome-ignore lint/suspicious/noConsole: no logger needed fot startup infos - console.log(`Server is running at http://localhost:${CONFIG.FEDERATION_PORT}`) + const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}`) + logger.info(`Server is running at http://localhost:${CONFIG.FEDERATION_PORT}`) if (CONFIG.GRAPHIQL) { - // biome-ignore lint/suspicious/noConsole: no logger needed fot startup infos - console.log( + logger.info( `GraphIQL available at ${CONFIG.FEDERATION_COMMUNITY_URL}/api/${CONFIG.FEDERATION_API}`, ) } diff --git a/federation/src/server/LogError.ts b/federation/src/server/LogError.ts index fbd6e6d18..0a91b17c6 100644 --- a/federation/src/server/LogError.ts +++ b/federation/src/server/LogError.ts @@ -1,8 +1,23 @@ -import { federationLogger as logger } from './logger' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLogger } from 'log4js' +/** + * A custom Error that logs itself immediately upon instantiation. + * + * TODO: Anti-pattern warning: + * Logging inside the constructor introduces side effects during object creation, + * which breaks separation of concerns and can lead to duplicate or unwanted logs. + * It is generally better to log errors where they are caught, not where they are thrown. + * + * @class LogError + * @extends {Error} + * @param {string} msg - The error message. + * @param {...any} details - Additional details passed to the logger. + */ export class LogError extends Error { constructor(msg: string, ...details: any[]) { super(msg) + const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.logError`) logger.error(msg, ...details) } } diff --git a/federation/src/server/createServer.ts b/federation/src/server/createServer.ts index 0575e72fc..015fbba48 100644 --- a/federation/src/server/createServer.ts +++ b/federation/src/server/createServer.ts @@ -11,15 +11,12 @@ import { plugins } from './plugins' // graphql import { schema } from '@/graphql/schema' -// webhooks -// import { elopageWebhook } from '@/webhook/elopage' - import { AppDatabase } from 'database' import { slowDown } from 'express-slow-down' import helmet from 'helmet' import { Logger } from 'log4js' import { DataSource } from 'typeorm' -import { apolloLogger } from './logger' + // i18n // import { i18n } from './localization' @@ -30,12 +27,9 @@ type ServerDef = { apollo: ApolloServer; app: Express; con: DataSource } export const createServer = async ( // context: any = serverContext, - logger: Logger = apolloLogger, + apolloLogger: Logger, // localization: i18n.I18n = i18n, ): Promise => { - logger.addContext('user', 'unknown') - logger.debug('createServer...') - // open mysql connection const db = AppDatabase.getInstance() await db.init() @@ -77,9 +71,6 @@ export const createServer = async ( // i18n // app.use(localization.init) - // Elopage Webhook - // app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook) - // Apollo Server const apollo = new ApolloServer({ schema: await schema(), @@ -87,10 +78,8 @@ export const createServer = async ( // introspection: CONFIG.GRAPHIQL, // context, plugins, - logger, + logger: apolloLogger, }) apollo.applyMiddleware({ app, path: '/' }) - logger.debug('createServer...successful') - return { apollo, app, con: db.getDataSource() } } diff --git a/federation/src/server/logger.ts b/federation/src/server/logger.ts deleted file mode 100644 index 505c7e4d8..000000000 --- a/federation/src/server/logger.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { CONFIG } from '@/config' -import log4js from 'log4js' - -import { readFileSync } from 'fs' - -const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8')) - -options.categories.backend.level = CONFIG.LOG_LEVEL -options.categories.apollo.level = CONFIG.LOG_LEVEL -let filename: string = options.appenders.federation.filename -options.appenders.federation.filename = filename - .replace('%v', CONFIG.FEDERATION_API) - .replace('%p', CONFIG.FEDERATION_PORT.toString()) -filename = options.appenders.access.filename -options.appenders.access.filename = filename.replace('%p', CONFIG.FEDERATION_PORT.toString()) -filename = options.appenders.apollo.filename -options.appenders.apollo.filename = filename.replace('%p', CONFIG.FEDERATION_PORT.toString()) -filename = options.appenders.backend.filename -options.appenders.backend.filename = filename.replace('%p', CONFIG.FEDERATION_PORT.toString()) -filename = options.appenders.errorFile.filename -options.appenders.errorFile.filename = filename.replace('%p', CONFIG.FEDERATION_PORT.toString()) - -log4js.configure(options) - -const apolloLogger = log4js.getLogger('apollo') -// const backendLogger = log4js.getLogger('backend') -const federationLogger = log4js.getLogger('federation') - -// backendLogger.addContext('user', 'unknown') - -export { apolloLogger, federationLogger } diff --git a/federation/test/helpers.ts b/federation/test/helpers.ts index 9a56caedd..7c6280a44 100644 --- a/federation/test/helpers.ts +++ b/federation/test/helpers.ts @@ -3,7 +3,7 @@ import { createTestClient } from 'apollo-server-testing' import { createServer } from '@/server/createServer' -import { logger } from './testSetup' +import { getLogger } from 'config-schema/test/testSetup' export const headerPushMock = jest.fn((t) => { context.token = t.value @@ -21,11 +21,13 @@ const context = { export const cleanDB = async () => { // this only works as long we do not have foreign key constraints for (const entity of entities) { - await resetEntity(entity) + if (entity.name !== 'Migration') { + await resetEntity(entity) + } } } -export const testEnvironment = async (testLogger = logger /*, testI18n = i18n */) => { +export const testEnvironment = async (testLogger = getLogger('apollo') /*, testI18n = i18n */) => { const server = await createServer(/* context, */ testLogger /* , testI18n */) const con = server.con const testClient = createTestClient(server.apollo) diff --git a/federation/test/testSetup.ts b/federation/test/testSetup.ts deleted file mode 100644 index 85008799f..000000000 --- a/federation/test/testSetup.ts +++ /dev/null @@ -1,43 +0,0 @@ -// import { CONFIG } from '@/config' -// import { i18n } from '@/server/localization' -import { federationLogger as logger } from '@/server/logger' - -// CONFIG.EMAIL = true -// CONFIG.EMAIL_TEST_MODUS = false - -jest.setTimeout(1000000) - -jest.mock('@/server/logger', () => { - const originalModule = jest.requireActual('@/server/logger') - return { - __esModule: true, - ...originalModule, - backendLogger: { - addContext: jest.fn(), - trace: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - error: jest.fn(), - fatal: jest.fn(), - }, - } -}) - -/* -jest.mock('@/server/localization', () => { - const originalModule = jest.requireActual('@/server/localization') - return { - __esModule: true, - ...originalModule, - i18n: { - init: jest.fn(), - // configure: jest.fn(), - // __: jest.fn(), - // setLocale: jest.fn(), - }, - } -}) -*/ - -export { logger } diff --git a/federation/tsconfig.json b/federation/tsconfig.json index 2d0e18ed8..86b102fa6 100644 --- a/federation/tsconfig.json +++ b/federation/tsconfig.json @@ -51,7 +51,7 @@ // "@arg/*": ["src/graphql/arg/*"], // "@enum/*": ["src/graphql/enum/*"], // "@model/*": ["src/graphql/model/*"], - "@repository/*": ["src/typeorm/repository/*"], + // "@repository/*": ["src/typeorm/repository/*"], "@test/*": ["test/*"], /* common */ // "@common/*": ["../common/src/*"], diff --git a/federation/turbo.json b/federation/turbo.json index 2533a8de2..77a13a055 100644 --- a/federation/turbo.json +++ b/federation/turbo.json @@ -2,10 +2,10 @@ "extends": ["//"], "tasks": { "test": { - "dependsOn": ["database#up:federation_test", "config-schema#build", "database#build"] + "dependsOn": ["database#up:federation_test", "^build"] }, "dev": { - "dependsOn": ["database#up"] + "dependsOn": ["database#up", "^build"] }, "start": { "dependsOn": ["database#up", "build"] diff --git a/frontend/package.json b/frontend/package.json index 7e122a237..4dbace5c1 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -81,7 +81,7 @@ "@vue/test-utils": "^2.4.6", "chokidar-cli": "^3.0.0", "concurrently": "^9.1.2", - "config-schema": "*", + "config-schema": "2.6.0", "cross-env": "^7.0.3", "dotenv-webpack": "^7.0.3", "eslint": "8.57.1", diff --git a/package.json b/package.json index a9f470430..f2c19d169 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "admin", "backend", "config-schema", + "core", "database", "dht-node", "federation", - "frontend" + "frontend", + "shared" ], "scripts": { "release": "scripts/release.sh", @@ -33,7 +35,7 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@biomejs/biome": "1.9.4" + "@biomejs/biome": "2.0.0" }, "engines": { "node": ">=18" diff --git a/scripts/release.sh b/scripts/release.sh index caa8d517c..45221398d 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -6,6 +6,9 @@ PROJECT_DIR="${SCRIPT_DIR}/../" FRONTEND_DIR="${PROJECT_DIR}/frontend/" BACKEND_DIR="${PROJECT_DIR}/backend/" DATABASE_DIR="${PROJECT_DIR}/database/" +SHARED_DIR="${PROJECT_DIR}/shared/" +CONFIG_SCHEMA_DIR="${PROJECT_DIR}/config-schema/" +CORE_DIR="${PROJECT_DIR}/core/" ADMIN_DIR="${PROJECT_DIR}/admin/" DHTNODE_DIR="${PROJECT_DIR}/dht-node/" FEDERATION_DIR="${PROJECT_DIR}/federation/" @@ -27,6 +30,12 @@ cd ${BACKEND_DIR} yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION} cd ${DATABASE_DIR} yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION} +cd ${SHARED_DIR} +yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION} +cd ${CONFIG_SCHEMA_DIR} +yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION} +cd ${CORE_DIR} +yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION} cd ${ADMIN_DIR} yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION} cd ${DHTNODE_DIR} diff --git a/shared/README.md b/shared/README.md new file mode 100644 index 000000000..43517fe98 --- /dev/null +++ b/shared/README.md @@ -0,0 +1,13 @@ +# shared +Gradido Shared Code, Low-Level Shared Code, without dependencies on other modules + +## Bun-Compatibility +Full bun compatible + +## Enums +All enums used across more than one module +Additional with zod Schema but working only with zod v4 and this needs typescript 5 + +## Schemas +All schemas for data validation used across more than one module +Tests written for bun diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 000000000..31190ec7c --- /dev/null +++ b/shared/package.json @@ -0,0 +1,42 @@ +{ + "name": "shared", + "version": "2.6.0", + "description": "Gradido Shared Code, Low-Level Shared Code, without dependencies on other modules", + "main": "./build/index.js", + "types": "./src/index.ts", + "exports": { + ".": { + "import": "./build/index.js", + "require": "./build/index.js" + } + }, + "repository": "https://github.com/gradido/gradido/shared", + "author": "Gradido Academy - https://www.gradido.net", + "license": "Apache-2.0", + "private": true, + "scripts": { + "build": "esbuild src/index.ts --outdir=build --platform=node --target=node18.20.7 --bundle --packages=external", + "build:bun": "bun build src/index.ts --outdir=build --target=bun --packages=external", + "test": "bun test", + "test:debug": "bun test --inspect-brk", + "typecheck": "tsc --noEmit", + "lint": "biome check --error-on-warnings .", + "lint:fix": "biome check --error-on-warnings . --write" + }, + "devDependencies": { + "@biomejs/biome": "2.0.0", + "@types/node": "^17.0.21", + "@types/uuid": "^10.0.0", + "typescript": "^4.9.5", + "uuid": "^8.3.2" + }, + "dependencies": { + "decimal.js-light": "^2.5.1", + "esbuild": "^0.25.2", + "log4js": "^6.9.1", + "zod": "^3.25.61" + }, + "engines": { + "node": ">=18" + } +} diff --git a/shared/src/const/index.ts b/shared/src/const/index.ts new file mode 100644 index 000000000..6976b019a --- /dev/null +++ b/shared/src/const/index.ts @@ -0,0 +1,2 @@ +export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z') +export const LOG4JS_BASE_CATEGORY_NAME = 'shared' \ No newline at end of file diff --git a/shared/src/enum/OptInType.ts b/shared/src/enum/OptInType.ts new file mode 100644 index 000000000..1f6300b39 --- /dev/null +++ b/shared/src/enum/OptInType.ts @@ -0,0 +1,8 @@ +// not compatible with typescript 4 +// import { enum as zEnum } from 'zod/v4-mini' +export enum OptInType { + EMAIL_OPT_IN_REGISTER = 1, + EMAIL_OPT_IN_RESET_PASSWORD = 2, +} + +// export const OptInTypeSchema = zEnum(OptInType) \ No newline at end of file diff --git a/shared/src/enum/PasswordEncryptionType.ts b/shared/src/enum/PasswordEncryptionType.ts new file mode 100644 index 000000000..13b72be0c --- /dev/null +++ b/shared/src/enum/PasswordEncryptionType.ts @@ -0,0 +1,11 @@ +// not compatible with typescript 4 +// import { enum as zEnum } from 'zod/v4-mini' + +export enum PasswordEncryptionType { + NO_PASSWORD = 0, + EMAIL = 1, + GRADIDO_ID = 2, +} + +// export const PasswordEncryptionTypeSchema = zEnum(PasswordEncryptionType) + diff --git a/shared/src/enum/PendingTransactionState.ts b/shared/src/enum/PendingTransactionState.ts new file mode 100644 index 000000000..644fdc8dc --- /dev/null +++ b/shared/src/enum/PendingTransactionState.ts @@ -0,0 +1,6 @@ +export enum PendingTransactionState { + NEW = 1, + PENDING = 2, + SETTLED = 3, + REVERTED = 4, +} diff --git a/shared/src/enum/RoleNames.ts b/shared/src/enum/RoleNames.ts new file mode 100644 index 000000000..fec1f47a1 --- /dev/null +++ b/shared/src/enum/RoleNames.ts @@ -0,0 +1,13 @@ +// not compatible with typescript 4 +// import { enum as zEnum } from 'zod/v4-mini' + +export enum RoleNames { + UNAUTHORIZED = 'UNAUTHORIZED', + USER = 'USER', + MODERATOR = 'MODERATOR', + MODERATOR_AI = 'MODERATOR_AI', + ADMIN = 'ADMIN', + DLT_CONNECTOR = 'DLT_CONNECTOR_ROLE', +} + +// export const RoleNamesSchema = zEnum(RoleNames) diff --git a/shared/src/enum/UserContactType.ts b/shared/src/enum/UserContactType.ts new file mode 100644 index 000000000..c6f1ef03b --- /dev/null +++ b/shared/src/enum/UserContactType.ts @@ -0,0 +1,9 @@ +// not compatible with typescript 4 +// import { enum as zEnum } from 'zod/v4-mini' + +export enum UserContactType { + USER_CONTACT_EMAIL = 'EMAIL', + USER_CONTACT_PHONE = 'PHONE', +} + +// export const UserContactTypeSchema = zEnum(UserContactType) diff --git a/shared/src/enum/index.ts b/shared/src/enum/index.ts new file mode 100644 index 000000000..adadf38ce --- /dev/null +++ b/shared/src/enum/index.ts @@ -0,0 +1,5 @@ +export * from './RoleNames' +export * from './UserContactType' +export * from './PasswordEncryptionType' +export * from './OptInType' +export * from './PendingTransactionState' diff --git a/shared/src/index.ts b/shared/src/index.ts new file mode 100644 index 000000000..4c6acd552 --- /dev/null +++ b/shared/src/index.ts @@ -0,0 +1,3 @@ +export * from './schema' +export * from './enum' +export * from './logic/decay' diff --git a/backend/src/util/decay.test.ts b/shared/src/logic/decay.test.ts similarity index 81% rename from backend/src/util/decay.test.ts rename to shared/src/logic/decay.test.ts index f419982ac..7678c945b 100644 --- a/backend/src/util/decay.test.ts +++ b/shared/src/logic/decay.test.ts @@ -10,6 +10,7 @@ describe('utils/decay', () => { // TODO: toString() was required, we could not compare two decimals expect(decayFormula(amount, seconds).toString()).toBe('0.999999978035040489732012') }) + it('has correct backward calculation', () => { const amount = new Decimal(1.0) const seconds = -1 @@ -25,6 +26,7 @@ describe('utils/decay', () => { expect(decayFormula(amount, seconds).toString()).toBe('1.0') }) }) + it('has base 0.99999997802044727', () => { const now = new Date() now.setSeconds(1) @@ -39,4 +41,13 @@ describe('utils/decay', () => { const now = new Date() expect(calculateDecay(new Decimal(100.0), now, now).balance.toString()).toBe('100') }) + + describe('calculateDecay called with invalid dates', () => { + it('throws an error when to is before from', () => { + const now = new Date() + const oneSecondAgo = new Date(now.getTime()) + oneSecondAgo.setSeconds(0) + expect(() => calculateDecay(new Decimal(1.0), now, oneSecondAgo)).toThrowError() + }) + }) }) diff --git a/backend/src/util/decay.ts b/shared/src/logic/decay.ts similarity index 55% rename from backend/src/util/decay.ts rename to shared/src/logic/decay.ts index 1d1775cc9..35ab99803 100644 --- a/backend/src/util/decay.ts +++ b/shared/src/logic/decay.ts @@ -1,31 +1,43 @@ import { Decimal } from 'decimal.js-light' -import { Decay } from '@model/Decay' +import { DECAY_START_TIME } from '../const' -import { CONFIG } from '@/config' -import { LogError } from '@/server/LogError' +Decimal.set({ + precision: 25, + rounding: Decimal.ROUND_HALF_UP, +}) -// TODO: externalize all those definitions and functions into an external decay library - -function decayFormula(value: Decimal, seconds: number): Decimal { - // TODO why do we need to convert this here to a stting to work properly? +export interface Decay { + balance: Decimal + decay: Decimal + roundedDecay: Decimal + start: Date | null + end: Date | null + duration: number | null +} + +export function decayFormula(value: Decimal, seconds: number): Decimal { + // TODO why do we need to convert this here to a string to work properly? + // chatgpt: We convert to string here to avoid precision loss: + // .pow(seconds) can internally round the result, especially for large values of `seconds`. + // Using .toString() ensures full precision is preserved in the multiplication. return value.mul( new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds).toString(), ) } -function calculateDecay( +export function calculateDecay( amount: Decimal, from: Date, - to: Date, - startBlock: Date = CONFIG.DECAY_START_TIME, + to: Date ): Decay { const fromMs = from.getTime() const toMs = to.getTime() - const startBlockMs = startBlock.getTime() + const startBlockMs = DECAY_START_TIME.getTime() if (toMs < fromMs) { - throw new LogError('calculateDecay: to < from, reverse decay calculation is invalid') + // TODO: refactor, use custom Error Classes which contain context variables + throw new Error('calculateDecay: to < from, reverse decay calculation is invalid') } // Initialize with no decay @@ -49,7 +61,7 @@ function calculateDecay( } // decay started between start and end date; decay from decay start till end date else { - decay.start = startBlock + decay.start = DECAY_START_TIME decay.duration = (toMs - startBlockMs) / 1000 } @@ -62,5 +74,3 @@ function calculateDecay( .mul(-1) return decay } - -export { decayFormula, calculateDecay } diff --git a/shared/src/schema/base.schema.test.ts b/shared/src/schema/base.schema.test.ts new file mode 100644 index 000000000..3e56a9a22 --- /dev/null +++ b/shared/src/schema/base.schema.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'bun:test' +import { uuidv4Schema } from './base.schema' +import { v4 as uuidv4 } from 'uuid' + +describe('uuidv4 schema', () => { + it('should validate uuidv4 (40x)', () => { + for (let i = 0; i < 40; i++) { + const uuid = uuidv4() + expect(uuidv4Schema.safeParse(uuid).success).toBeTruthy() + } + }) +}) diff --git a/shared/src/schema/base.schema.ts b/shared/src/schema/base.schema.ts new file mode 100644 index 000000000..ed341c48b --- /dev/null +++ b/shared/src/schema/base.schema.ts @@ -0,0 +1,6 @@ +import { string } from 'zod' +import { validate, version } from 'uuid' + +export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid') +export const emailSchema = string().email() +export const urlSchema = string().url() \ No newline at end of file diff --git a/shared/src/schema/index.ts b/shared/src/schema/index.ts new file mode 100644 index 000000000..d8c9f9e4c --- /dev/null +++ b/shared/src/schema/index.ts @@ -0,0 +1,2 @@ +export * from './user.schema' +export * from './base.schema' \ No newline at end of file diff --git a/shared/src/schema/user.schema.test.ts b/shared/src/schema/user.schema.test.ts new file mode 100644 index 000000000..015309dfd --- /dev/null +++ b/shared/src/schema/user.schema.test.ts @@ -0,0 +1,129 @@ +import { aliasSchema, firstNameSchema } from './user.schema' +import { describe, it, expect } from 'bun:test' + +describe('validate alias', () => { + describe('alias contains invalid characters', () => { + it('throws and logs an error', () => { + expect(() => aliasSchema.parse('Bibi.Bloxberg')).toThrowError(expect.objectContaining( + expect.arrayContaining([ + expect.objectContaining({ + origin: 'string', + code: 'invalid_format', + format: 'regex', + message: 'Invalid characters in alias', + path: [], + }) + ]) + )) + }) + }) + + describe('alias is a reserved word', () => { + it('throws and logs an error', () => { + expect(() => aliasSchema.parse('admin')).toThrowError(expect.objectContaining( + expect.arrayContaining([ + expect.objectContaining({ + code: 'custom', + message: 'Given alias is not allowed', + path: [], + }), + ]), + )) + }) + }) + + describe('alias length', () => { + it('2 characters is not ok', () => { + expect(() => aliasSchema.parse('Bi')).toThrowError() + }) + it('3 characters is ok', () => { + expect(() => aliasSchema.parse('Bib')).not.toThrowError() + }) + it('20 characters is ok', () => { + expect(() => aliasSchema.parse('BibiBloxbergMondLich')).not.toThrowError() + }) + it('21 characters is not ok', () => { + expect(() => aliasSchema.parse('BibiBloxbergZauberwald')).toThrowError() + }) + }) + + describe('alias is a reserved word with uppercase characters', () => { + it('throws and logs an error', () => { + expect(() => aliasSchema.parse('Admin')).toThrowError(expect.objectContaining( + expect.arrayContaining([ + expect.objectContaining({ + code: 'custom', + message: 'Given alias is not allowed', + path: [], + }), + ]), + )) + }) + }) + + describe('hyphens and underscore', () => { + describe('alias starts with underscore', () => { + it('throws and logs an error', () => { + expect(() => aliasSchema.parse('_bibi')).toThrowError(expect.objectContaining( + expect.arrayContaining([ + expect.objectContaining({ + origin: 'string', + code: 'invalid_format', + format: 'regex', + message: 'Invalid characters in alias', + path: [], + }) + ]) + )) + }) + }) + + describe('alias contains two following hyphens', () => { + it('throws and logs an error', () => { + expect(() => aliasSchema.parse('bi--bi')).toThrowError(expect.objectContaining( + expect.arrayContaining([ + expect.objectContaining({ + origin: 'string', + code: 'invalid_format', + format: 'regex', + message: 'Invalid characters in alias', + path: [], + }) + ]) + )) + }) + }) + }) +}) + +describe('validate first name', () => { + describe('first name contains invalid characters', () => { + it('throws and logs an error', () => { + expect(() => firstNameSchema.parse('')).toThrowError(expect.objectContaining( + expect.arrayContaining([ + expect.objectContaining({ + origin: 'string', + code: 'invalid_format', + format: 'regex', + message: 'Invalid characters in first name', + path: [], + }) + ]) + )) + }) + }) + it('use greek symbols', () => { + expect(() => firstNameSchema.parse('Αλέξανδρος')).not.toThrowError() + }) + it('use korean symbols', () => { + expect(() => firstNameSchema.parse('김민수')).not.toThrowError() + }) + // TODO: use min length depending of language, because in asiatic languages first and/or last names can have only one character + it.skip('use japanese symbols', () => { + expect(() => firstNameSchema.parse('田中')).not.toThrowError() + }) + // TODO: fix this + it.skip('use chinese symbols', () => { + expect(() => firstNameSchema.parse('张三')).not.toThrowError() + }) +}) diff --git a/shared/src/schema/user.schema.ts b/shared/src/schema/user.schema.ts new file mode 100644 index 000000000..494ee03b6 --- /dev/null +++ b/shared/src/schema/user.schema.ts @@ -0,0 +1,45 @@ +import { string } from 'zod' + +export const VALID_ALIAS_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ +// \p{L} = a character from every alphabet (latin, greek, cyrillic, etc.) +// first a character or ' is expected +// then all without the last a character, space, apostrophe or hyphen is expected +// last a character is expected +export const VALID_NAME_REGEX = /^[\p{L}'][ \p{L}'-_]*[\p{L}]$/u + +const RESERVED_ALIAS = [ + 'admin', + 'email', + 'gast', + 'gdd', + 'gradido', + 'guest', + 'home', + 'root', + 'support', + 'temp', + 'tmp', + 'user', + 'usr', + 'var', +] + +export const aliasSchema = string() + .min(3, 'Given alias is too short') + .max(20, 'Given alias is too long') + .regex(VALID_ALIAS_REGEX, 'Invalid characters in alias') + .refine((val) => !RESERVED_ALIAS.includes(val.toLowerCase()), { + message: 'Given alias is not allowed', + }) + +// TODO: use this schemas in backend, think about case which currently not fullfil the regex +// (some user start there name with : ) +export const firstNameSchema = string() + .min(3, 'First name is too short') + .max(255, 'First name is too long') + .regex(VALID_NAME_REGEX) + +export const lastNameSchema = string() + .min(2, 'Last name is too short') + .max(255, 'Last name is too long') + .regex(VALID_NAME_REGEX) \ No newline at end of file diff --git a/shared/tsconfig.json b/shared/tsconfig.json new file mode 100644 index 000000000..75b686340 --- /dev/null +++ b/shared/tsconfig.json @@ -0,0 +1,73 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./build/outfile.js", /* Concatenate and emit output to single file. */ + "outDir": "./build", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [".", "../database"], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": ["bun-types"], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + }, + "references": [], /* Any project that is referenced must itself have a `references` array (which may be empty). */ + "exclude": ["**/*.test.ts", "**/*.spec.ts", "test/*", "**/bun.d.ts"], +} diff --git a/turbo.json b/turbo.json index 65b0842a3..f3f8d92d2 100644 --- a/turbo.json +++ b/turbo.json @@ -6,11 +6,12 @@ "lint:fix": { }, "test": { + "dependsOn": ["^build"] }, "typecheck": { }, "dev": { - "dependsOn": ["config-schema#build"], + "dependsOn": ["^build"], "persistent": true, "cache": false },