Merge branch 'master' into admin_add_registerdAt_on_usersearch

This commit is contained in:
einhornimmond 2025-12-04 09:03:12 +01:00 committed by GitHub
commit d701e5c5cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 659 additions and 463 deletions

View File

@ -51,8 +51,8 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: docker-compose mariadb - name: docker-compose mariadb redis
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb redis
- name: install bun - name: install bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2

View File

@ -49,7 +49,7 @@ jobs:
node-version: '18.20.7' node-version: '18.20.7'
- name: Database | docker-compose - name: Database | docker-compose
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb redis
- name: install bun - name: install bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2

View File

@ -48,8 +48,8 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: docker-compose mariadb - name: docker-compose mariadb redis
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb redis
- name: install bun - name: install bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2

View File

@ -20,8 +20,8 @@ jobs:
with: with:
bun-version-file: '.bun-version' bun-version-file: '.bun-version'
- name: Boot up test system | docker-compose mariadb mailserver - name: Boot up test system | docker-compose mariadb mailserver redis
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver redis
- name: Prepare test system - name: Prepare test system
run: | run: |
@ -63,10 +63,11 @@ jobs:
sudo nginx -t sudo nginx -t
sudo systemctl start nginx sudo systemctl start nginx
- name: wait for nginx and mailserver to be ready - name: wait for nginx, mailserver and redis to be ready
run: | run: |
until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done; until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done;
until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done; until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done;
until nc -z 127.0.0.1 6379; do echo waiting for redis; sleep 1; done;
- name: End-to-end tests | run tests - name: End-to-end tests | run tests
id: e2e-tests id: e2e-tests
@ -125,8 +126,8 @@ jobs:
with: with:
bun-version-file: '.bun-version' bun-version-file: '.bun-version'
- name: Boot up test system | docker-compose mariadb mailserver - name: Boot up test system | docker-compose mariadb mailserver redis
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver redis
- name: Prepare test system - name: Prepare test system
run: | run: |
@ -169,10 +170,11 @@ jobs:
sudo nginx -t sudo nginx -t
sudo systemctl start nginx sudo systemctl start nginx
- name: wait for nginx and mailserver to be ready - name: wait for nginx, mailserver and redis to be ready
run: | run: |
until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done; until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done;
until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done; until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done;
until nc -z 127.0.0.1 6379; do echo waiting for redis; sleep 1; done;
- name: End-to-end tests | run tests - name: End-to-end tests | run tests
id: e2e-tests id: e2e-tests
@ -210,8 +212,8 @@ jobs:
with: with:
bun-version-file: '.bun-version' bun-version-file: '.bun-version'
- name: Boot up test system | docker-compose mariadb mailserver - name: Boot up test system | docker-compose mariadb mailserver redis
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver redis
- name: Prepare test system - name: Prepare test system
run: | run: |
@ -250,10 +252,11 @@ jobs:
sudo nginx -t sudo nginx -t
sudo systemctl start nginx sudo systemctl start nginx
- name: wait for nginx and mailserver to be ready - name: wait for nginx, mailserver and redis to be ready
run: | run: |
until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done; until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done;
until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done; until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done;
until nc -z 127.0.0.1 6379; do echo waiting for redis; sleep 1; done;
- name: End-to-end tests | run tests - name: End-to-end tests | run tests
id: e2e-tests id: e2e-tests

View File

@ -48,8 +48,8 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: docker-compose mariadb - name: docker-compose mariadb redis
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb redis
- name: install bun - name: install bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2

View File

@ -105,6 +105,22 @@ turbo start
[More Infos for using turbo](./working-native.md) [More Infos for using turbo](./working-native.md)
### Dependencies & Bundling
This project uses esbuild to bundle the main modules (backend, dht-node, federation) into single JavaScript files for optimized deployment. To ensure a minimal and reliable Docker image, dependencies are intentionally split:
- dependencies: Only packages that cannot be bundled by esbuild into the output files.
Examples include:
- Native modules (sodium-native)
- Packages incompatible with bundling (email-templates)
- Runtime helpers (cross-env)
- devDependencies: All other packages that are fully bundled into the build output by esbuild.
This setup ensures that:
- The production Docker image contains only the minimal set of necessary runtime modules.
- Native or runtime-sensitive packages are included in node_modules for proper execution.
Note: Even if Docker is not used in all environments, this organization ensures consistent and predictable builds across different platforms.
### For Windows ### For Windows

View File

@ -84,6 +84,7 @@
"openai": "^4.87.3", "openai": "^4.87.3",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"random-bigint": "^0.0.1", "random-bigint": "^0.0.1",
"redis-semaphore": "^5.6.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"regenerator-runtime": "^0.14.1", "regenerator-runtime": "^0.14.1",
"shared": "*", "shared": "*",

View File

@ -7,7 +7,7 @@ import { DataSource, Not } from 'typeorm'
import { cleanDB, testEnvironment } from '@test/helpers' import { cleanDB, testEnvironment } from '@test/helpers'
import { getLogger } from 'config-schema/test/testSetup' import { getLogger } from 'config-schema/test/testSetup'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { AppDatabase } from 'database'
import { validateCommunities } from './validateCommunities' import { validateCommunities } from './validateCommunities'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`) const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`)
@ -16,21 +16,25 @@ const federationClientLogger = getLogger(
) )
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment(logger) testEnv = await testEnvironment(logger)
con = testEnv.con con = testEnv.con
db = testEnv.db
await cleanDB() await cleanDB()
}) })
afterAll(async () => { afterAll(async () => {
// await cleanDB() // await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
describe('validate Communities', () => { describe('validate Communities', () => {

View File

@ -19,6 +19,7 @@ import { createCommunity, createVerifiedFederatedCommunity } from 'database/src/
import { getLogger } from 'config-schema/test/testSetup' import { getLogger } from 'config-schema/test/testSetup'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { AppDatabase } from 'database'
jest.mock('@/password/EncryptorUtils') jest.mock('@/password/EncryptorUtils')
@ -28,11 +29,12 @@ CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER = 1000
let mutate: ApolloServerTestClient['mutate'] let mutate: ApolloServerTestClient['mutate']
let query: ApolloServerTestClient['query'] let query: ApolloServerTestClient['query']
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
const peterLoginData = { const peterLoginData = {
@ -46,6 +48,7 @@ beforeAll(async () => {
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
db = testEnv.db
await cleanDB() await cleanDB()
// reset id auto increment // reset id auto increment
await DbCommunity.clear() await DbCommunity.clear()
@ -54,6 +57,7 @@ beforeAll(async () => {
afterAll(async () => { afterAll(async () => {
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
// real valid ed25519 key pairs // real valid ed25519 key pairs

View File

@ -19,6 +19,7 @@ import { listContributionLinks } from '@/seeds/graphql/queries'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { getLogger } from 'config-schema/test/testSetup' import { getLogger } from 'config-schema/test/testSetup'
import { AppDatabase } from 'database'
jest.mock('@/password/EncryptorUtils') jest.mock('@/password/EncryptorUtils')
@ -27,10 +28,12 @@ const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
let mutate: ApolloServerTestClient['mutate'] let mutate: ApolloServerTestClient['mutate']
let query: ApolloServerTestClient['query'] let query: ApolloServerTestClient['query']
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
beforeAll(async () => { beforeAll(async () => {
@ -38,6 +41,7 @@ beforeAll(async () => {
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
db = testEnv.db
await cleanDB() await cleanDB()
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig) await userFactory(testEnv, peterLustig)
@ -46,6 +50,7 @@ beforeAll(async () => {
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
describe('Contribution Links', () => { describe('Contribution Links', () => {

View File

@ -21,6 +21,7 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { getLogger} from 'config-schema/test/testSetup' import { getLogger} from 'config-schema/test/testSetup'
import { AppDatabase } from 'database'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionMessageResolver`) const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionMessageResolver`)
const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
@ -40,10 +41,12 @@ jest.mock('core', () => {
let mutate: ApolloServerTestClient['mutate'] let mutate: ApolloServerTestClient['mutate']
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
let result: any let result: any
@ -51,12 +54,14 @@ beforeAll(async () => {
testEnv = await testEnvironment(logger) testEnv = await testEnvironment(logger)
mutate = testEnv.mutate mutate = testEnv.mutate
con = testEnv.con con = testEnv.con
db = testEnv.db
await cleanDB() await cleanDB()
}) })
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
describe('ContributionMessageResolver', () => { describe('ContributionMessageResolver', () => {

View File

@ -52,6 +52,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { getFirstDayOfPreviousNMonth } from 'core' import { getFirstDayOfPreviousNMonth } from 'core'
import { getLogger } from 'config-schema/test/testSetup' import { getLogger } from 'config-schema/test/testSetup'
import { getLogger as originalGetLogger } from 'log4js' import { getLogger as originalGetLogger } from 'log4js'
import { AppDatabase } from 'database'
jest.mock('core', () => { jest.mock('core', () => {
const originalModule = jest.requireActual('core') const originalModule = jest.requireActual('core')
@ -71,10 +72,12 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
let mutate: ApolloServerTestClient['mutate'] let mutate: ApolloServerTestClient['mutate']
let query: ApolloServerTestClient['query'] let query: ApolloServerTestClient['query']
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
let creation: Contribution | null let creation: Contribution | null
let admin: User let admin: User
@ -90,12 +93,14 @@ beforeAll(async () => {
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
db = testEnv.db
await cleanDB() await cleanDB()
}) })
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
describe('ContributionResolver', () => { describe('ContributionResolver', () => {

View File

@ -1,8 +1,9 @@
import { import {
AppDatabase,
Contribution as DbContribution, Contribution as DbContribution,
Transaction as DbTransaction, Transaction as DbTransaction,
User as DbUser, User as DbUser,
DltTransaction as DbDltTransaction, getLastTransaction,
UserContact, UserContact,
} from 'database' } from 'database'
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
@ -10,26 +11,7 @@ import { GraphQLResolveInfo } from 'graphql'
import { Arg, Args, Authorized, Ctx, Info, Int, Mutation, Query, Resolver } from 'type-graphql' import { Arg, Args, Authorized, Ctx, Info, Int, Mutation, Query, Resolver } from 'type-graphql'
import { EntityManager, IsNull } from 'typeorm' import { EntityManager, IsNull } from 'typeorm'
import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs'
import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
import { ContributionArgs } from '@arg/ContributionArgs'
import { Paginated } from '@arg/Paginated'
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
import { ContributionStatus } from '@enum/ContributionStatus'
import { ContributionType } from '@enum/ContributionType'
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
import { Contribution, ContributionListResult } from '@model/Contribution'
import { OpenCreation } from '@model/OpenCreation'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import {
fullName,
sendContributionChangedByModeratorEmail,
sendContributionConfirmedEmail,
sendContributionDeletedEmail,
sendContributionDeniedEmail,
TransactionTypeId
} from 'core'
import { import {
EVENT_ADMIN_CONTRIBUTION_CONFIRM, EVENT_ADMIN_CONTRIBUTION_CONFIRM,
EVENT_ADMIN_CONTRIBUTION_CREATE, EVENT_ADMIN_CONTRIBUTION_CREATE,
@ -43,13 +25,32 @@ import {
import { UpdateUnconfirmedContributionContext } from '@/interactions/updateUnconfirmedContribution/UpdateUnconfirmedContribution.context' import { UpdateUnconfirmedContributionContext } from '@/interactions/updateUnconfirmedContribution/UpdateUnconfirmedContribution.context'
import { LogError } from '@/server/LogError' import { LogError } from '@/server/LogError'
import { Context, getClientTimezoneOffset, getUser } from '@/server/context' import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
import { TRANSACTIONS_LOCK } from 'database' import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs'
import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
import { ContributionArgs } from '@arg/ContributionArgs'
import { Paginated } from '@arg/Paginated'
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
import { ContributionStatus } from '@enum/ContributionStatus'
import { ContributionType } from '@enum/ContributionType'
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
import { Contribution, ContributionListResult } from '@model/Contribution'
import { OpenCreation } from '@model/OpenCreation'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import {
fullName,
sendContributionChangedByModeratorEmail,
sendContributionConfirmedEmail,
sendContributionDeletedEmail,
sendContributionDeniedEmail,
TransactionTypeId
} from 'core'
import { calculateDecay, Decay } from 'shared' import { calculateDecay, Decay } from 'shared'
import { contributionTransaction } from '@/apis/dltConnector'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { ContributionMessageType } from '@enum/ContributionMessageType' import { ContributionMessageType } from '@enum/ContributionMessageType'
import { AppDatabase } from 'database'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { Mutex } from 'redis-semaphore'
import { import {
contributionFrontendLink, contributionFrontendLink,
loadAllContributions, loadAllContributions,
@ -58,8 +59,6 @@ import {
import { getOpenCreations, getUserCreation, validateContribution } from './util/creations' import { getOpenCreations, getUserCreation, validateContribution } from './util/creations'
import { extractGraphQLFields } from './util/extractGraphQLFields' import { extractGraphQLFields } from './util/extractGraphQLFields'
import { findContributions } from './util/findContributions' import { findContributions } from './util/findContributions'
import { getLastTransaction } from 'database'
import { contributionTransaction } from '@/apis/dltConnector'
const db = AppDatabase.getInstance() const db = AppDatabase.getInstance()
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionResolver`) const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionResolver`)
@ -436,7 +435,9 @@ export class ContributionResolver {
const logger = createLogger() const logger = createLogger()
logger.addContext('contribution', id) logger.addContext('contribution', id)
// acquire lock // acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire() const mutex = new Mutex (db.getRedisClient(), 'TRANSACTIONS_LOCK')
await mutex.acquire()
try { try {
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
const contribution = await DbContribution.findOne({ where: { id }, relations: {user: {emailContact: true}} }) const contribution = await DbContribution.findOne({ where: { id }, relations: {user: {emailContact: true}} })
@ -549,7 +550,8 @@ export class ContributionResolver {
} }
await EVENT_ADMIN_CONTRIBUTION_CONFIRM(user, moderatorUser, contribution, contribution.amount) await EVENT_ADMIN_CONTRIBUTION_CONFIRM(user, moderatorUser, contribution, contribution.amount)
} finally { } finally {
releaseLock() // releaseLock()
await mutex.release()
} }
return true return true
} }

View File

@ -10,14 +10,17 @@ import { CONFIG } from '@/config'
import { writeHomeCommunityEntry } from '@/seeds/community' import { writeHomeCommunityEntry } from '@/seeds/community'
import { createUser, forgotPassword, setPassword } from '@/seeds/graphql/mutations' import { createUser, forgotPassword, setPassword } from '@/seeds/graphql/mutations'
import { queryOptIn } from '@/seeds/graphql/queries' import { queryOptIn } from '@/seeds/graphql/queries'
import { AppDatabase } from 'database'
let mutate: ApolloServerTestClient['mutate'] let mutate: ApolloServerTestClient['mutate']
let query: ApolloServerTestClient['query'] let query: ApolloServerTestClient['query']
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
CONFIG.EMAIL_CODE_VALID_TIME = 1440 CONFIG.EMAIL_CODE_VALID_TIME = 1440
@ -29,12 +32,14 @@ beforeAll(async () => {
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
db = testEnv.db
await cleanDB() await cleanDB()
}) })
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
describe('EmailOptinCodes', () => { describe('EmailOptinCodes', () => {

View File

@ -9,6 +9,7 @@ import { userFactory } from '@/seeds/factory/user'
import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { AppDatabase } from 'database'
jest.mock('@/password/EncryptorUtils') jest.mock('@/password/EncryptorUtils')
@ -17,17 +18,20 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.Klicktip
let testEnv: any let testEnv: any
let mutate: any let mutate: any
let con: any let con: any
let db: AppDatabase
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment(logger) testEnv = await testEnvironment(logger)
mutate = testEnv.mutate mutate = testEnv.mutate
con = testEnv.con con = testEnv.con
db = testEnv.db
await cleanDB() await cleanDB()
}) })
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
describe('KlicktippResolver', () => { describe('KlicktippResolver', () => {

View File

@ -32,12 +32,11 @@ import { listTransactionLinksAdmin } from '@/seeds/graphql/queries'
import { transactionLinks } from '@/seeds/transactionLink/index' import { transactionLinks } from '@/seeds/transactionLink/index'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { TRANSACTIONS_LOCK } from 'database'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { getLogger } from 'config-schema/test/testSetup' import { getLogger } from 'config-schema/test/testSetup'
import { transactionLinkCode } from './TransactionLinkResolver' import { transactionLinkCode } from './TransactionLinkResolver'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { AppDatabase } from 'database'
const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
@ -46,16 +45,18 @@ jest.mock('@/password/EncryptorUtils')
CONFIG.DLT_CONNECTOR = false CONFIG.DLT_CONNECTOR = false
// mock semaphore to allow use fake timers // mock semaphore to allow use fake timers
jest.mock('database/src/util/TRANSACTIONS_LOCK') // jest.mock('database/src/util/TRANSACTIONS_LOCK')
TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn()) // TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
let mutate: ApolloServerTestClient['mutate'] let mutate: ApolloServerTestClient['mutate']
let query: ApolloServerTestClient['query'] let query: ApolloServerTestClient['query']
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
let user: User let user: User
@ -65,6 +66,7 @@ beforeAll(async () => {
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
db = testEnv.db
await cleanDB() await cleanDB()
await userFactory(testEnv, bibiBloxberg) await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig) await userFactory(testEnv, peterLustig)
@ -73,6 +75,7 @@ beforeAll(async () => {
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
describe('TransactionLinkResolver', () => { describe('TransactionLinkResolver', () => {

View File

@ -39,7 +39,7 @@ import { LogError } from '@/server/LogError'
import { Context, getClientTimezoneOffset, getUser } from '@/server/context' import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
import { calculateBalance } from '@/util/validate' import { calculateBalance } from '@/util/validate'
import { fullName } from 'core' import { fullName } from 'core'
import { TRANSACTION_LINK_LOCK, TRANSACTIONS_LOCK } from 'database' // import { TRANSACTION_LINK_LOCK, TRANSACTIONS_LOCK } from 'database'
import { import {
calculateDecay, calculateDecay,
compoundInterest, compoundInterest,
@ -68,6 +68,8 @@ import { transactionLinkList } from './util/transactionLinkList'
import { SignedTransferPayloadType } from 'shared' import { SignedTransferPayloadType } from 'shared'
import { contributionTransaction, deferredTransferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector' import { contributionTransaction, deferredTransferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector'
import { CODE_VALID_DAYS_DURATION } from './const/const' import { CODE_VALID_DAYS_DURATION } from './const/const'
import { Redis } from 'ioredis'
import { Mutex } from 'redis-semaphore'
const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionLinkResolver.${method}`) const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionLinkResolver.${method}`)
@ -237,7 +239,9 @@ export class TransactionLinkResolver {
const user = getUser(context) const user = getUser(context)
if (code.match(/^CL-/)) { if (code.match(/^CL-/)) {
// acquire lock // acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire() // const releaseLock = await TRANSACTIONS_LOCK.acquire()
const mutex = new Mutex(db.getRedisClient(), 'TRANSACTIONS_LOCK')
await mutex.acquire()
try { try {
methodLogger.info('redeem contribution link...') methodLogger.info('redeem contribution link...')
const now = new Date() const now = new Date()
@ -392,11 +396,14 @@ export class TransactionLinkResolver {
await queryRunner.release() await queryRunner.release()
} }
} finally { } finally {
releaseLock() // releaseLock()
await mutex.release()
} }
return true return true
} else { } else {
const releaseLinkLock = await TRANSACTION_LINK_LOCK.acquire() // const releaseLinkLock = await TRANSACTION_LINK_LOCK.acquire()
const mutex = new Mutex(db.getRedisClient(), 'TRANSACTION_LINK_LOCK')
await mutex.acquire()
const now = new Date() const now = new Date()
try { try {
const transactionLink = await DbTransactionLink.findOne({ where: { code } }) const transactionLink = await DbTransactionLink.findOne({ where: { code } })
@ -441,7 +448,8 @@ export class TransactionLinkResolver {
transactionLink.amount, transactionLink.amount,
) )
} finally { } finally {
releaseLinkLock() // releaseLinkLock()
await mutex.release()
} }
return true return true
} }

View File

@ -35,6 +35,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { getLogger } from 'config-schema/test/testSetup' import { getLogger } from 'config-schema/test/testSetup'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { CONFIG as CORE_CONFIG} from 'core' import { CONFIG as CORE_CONFIG} from 'core'
import { AppDatabase } from 'database'
jest.mock('@/password/EncryptorUtils') jest.mock('@/password/EncryptorUtils')
@ -49,6 +50,7 @@ let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
beforeAll(async () => { beforeAll(async () => {
@ -62,6 +64,7 @@ beforeAll(async () => {
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() // close() await con.destroy() // close()
await testEnv.db.getRedisClient().quit()
}) })
let bobData: any let bobData: any

View File

@ -33,7 +33,7 @@ import { Context, getUser } from '@/server/context'
import { communityUser } from '@/util/communityUser' import { communityUser } from '@/util/communityUser'
import { calculateBalance } from '@/util/validate' import { calculateBalance } from '@/util/validate'
import { virtualDecayTransaction, virtualLinkTransaction } from '@/util/virtualTransactions' import { virtualDecayTransaction, virtualLinkTransaction } from '@/util/virtualTransactions'
import { TRANSACTIONS_LOCK } from 'database' // import { TRANSACTIONS_LOCK } from 'database'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { getLastTransaction } from 'database' import { getLastTransaction } from 'database'
@ -44,6 +44,8 @@ import { getCommunityName, isHomeCommunity } from './util/communities'
import { getTransactionList } from './util/getTransactionList' import { getTransactionList } from './util/getTransactionList'
import { transactionLinkSummary } from './util/transactionLinkSummary' import { transactionLinkSummary } from './util/transactionLinkSummary'
import { transferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector' import { transferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector'
import { Redis } from 'ioredis'
import { Mutex } from 'redis-semaphore'
const db = AppDatabase.getInstance() const db = AppDatabase.getInstance()
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionResolver`) const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionResolver`)
@ -57,7 +59,10 @@ export const executeTransaction = async (
transactionLink?: dbTransactionLink | null, transactionLink?: dbTransactionLink | null,
): Promise<boolean> => { ): Promise<boolean> => {
// acquire lock // acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire() // const releaseLock = await TRANSACTIONS_LOCK.acquire()
const mutex = new Mutex(db.getRedisClient(), 'TRANSACTIONS_LOCK')
await mutex.acquire()
const receivedCallDate = new Date() const receivedCallDate = new Date()
let dltTransactionPromise: Promise<DbDltTransaction | null> = Promise.resolve(null) let dltTransactionPromise: Promise<DbDltTransaction | null> = Promise.resolve(null)
if (!transactionLink) { if (!transactionLink) {
@ -210,7 +215,8 @@ export const executeTransaction = async (
} }
logger.info(`finished executeTransaction successfully`) logger.info(`finished executeTransaction successfully`)
} finally { } finally {
releaseLock() // releaseLock()
await mutex.release()
} }
return true return true
} }

View File

@ -1,6 +1,7 @@
import { UserInputError } from 'apollo-server-express' import { UserInputError } from 'apollo-server-express'
import { ApolloServerTestClient } from 'apollo-server-testing' import { ApolloServerTestClient } from 'apollo-server-testing'
import { import {
AppDatabase,
Community as DbCommunity, Community as DbCommunity,
Event as DbEvent, Event as DbEvent,
TransactionLink, TransactionLink,
@ -103,10 +104,12 @@ let user: User
let mutate: ApolloServerTestClient['mutate'] let mutate: ApolloServerTestClient['mutate']
let query: ApolloServerTestClient['query'] let query: ApolloServerTestClient['query']
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
beforeAll(async () => { beforeAll(async () => {
@ -114,6 +117,7 @@ beforeAll(async () => {
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
db = testEnv.db
CONFIG.HUMHUB_ACTIVE = false CONFIG.HUMHUB_ACTIVE = false
CONFIG.DLT_CONNECTOR = false CONFIG.DLT_CONNECTOR = false
await cleanDB() await cleanDB()
@ -122,6 +126,7 @@ beforeAll(async () => {
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
describe('UserResolver', () => { describe('UserResolver', () => {

View File

@ -23,7 +23,9 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { CONFIG as CORE_CONFIG } from 'core' import { CONFIG as CORE_CONFIG } from 'core'
import { TRANSACTIONS_LOCK } from 'database' // import { TRANSACTIONS_LOCK } from 'database'
import { Mutex } from 'redis-semaphore'
import { AppDatabase } from 'database'
jest.mock('@/password/EncryptorUtils') jest.mock('@/password/EncryptorUtils')
@ -36,8 +38,8 @@ let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment()
mutate = testEnv.mutate mutate = testEnv.mutate
@ -48,41 +50,41 @@ beforeAll(async () => {
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await testEnv.db.getRedisClient().quit()
}) })
type RunOrder = { [key: number]: { start: number, end: number } } type WorkData = { start: number, end: number }
async function fakeWork(runOrder: RunOrder, index: number) { async function fakeWork(workData: WorkData[], index: number) {
const releaseLock = await TRANSACTIONS_LOCK.acquire() // const releaseLock = await TRANSACTIONS_LOCK.acquire()
// create a new mutex for every function call, like in production code
const mutex = new Mutex(testEnv.db.getRedisClient(), 'TRANSACTIONS_LOCK')
await mutex.acquire()
const startDate = new Date() const startDate = new Date()
await new Promise((resolve) => setTimeout(resolve, Math.random() * 50)) await new Promise((resolve) => setTimeout(resolve, Math.random() * 50))
const endDate = new Date() const endDate = new Date()
runOrder[index] = { start: startDate.getTime(), end: endDate.getTime() } workData[index] = { start: startDate.getTime(), end: endDate.getTime() }
releaseLock() // releaseLock()
await mutex.release()
} }
describe('semaphore', () => { describe('semaphore', () => {
it("didn't should run in parallel", async () => { it("didn't should run in parallel", async () => {
const runOrder: RunOrder = {} const workData: WorkData[] = []
await Promise.all([
fakeWork(runOrder, 1), const promises: Promise<void>[] = []
fakeWork(runOrder, 2), for(let i = 0; i < 20; i++) {
fakeWork(runOrder, 3), promises.push(fakeWork(workData, i))
fakeWork(runOrder, 4), }
fakeWork(runOrder, 5), await Promise.all(promises)
]) workData.sort((a, b) => a.start - b.start)
expect(runOrder[1].start).toBeLessThan(runOrder[1].end) workData.forEach((work, index) => {
expect(runOrder[1].start).toBeLessThan(runOrder[2].start) expect(work.start).toBeLessThan(work.end)
expect(runOrder[2].start).toBeLessThan(runOrder[2].end) if(index < workData.length - 1) {
expect(runOrder[2].start).toBeLessThan(runOrder[3].start) expect(work.start).toBeLessThan(workData[index + 1].start)
expect(runOrder[3].start).toBeLessThan(runOrder[3].end) expect(work.end).toBeLessThanOrEqual(workData[index + 1].start)
expect(runOrder[3].start).toBeLessThan(runOrder[4].start) }
expect(runOrder[4].start).toBeLessThan(runOrder[4].end) })
expect(runOrder[4].start).toBeLessThan(runOrder[5].start)
expect(runOrder[5].start).toBeLessThan(runOrder[5].end)
expect(runOrder[1].end).toBeLessThan(runOrder[2].end)
expect(runOrder[2].end).toBeLessThan(runOrder[3].end)
expect(runOrder[3].end).toBeLessThan(runOrder[4].end)
expect(runOrder[4].end).toBeLessThan(runOrder[5].end)
}) })
}) })

View File

@ -1,6 +1,7 @@
import { ApolloServerTestClient } from 'apollo-server-testing' import { ApolloServerTestClient } from 'apollo-server-testing'
import { Contribution, User } from 'database' import { Contribution, User } from 'database'
import { DataSource } from 'typeorm' import { DataSource } from 'typeorm'
import { AppDatabase } from 'database'
import { cleanDB, contributionDateFormatter, testEnvironment } from '@test/helpers' import { cleanDB, contributionDateFormatter, testEnvironment } from '@test/helpers'
@ -18,22 +19,26 @@ CONFIG.HUMHUB_ACTIVE = false
let mutate: ApolloServerTestClient['mutate'] let mutate: ApolloServerTestClient['mutate']
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment()
mutate = testEnv.mutate mutate = testEnv.mutate
con = testEnv.con con = testEnv.con
db = testEnv.db
await cleanDB() await cleanDB()
}) })
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
const setZeroHours = (date: Date): Date => { const setZeroHours = (date: Date): Date => {

View File

@ -9,7 +9,6 @@ import { slowDown } from 'express-slow-down'
import helmet from 'helmet' import helmet from 'helmet'
import { Logger, getLogger } from 'log4js' import { Logger, getLogger } from 'log4js'
import { DataSource } from 'typeorm' import { DataSource } from 'typeorm'
import { GRADIDO_REALM, LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { GRADIDO_REALM, LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { AppDatabase } from 'database' import { AppDatabase } from 'database'
import { context as serverContext } from './context' import { context as serverContext } from './context'
@ -23,6 +22,7 @@ interface ServerDef {
apollo: ApolloServer apollo: ApolloServer
app: Express app: Express
con: DataSource con: DataSource
db: AppDatabase
} }
export const createServer = async ( export const createServer = async (
@ -100,5 +100,5 @@ export const createServer = async (
) )
logger.debug('createServer...successful') logger.debug('createServer...successful')
return { apollo, app, con: db.getDataSource() } return { apollo, app, con: db.getDataSource(), db }
} }

View File

@ -1,6 +1,7 @@
import { ApolloServerTestClient } from 'apollo-server-testing' import { ApolloServerTestClient } from 'apollo-server-testing'
import { Event as DbEvent } from 'database' import { Event as DbEvent } from 'database'
import { DataSource } from 'typeorm' import { DataSource } from 'typeorm'
import { AppDatabase } from 'database'
import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
@ -19,22 +20,26 @@ jest.mock('@/password/EncryptorUtils')
let mutate: ApolloServerTestClient['mutate'] let mutate: ApolloServerTestClient['mutate']
let con: DataSource let con: DataSource
let db: AppDatabase
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: DataSource con: DataSource
db: AppDatabase
} }
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment()
mutate = testEnv.mutate mutate = testEnv.mutate
con = testEnv.con con = testEnv.con
db = testEnv.db
await DbEvent.clear() await DbEvent.clear()
}) })
afterAll(async () => { afterAll(async () => {
await cleanDB() await cleanDB()
await con.destroy() await con.destroy()
await db.getRedisClient().quit()
}) })
describe('klicktipp', () => { describe('klicktipp', () => {

View File

@ -33,7 +33,7 @@ export const testEnvironment = async (testLogger = getLogger('apollo')) => {
const testClient = createTestClient(server.apollo) const testClient = createTestClient(server.apollo)
const mutate = testClient.mutate const mutate = testClient.mutate
const query = testClient.query const query = testClient.query
return { mutate, query, con } return { mutate, query, con, db: server.db }
} }
export const resetEntity = async (entity: any) => { export const resetEntity = async (entity: any) => {

755
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@
"log4js": "^6.9.1", "log4js": "^6.9.1",
"nodemailer": "^6.6.5", "nodemailer": "^6.6.5",
"pug": "^3.0.2", "pug": "^3.0.2",
"redis-semaphore": "^5.6.2",
"shared": "*", "shared": "*",
"sodium-native": "^3.4.1", "sodium-native": "^3.4.1",
"zod": "^3.25.61" "zod": "^3.25.61"

View File

@ -41,7 +41,7 @@ const spySetLocale = jest.spyOn(i18n, 'setLocale')
const spyTranslate = jest.spyOn(i18n, '__') const spyTranslate = jest.spyOn(i18n, '__')
describe('sendEmailTranslated', () => { describe('sendEmailTranslated', () => {
let result: Record<string, unknown> | boolean | Error | null let result: Record<string, unknown> | boolean | null | Error
describe('config email is false', () => { describe('config email is false', () => {
beforeEach(async () => { beforeEach(async () => {

View File

@ -1,5 +1,6 @@
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { CONFIG } from '../config' import { CONFIG } from '../config'
import { mock, jest, describe, it, expect, beforeAll, afterEach } from 'bun:test'
import * as sendEmailTranslatedApi from './sendEmailTranslated' import * as sendEmailTranslatedApi from './sendEmailTranslated'
import { import {
@ -25,7 +26,7 @@ CONFIG.EMAIL_SMTP_HOST = testMailServerHost
CONFIG.EMAIL_SMTP_PORT = testMailServerPort CONFIG.EMAIL_SMTP_PORT = testMailServerPort
CONFIG.EMAIL_TLS = testMailTLS CONFIG.EMAIL_TLS = testMailTLS
jest.mock('nodemailer', () => { mock.module('nodemailer', () => {
return { return {
__esModule: true, __esModule: true,
createTransport: jest.fn(() => { createTransport: jest.fn(() => {

View File

@ -14,8 +14,10 @@ import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
import { PendingTransactionState } from 'shared' import { PendingTransactionState } from 'shared'
// import { LogError } from '@/server/LogError' // import { LogError } from '@/server/LogError'
import { calculateSenderBalance } from '../../util/calculateSenderBalance' import { calculateSenderBalance } from '../../util/calculateSenderBalance'
import { TRANSACTIONS_LOCK, getLastTransaction } from 'database' // import { TRANSACTIONS_LOCK, getLastTransaction } from 'database'
import { getLastTransaction } from 'database'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { Mutex } from 'redis-semaphore'
const db = AppDatabase.getInstance() const db = AppDatabase.getInstance()
const logger = getLogger( const logger = getLogger(
@ -29,7 +31,10 @@ export async function settlePendingSenderTransaction(
): Promise<boolean> { ): Promise<boolean> {
// TODO: synchronisation with TRANSACTION_LOCK of federation-modul necessary!!! // TODO: synchronisation with TRANSACTION_LOCK of federation-modul necessary!!!
// acquire lock // acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire() // const releaseLock = await TRANSACTIONS_LOCK.acquire()
const mutex = new Mutex(db.getRedisClient(), 'TRANSACTIONS_LOCK')
await mutex.acquire()
const queryRunner = db.getDataSource().createQueryRunner() const queryRunner = db.getDataSource().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') await queryRunner.startTransaction('REPEATABLE READ')
@ -121,7 +126,8 @@ export async function settlePendingSenderTransaction(
throw new Error('X-Com: send Transaction was not successful') throw new Error('X-Com: send Transaction was not successful')
} finally { } finally {
await queryRunner.release() await queryRunner.release()
releaseLock() // releaseLock()
await mutex.release()
} }
/* /*
void sendTransactionReceivedEmail({ void sendTransactionReceivedEmail({

View File

@ -37,7 +37,6 @@
"@types/geojson": "^7946.0.13", "@types/geojson": "^7946.0.13",
"@types/mysql": "^2.15.27", "@types/mysql": "^2.15.27",
"@types/node": "^18.7.14", "@types/node": "^18.7.14",
"await-semaphore": "^0.1.3",
"random-bigint": "^0.0.1", "random-bigint": "^0.0.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^4.9.5" "typescript": "^4.9.5"
@ -49,6 +48,7 @@
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"esbuild": "^0.25.2", "esbuild": "^0.25.2",
"geojson": "^0.5.0", "geojson": "^0.5.0",
"ioredis": "^5.8.2",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"mysql2": "^3.15.3", "mysql2": "^3.15.3",

View File

@ -5,12 +5,14 @@ import { getLogger } from 'log4js'
import { latestDbVersion } from '.' import { latestDbVersion } from '.'
import { CONFIG } from './config' import { CONFIG } from './config'
import { LOG4JS_BASE_CATEGORY_NAME } from './config/const' import { LOG4JS_BASE_CATEGORY_NAME } from './config/const'
import Redis from 'ioredis'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.AppDatabase`) const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.AppDatabase`)
export class AppDatabase { export class AppDatabase {
private static instance: AppDatabase private static instance: AppDatabase
private dataSource: DBDataSource | undefined private dataSource: DBDataSource | undefined
private redisClient: Redis | undefined
/** /**
* The Singleton's constructor should always be private to prevent direct * The Singleton's constructor should always be private to prevent direct
@ -88,10 +90,24 @@ export class AppDatabase {
} }
// check for correct database version // check for correct database version
await this.checkDBVersion() await this.checkDBVersion()
this.redisClient = new Redis(CONFIG.REDIS_URL)
logger.info('Redis status=', this.redisClient.status)
} }
public async destroy(): Promise<void> { public async destroy(): Promise<void> {
await this.dataSource?.destroy() await this.dataSource?.destroy()
if (this.redisClient) {
await this.redisClient.quit()
this.redisClient = undefined
}
}
public getRedisClient(): Redis {
if (!this.redisClient) {
throw new Error('Redis client not initialized')
}
return this.redisClient
} }
// ###################################### // ######################################

View File

@ -24,5 +24,6 @@ const database = {
} }
const PRODUCTION = process.env.NODE_ENV === 'production' || false const PRODUCTION = process.env.NODE_ENV === 'production' || false
const nodeEnv = process.env.NODE_ENV || 'development' const nodeEnv = process.env.NODE_ENV || 'development'
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'
export const CONFIG = { ...database, NODE_ENV: nodeEnv, PRODUCTION, ...defaults } export const CONFIG = { ...database, NODE_ENV: nodeEnv, PRODUCTION, REDIS_URL, ...defaults }

View File

@ -4,7 +4,6 @@ export { latestDbVersion }
export * from './entity' export * from './entity'
export * from './logging' export * from './logging'
export * from './queries' export * from './queries'
export * from './seeds' export * from './seeds'
export * from './util'
export * from './enum' export * from './enum'
export { AppDatabase } from './AppDatabase' export { AppDatabase } from './AppDatabase'

View File

@ -1,4 +0,0 @@
import { Semaphore } from 'await-semaphore'
const CONCURRENT_TRANSACTIONS = 1
export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)

View File

@ -1,4 +0,0 @@
import { Semaphore } from 'await-semaphore'
const CONCURRENT_TRANSACTIONS = 1
export const TRANSACTION_LINK_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)

View File

@ -1,2 +0,0 @@
export * from './TRANSACTIONS_LOCK'
export * from './TRANSACTION_LINK_LOCK'

View File

@ -5,7 +5,7 @@
# make sure correct node version is installed # make sure correct node version is installed
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
if ! command -v nvm &> /dev/null if ! command -v nvm > /dev/null
then then
echo "'nvm' is missing, will be installed now!" echo "'nvm' is missing, will be installed now!"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
@ -20,7 +20,7 @@ install_nvm() {
nvm use || install_nvm nvm use || install_nvm
# unzip needed for bun install script # unzip needed for bun install script
if ! command -v unzip &> /dev/null if ! command -v unzip > /dev/null
then then
echo "'unzip' is missing, will be installed now!" echo "'unzip' is missing, will be installed now!"
sudo apt-get install -y unzip sudo apt-get install -y unzip
@ -34,7 +34,7 @@ if [ ! -f "$BUN_VERSION_FILE" ]; then
exit 1 exit 1
fi fi
BUN_VERSION="$(cat "$BUN_VERSION_FILE" | tr -d '[:space:]')" BUN_VERSION="$(cat "$BUN_VERSION_FILE" | tr -d '[:space:]')"
if ! command -v bun &> /dev/null if ! command -v bun > /dev/null
then then
echo "'bun' is missing, v$BUN_VERSION will be installed now!" echo "'bun' is missing, v$BUN_VERSION will be installed now!"
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}" curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
@ -49,22 +49,30 @@ else
fi fi
fi fi
# turbo https://turborepo.com/docs/getting-started # turbo https://turborepo.com/docs/getting-started
if ! command -v turbo &> /dev/null if ! command -v turbo > /dev/null
then then
echo "'turbo' is missing, will be installed now!" echo "'turbo' is missing, will be installed now!"
bun install --global turbo bun install --global turbo
fi fi
# rust and grass # rust and grass
if ! command -v cargo &> /dev/null if ! command -v cargo > /dev/null
then then
echo "'cargo' is missing, will be installed now!" echo "'cargo' is missing, will be installed now!"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
export CARGO_HOME="$HOME/.cargo" export CARGO_HOME="$HOME/.cargo"
export PATH="$CARGO_HOME/bin:$PATH" export PATH="$CARGO_HOME/bin:$PATH"
fi fi
if ! command -v grass &> /dev/null if ! command -v grass > /dev/null
then then
echo "'grass' is missing, will be installed now!" echo "'grass' is missing, will be installed now!"
cargo install grass cargo install grass
fi fi
# redis
if ! command -v redis-cli --version > /dev/null
then
echo "redis is missing, will be installed now!"
sudo apt update
sudo apt install redis -y
fi

View File

@ -45,6 +45,7 @@
"log4js": "^6.9.1", "log4js": "^6.9.1",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"require-addon": "1.1.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"ts-jest": "27.1.4", "ts-jest": "27.1.4",
"tsconfig-paths": "^4.1.1", "tsconfig-paths": "^4.1.1",

View File

@ -159,6 +159,14 @@ services:
- internal-net - internal-net
- external-net - external-net
#########################################################
## REDIS ################################################
#########################################################
redis:
networks:
- internal-net
- external-net
######################################################### #########################################################
## NGINX ################################################ ## NGINX ################################################
######################################################### #########################################################

View File

@ -87,6 +87,14 @@ services:
volumes: volumes:
- db_test_vol:/var/lib/mysql - db_test_vol:/var/lib/mysql
#########################################################
## REDIS ################################################
#########################################################
redis:
networks:
- internal-net
- external-net
######################################################### #########################################################
## PHPMYADMIN ########################################### ## PHPMYADMIN ###########################################
######################################################### #########################################################

View File

@ -76,6 +76,18 @@ services:
volumes: volumes:
- db_vol:/var/lib/mysql - db_vol:/var/lib/mysql
#########################################################
## REDIS ################################################
#########################################################
redis:
image: redis:6.2.6
networks:
- internal-net
ports:
- ${REDIS_PORT:-6379}:6379
volumes:
- ./logs/redis:/logs/redis
######################################################## ########################################################
# BACKEND ############################################## # BACKEND ##############################################
######################################################## ########################################################

View File

@ -66,6 +66,7 @@
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"redis-semaphore": "^5.6.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"shared": "*", "shared": "*",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",