mirror of
https://github.com/IT4Change/gradido.git
synced 2026-03-01 12:44:43 +00:00
Merge branch 'master' into dlt_export_existing_transactions_drizzleOrm
This commit is contained in:
commit
d6feabeae1
4
.github/workflows/test_backend.yml
vendored
4
.github/workflows/test_backend.yml
vendored
@ -51,8 +51,8 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: docker-compose mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
- name: docker-compose mariadb redis
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb redis
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
2
.github/workflows/test_database.yml
vendored
2
.github/workflows/test_database.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
||||
node-version: '18.20.7'
|
||||
|
||||
- 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
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
4
.github/workflows/test_dht_node.yml
vendored
4
.github/workflows/test_dht_node.yml
vendored
@ -48,8 +48,8 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: docker-compose mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
- name: docker-compose mariadb redis
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb redis
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
21
.github/workflows/test_e2e.yml
vendored
21
.github/workflows/test_e2e.yml
vendored
@ -20,8 +20,8 @@ jobs:
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach 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 redis
|
||||
|
||||
- name: Prepare test system
|
||||
run: |
|
||||
@ -63,10 +63,11 @@ jobs:
|
||||
sudo nginx -t
|
||||
sudo systemctl start nginx
|
||||
|
||||
- name: wait for nginx and mailserver to be ready
|
||||
- name: wait for nginx, mailserver and redis to be ready
|
||||
run: |
|
||||
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 6379; do echo waiting for redis; sleep 1; done;
|
||||
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
@ -125,8 +126,8 @@ jobs:
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach 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 redis
|
||||
|
||||
- name: Prepare test system
|
||||
run: |
|
||||
@ -169,10 +170,11 @@ jobs:
|
||||
sudo nginx -t
|
||||
sudo systemctl start nginx
|
||||
|
||||
- name: wait for nginx and mailserver to be ready
|
||||
- name: wait for nginx, mailserver and redis to be ready
|
||||
run: |
|
||||
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 6379; do echo waiting for redis; sleep 1; done;
|
||||
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
@ -210,8 +212,8 @@ jobs:
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach 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 redis
|
||||
|
||||
- name: Prepare test system
|
||||
run: |
|
||||
@ -250,10 +252,11 @@ jobs:
|
||||
sudo nginx -t
|
||||
sudo systemctl start nginx
|
||||
|
||||
- name: wait for nginx and mailserver to be ready
|
||||
- name: wait for nginx, mailserver and redis to be ready
|
||||
run: |
|
||||
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 6379; do echo waiting for redis; sleep 1; done;
|
||||
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
|
||||
4
.github/workflows/test_federation.yml
vendored
4
.github/workflows/test_federation.yml
vendored
@ -48,8 +48,8 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: docker-compose mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
- name: docker-compose mariadb redis
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb redis
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@ -4,8 +4,25 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v2.7.1](https://github.com/gradido/gradido/compare/v2.7.0...v2.7.1)
|
||||
#### [v2.7.3](https://github.com/gradido/gradido/compare/v2.7.2...v2.7.3)
|
||||
|
||||
- feat(admin): show user registered at in admin [`#3589`](https://github.com/gradido/gradido/pull/3589)
|
||||
- feat(backend): 3573 feature introduce distributed semaphore base on redis [`#3580`](https://github.com/gradido/gradido/pull/3580)
|
||||
- fix(workflow): make deployment install script more robust [`#3588`](https://github.com/gradido/gradido/pull/3588)
|
||||
- chore(release): v2.7.2 [`#3587`](https://github.com/gradido/gradido/pull/3587)
|
||||
|
||||
#### [v2.7.2](https://github.com/gradido/gradido/compare/v2.7.0...v2.7.2)
|
||||
|
||||
> 2 December 2025
|
||||
|
||||
- feat(frontend): success message on create contribution like on send [`#3583`](https://github.com/gradido/gradido/pull/3583)
|
||||
- refactor(backend): rewrite seeding in database [`#3586`](https://github.com/gradido/gradido/pull/3586)
|
||||
- fix(other): complete email tests and move localization complete into core [`#3585`](https://github.com/gradido/gradido/pull/3585)
|
||||
- fix(workflow): editor warnings [`#3584`](https://github.com/gradido/gradido/pull/3584)
|
||||
- fix(other): fix code which lead to biome linting errors [`#3582`](https://github.com/gradido/gradido/pull/3582)
|
||||
- refactor(backend): moved email to core [`#3579`](https://github.com/gradido/gradido/pull/3579)
|
||||
- refactor(database): stabilize entity loading across runtimes by introducing deferred relation resolution [`#3578`](https://github.com/gradido/gradido/pull/3578)
|
||||
- chore(release): v2.7.1 [`#3577`](https://github.com/gradido/gradido/pull/3577)
|
||||
- feat(frontend): new startpage images [`#3576`](https://github.com/gradido/gradido/pull/3576)
|
||||
- feat(frontend): update login subtitle [`#3574`](https://github.com/gradido/gradido/pull/3574)
|
||||
- feat(frontend): update copy symbol and change link order [`#3575`](https://github.com/gradido/gradido/pull/3575)
|
||||
|
||||
27
README.md
27
README.md
@ -20,6 +20,7 @@ Clone the Gradido repository to your local machine.
|
||||
```bash
|
||||
git clone https://github.com/gradido/gradido.git
|
||||
cd gradido
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
For local development, you can run Gradido with **Docker** or **natively**, depending on your preferences and system setup. If you don't have a native MariaDB or MySQL installation, Docker can be used to handle the database as well.
|
||||
@ -105,6 +106,22 @@ turbo start
|
||||
|
||||
[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
|
||||
|
||||
@ -211,6 +228,16 @@ In root folder calling `bun clear` will clear all turbo caches, node_modules and
|
||||
bun clear
|
||||
```
|
||||
|
||||
### git Submodule
|
||||
The new Module `inspector` was added as git submodule.
|
||||
So after
|
||||
- `git clone`
|
||||
- `git checkout`
|
||||
- `git pull`
|
||||
|
||||
you have to run `git submodule update --init` to get the correct submodule version.
|
||||
|
||||
[Read More](https://git-scm.com/book/en/v2/Git-Tools-Submodules)
|
||||
|
||||
## Services defined in this package
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administration Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Gradido Academy - https://www.gradido.net",
|
||||
"version": "2.7.1",
|
||||
"version": "2.7.3",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
<div v-html="data.value" />
|
||||
</template>
|
||||
|
||||
<template #cell(createdAt)="data">
|
||||
{{ $d(new Date(data.value), 'long') }}
|
||||
</template>
|
||||
|
||||
<template #cell(status)="row">
|
||||
<div class="d-flex gap-3 justify-content-end align-items-center">
|
||||
<div
|
||||
|
||||
@ -26,6 +26,7 @@ export const searchUsers = gql`
|
||||
hasElopage
|
||||
emailConfirmationSend
|
||||
deletedAt
|
||||
createdAt
|
||||
roles
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,6 +244,7 @@
|
||||
},
|
||||
"redeemed": "eingelöst",
|
||||
"registered": "Registriert",
|
||||
"registered_at": "Registriert am",
|
||||
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
|
||||
"reset": "Zurücksetzen",
|
||||
"save": "Speichern",
|
||||
|
||||
@ -244,6 +244,7 @@
|
||||
},
|
||||
"redeemed": "redeemed",
|
||||
"registered": "Registered",
|
||||
"registered_at": "Registered at",
|
||||
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
|
||||
"reset": "Reset",
|
||||
"save": "Save",
|
||||
|
||||
@ -125,6 +125,7 @@ const fields = computed(() => [
|
||||
return value.join(' | ')
|
||||
},
|
||||
},
|
||||
{ key: 'createdAt', label: t('registered_at') },
|
||||
// { key: 'show_details', label: t('details') },
|
||||
// { key: 'confirm_mail', label: t('confirmed') },
|
||||
// { key: 'has_elopage', label: 'elopage' },
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "2.7.1",
|
||||
"version": "2.7.3",
|
||||
"private": false,
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -84,6 +84,7 @@
|
||||
"openai": "^4.87.3",
|
||||
"prettier": "^3.5.3",
|
||||
"random-bigint": "^0.0.1",
|
||||
"redis-semaphore": "^5.6.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"shared": "*",
|
||||
|
||||
@ -7,7 +7,7 @@ import { DataSource, Not } from 'typeorm'
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
|
||||
import { AppDatabase } from 'database'
|
||||
import { validateCommunities } from './validateCommunities'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`)
|
||||
@ -16,21 +16,25 @@ const federationClientLogger = getLogger(
|
||||
)
|
||||
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger)
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('validate Communities', () => {
|
||||
|
||||
@ -1,27 +1,7 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { ContributionCycleType } from 'database'
|
||||
|
||||
// lowercase values are not implemented yet
|
||||
export enum ContributionCycleType {
|
||||
ONCE = 'ONCE',
|
||||
HOUR = 'hour',
|
||||
TWO_HOURS = 'two_hours',
|
||||
FOUR_HOURS = 'four_hours',
|
||||
EIGHT_HOURS = 'eight_hours',
|
||||
HALF_DAY = 'half_day',
|
||||
DAILY = 'DAILY',
|
||||
TWO_DAYS = 'two_days',
|
||||
THREE_DAYS = 'three_days',
|
||||
FOUR_DAYS = 'four_days',
|
||||
FIVE_DAYS = 'five_days',
|
||||
SIX_DAYS = 'six_days',
|
||||
WEEK = 'week',
|
||||
TWO_WEEKS = 'two_weeks',
|
||||
MONTH = 'month',
|
||||
TWO_MONTH = 'two_month',
|
||||
QUARTER = 'quarter',
|
||||
HALF_YEAR = 'half_year',
|
||||
YEAR = 'year',
|
||||
}
|
||||
export { ContributionCycleType }
|
||||
|
||||
registerEnumType(ContributionCycleType, {
|
||||
name: 'ContributionCycleType', // this one is mandatory
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { ContributionStatus } from 'database'
|
||||
|
||||
export enum ContributionStatus {
|
||||
PENDING = 'PENDING',
|
||||
DELETED = 'DELETED',
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
DENIED = 'DENIED',
|
||||
CONFIRMED = 'CONFIRMED',
|
||||
}
|
||||
export { ContributionStatus }
|
||||
|
||||
registerEnumType(ContributionStatus, {
|
||||
name: 'ContributionStatus',
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { ContributionType } from 'database'
|
||||
|
||||
export enum ContributionType {
|
||||
ADMIN = 'ADMIN',
|
||||
USER = 'USER',
|
||||
LINK = 'LINK',
|
||||
}
|
||||
export { ContributionType }
|
||||
|
||||
registerEnumType(ContributionType, {
|
||||
name: 'ContributionType',
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { PendingTransactionState } from 'shared'
|
||||
|
||||
export { PendingTransactionState }
|
||||
|
||||
registerEnumType(PendingTransactionState, {
|
||||
name: 'PendingTransactionState', // this one is mandatory
|
||||
description: 'State of the PendingTransaction', // this one is optional
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { RoleNames } from 'database'
|
||||
|
||||
export enum RoleNames {
|
||||
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||
USER = 'USER',
|
||||
MODERATOR = 'MODERATOR',
|
||||
MODERATOR_AI = 'MODERATOR_AI',
|
||||
ADMIN = 'ADMIN',
|
||||
DLT_CONNECTOR = 'DLT_CONNECTOR_ROLE',
|
||||
}
|
||||
export { RoleNames }
|
||||
|
||||
registerEnumType(RoleNames, {
|
||||
name: 'RoleNames', // this one is mandatory
|
||||
|
||||
@ -13,6 +13,7 @@ export class UserAdmin {
|
||||
this.emailChecked = user.emailContact?.emailChecked
|
||||
this.hasElopage = hasElopage
|
||||
this.deletedAt = user.deletedAt
|
||||
this.createdAt = user.createdAt
|
||||
this.emailConfirmationSend = emailConfirmationSend
|
||||
this.roles = user.userRoles?.map((userRole) => userRole.role) ?? []
|
||||
}
|
||||
@ -41,6 +42,9 @@ export class UserAdmin {
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
emailConfirmationSend: string | null
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import { createCommunity, createVerifiedFederatedCommunity } from 'database/src/
|
||||
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { CONFIG } from '@/config'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
@ -28,11 +29,12 @@ CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER = 1000
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
const peterLoginData = {
|
||||
@ -46,6 +48,7 @@ beforeAll(async () => {
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
// reset id auto increment
|
||||
await DbCommunity.clear()
|
||||
@ -54,6 +57,7 @@ beforeAll(async () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
// real valid ed25519 key pairs
|
||||
|
||||
@ -19,6 +19,7 @@ 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'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
@ -27,10 +28,12 @@ const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -38,6 +41,7 @@ beforeAll(async () => {
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
@ -46,6 +50,7 @@ beforeAll(async () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('Contribution Links', () => {
|
||||
|
||||
@ -21,6 +21,7 @@ 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'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionMessageResolver`)
|
||||
const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
@ -40,10 +41,12 @@ jest.mock('core', () => {
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
let result: any
|
||||
|
||||
@ -51,12 +54,14 @@ beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger)
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('ContributionMessageResolver', () => {
|
||||
|
||||
@ -52,6 +52,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { getFirstDayOfPreviousNMonth } from 'core'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { getLogger as originalGetLogger } from 'log4js'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('core', () => {
|
||||
const originalModule = jest.requireActual('core')
|
||||
@ -71,10 +72,12 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
let creation: Contribution | null
|
||||
let admin: User
|
||||
@ -90,12 +93,14 @@ beforeAll(async () => {
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('ContributionResolver', () => {
|
||||
@ -2139,14 +2144,6 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('stores the EMAIL_CONFIRMATION event in the database', async () => {
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.EMAIL_CONFIRMATION,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('confirm same contribution again', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import {
|
||||
AppDatabase,
|
||||
Contribution as DbContribution,
|
||||
Transaction as DbTransaction,
|
||||
User as DbUser,
|
||||
DltTransaction as DbDltTransaction,
|
||||
getLastTransaction,
|
||||
UserContact,
|
||||
} from 'database'
|
||||
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 { 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 {
|
||||
fullName,
|
||||
sendContributionChangedByModeratorEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionDeletedEmail,
|
||||
sendContributionDeniedEmail,
|
||||
TransactionTypeId
|
||||
} from 'core'
|
||||
import {
|
||||
EVENT_ADMIN_CONTRIBUTION_CONFIRM,
|
||||
EVENT_ADMIN_CONTRIBUTION_CREATE,
|
||||
@ -43,13 +25,32 @@ import {
|
||||
import { UpdateUnconfirmedContributionContext } from '@/interactions/updateUnconfirmedContribution/UpdateUnconfirmedContribution.context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
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 { contributionTransaction } from '@/apis/dltConnector'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
import { AppDatabase } from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { Mutex } from 'redis-semaphore'
|
||||
import {
|
||||
contributionFrontendLink,
|
||||
loadAllContributions,
|
||||
@ -58,8 +59,6 @@ import {
|
||||
import { getOpenCreations, getUserCreation, validateContribution } from './util/creations'
|
||||
import { extractGraphQLFields } from './util/extractGraphQLFields'
|
||||
import { findContributions } from './util/findContributions'
|
||||
import { getLastTransaction } from 'database'
|
||||
import { contributionTransaction } from '@/apis/dltConnector'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionResolver`)
|
||||
@ -436,7 +435,9 @@ export class ContributionResolver {
|
||||
const logger = createLogger()
|
||||
logger.addContext('contribution', id)
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
const mutex = new Mutex (db.getRedisClient(), 'TRANSACTIONS_LOCK')
|
||||
await mutex.acquire()
|
||||
|
||||
try {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
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)
|
||||
} finally {
|
||||
releaseLock()
|
||||
// releaseLock()
|
||||
await mutex.release()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -10,14 +10,17 @@ import { CONFIG } from '@/config'
|
||||
import { writeHomeCommunityEntry } from '@/seeds/community'
|
||||
import { createUser, forgotPassword, setPassword } from '@/seeds/graphql/mutations'
|
||||
import { queryOptIn } from '@/seeds/graphql/queries'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
CONFIG.EMAIL_CODE_VALID_TIME = 1440
|
||||
@ -29,12 +32,14 @@ beforeAll(async () => {
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('EmailOptinCodes', () => {
|
||||
|
||||
@ -9,6 +9,7 @@ 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'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
@ -17,17 +18,20 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.Klicktip
|
||||
let testEnv: any
|
||||
let mutate: any
|
||||
let con: any
|
||||
let db: AppDatabase
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger)
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('KlicktippResolver', () => {
|
||||
|
||||
@ -32,12 +32,11 @@ import { listTransactionLinksAdmin } from '@/seeds/graphql/queries'
|
||||
import { transactionLinks } from '@/seeds/transactionLink/index'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { TRANSACTIONS_LOCK } from 'database'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { transactionLinkCode } from './TransactionLinkResolver'
|
||||
import { CONFIG } from '@/config'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
|
||||
@ -46,16 +45,18 @@ jest.mock('@/password/EncryptorUtils')
|
||||
CONFIG.DLT_ACTIVE = false
|
||||
|
||||
// mock semaphore to allow use fake timers
|
||||
jest.mock('database/src/util/TRANSACTIONS_LOCK')
|
||||
TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
|
||||
// jest.mock('database/src/util/TRANSACTIONS_LOCK')
|
||||
// TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
let user: User
|
||||
@ -65,6 +66,7 @@ beforeAll(async () => {
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
@ -73,6 +75,7 @@ beforeAll(async () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('TransactionLinkResolver', () => {
|
||||
|
||||
@ -39,7 +39,7 @@ import { LogError } from '@/server/LogError'
|
||||
import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
|
||||
import { calculateBalance } from '@/util/validate'
|
||||
import { fullName } from 'core'
|
||||
import { TRANSACTION_LINK_LOCK, TRANSACTIONS_LOCK } from 'database'
|
||||
// import { TRANSACTION_LINK_LOCK, TRANSACTIONS_LOCK } from 'database'
|
||||
import {
|
||||
calculateDecay,
|
||||
compoundInterest,
|
||||
@ -68,6 +68,8 @@ import { transactionLinkList } from './util/transactionLinkList'
|
||||
import { SignedTransferPayloadType } from 'shared'
|
||||
import { contributionTransaction, deferredTransferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector'
|
||||
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}`)
|
||||
|
||||
@ -237,7 +239,9 @@ export class TransactionLinkResolver {
|
||||
const user = getUser(context)
|
||||
if (code.match(/^CL-/)) {
|
||||
// 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 {
|
||||
methodLogger.info('redeem contribution link...')
|
||||
const now = new Date()
|
||||
@ -392,11 +396,14 @@ export class TransactionLinkResolver {
|
||||
await queryRunner.release()
|
||||
}
|
||||
} finally {
|
||||
releaseLock()
|
||||
// releaseLock()
|
||||
await mutex.release()
|
||||
}
|
||||
return true
|
||||
} 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()
|
||||
try {
|
||||
const transactionLink = await DbTransactionLink.findOne({ where: { code } })
|
||||
@ -441,7 +448,8 @@ export class TransactionLinkResolver {
|
||||
transactionLink.amount,
|
||||
)
|
||||
} finally {
|
||||
releaseLinkLock()
|
||||
// releaseLinkLock()
|
||||
await mutex.release()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { CONFIG } from '@/config'
|
||||
import { CONFIG as CORE_CONFIG} from 'core'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
@ -49,6 +50,7 @@ let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -62,6 +64,7 @@ beforeAll(async () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy() // close()
|
||||
await testEnv.db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
let bobData: any
|
||||
|
||||
@ -33,7 +33,7 @@ import { Context, getUser } from '@/server/context'
|
||||
import { communityUser } from '@/util/communityUser'
|
||||
import { calculateBalance } from '@/util/validate'
|
||||
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 { getLastTransaction } from 'database'
|
||||
@ -44,6 +44,8 @@ import { getCommunityName, isHomeCommunity } from './util/communities'
|
||||
import { getTransactionList } from './util/getTransactionList'
|
||||
import { transactionLinkSummary } from './util/transactionLinkSummary'
|
||||
import { transferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector'
|
||||
import { Redis } from 'ioredis'
|
||||
import { Mutex } from 'redis-semaphore'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionResolver`)
|
||||
@ -57,7 +59,10 @@ export const executeTransaction = async (
|
||||
transactionLink?: dbTransactionLink | null,
|
||||
): Promise<boolean> => {
|
||||
// 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()
|
||||
let dltTransactionPromise: Promise<DbDltTransaction | null> = Promise.resolve(null)
|
||||
if (!transactionLink) {
|
||||
@ -210,7 +215,8 @@ export const executeTransaction = async (
|
||||
}
|
||||
logger.info(`finished executeTransaction successfully`)
|
||||
} finally {
|
||||
releaseLock()
|
||||
// releaseLock()
|
||||
await mutex.release()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { UserInputError } from 'apollo-server-express'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import {
|
||||
AppDatabase,
|
||||
Community as DbCommunity,
|
||||
Event as DbEvent,
|
||||
TransactionLink,
|
||||
@ -103,10 +104,12 @@ let user: User
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -114,6 +117,7 @@ beforeAll(async () => {
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
CONFIG.HUMHUB_ACTIVE = false
|
||||
CONFIG.DLT_ACTIVE = false
|
||||
await cleanDB()
|
||||
@ -122,6 +126,7 @@ beforeAll(async () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('UserResolver', () => {
|
||||
@ -2256,7 +2261,7 @@ describe('UserResolver', () => {
|
||||
relations: ['user'],
|
||||
})
|
||||
const activationLink = `${
|
||||
CONFIG.EMAIL_LINK_VERIFICATION
|
||||
CONFIG.EMAIL_LINK_SETPASSWORD
|
||||
}${userContact.emailVerificationCode.toString()}`
|
||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||
firstName: 'Bibi',
|
||||
|
||||
@ -976,7 +976,7 @@ export class UserResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<SearchUsersResult> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const userFields = ['id', 'firstName', 'lastName', 'emailId', 'emailContact', 'deletedAt']
|
||||
const userFields = ['id', 'firstName', 'lastName', 'emailId', 'emailContact', 'deletedAt', 'createdAt']
|
||||
const [users, count] = await findUsers(
|
||||
userFields,
|
||||
query,
|
||||
|
||||
@ -23,7 +23,9 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { CONFIG } from '@/config'
|
||||
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')
|
||||
|
||||
@ -36,8 +38,8 @@ let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
@ -48,41 +50,41 @@ beforeAll(async () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await testEnv.db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
type RunOrder = { [key: number]: { start: number, end: number } }
|
||||
async function fakeWork(runOrder: RunOrder, index: number) {
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
type WorkData = { start: number, end: number }
|
||||
async function fakeWork(workData: WorkData[], index: number) {
|
||||
// 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()
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 50))
|
||||
const endDate = new Date()
|
||||
runOrder[index] = { start: startDate.getTime(), end: endDate.getTime() }
|
||||
releaseLock()
|
||||
workData[index] = { start: startDate.getTime(), end: endDate.getTime() }
|
||||
// releaseLock()
|
||||
await mutex.release()
|
||||
}
|
||||
|
||||
describe('semaphore', () => {
|
||||
it("didn't should run in parallel", async () => {
|
||||
const runOrder: RunOrder = {}
|
||||
await Promise.all([
|
||||
fakeWork(runOrder, 1),
|
||||
fakeWork(runOrder, 2),
|
||||
fakeWork(runOrder, 3),
|
||||
fakeWork(runOrder, 4),
|
||||
fakeWork(runOrder, 5),
|
||||
])
|
||||
expect(runOrder[1].start).toBeLessThan(runOrder[1].end)
|
||||
expect(runOrder[1].start).toBeLessThan(runOrder[2].start)
|
||||
expect(runOrder[2].start).toBeLessThan(runOrder[2].end)
|
||||
expect(runOrder[2].start).toBeLessThan(runOrder[3].start)
|
||||
expect(runOrder[3].start).toBeLessThan(runOrder[3].end)
|
||||
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)
|
||||
const workData: WorkData[] = []
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
for(let i = 0; i < 20; i++) {
|
||||
promises.push(fakeWork(workData, i))
|
||||
}
|
||||
await Promise.all(promises)
|
||||
workData.sort((a, b) => a.start - b.start)
|
||||
workData.forEach((work, index) => {
|
||||
expect(work.start).toBeLessThan(work.end)
|
||||
if(index < workData.length - 1) {
|
||||
expect(work.start).toBeLessThan(workData[index + 1].start)
|
||||
expect(work.end).toBeLessThanOrEqual(workData[index + 1].start)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Contribution, User } from 'database'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
import { cleanDB, contributionDateFormatter, testEnvironment } from '@test/helpers'
|
||||
|
||||
@ -18,22 +19,26 @@ CONFIG.HUMHUB_ACTIVE = false
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
const setZeroHours = (date: Date): Date => {
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
export { ContributionLinkInterface } from 'database'
|
||||
|
||||
/*
|
||||
export interface ContributionLinkInterface {
|
||||
amount: number
|
||||
name: string
|
||||
@ -5,3 +8,4 @@ export interface ContributionLinkInterface {
|
||||
validFrom?: Date
|
||||
validTo?: Date
|
||||
}
|
||||
*/
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ContributionLinkInterface } from './ContributionLinkInterface'
|
||||
export { contributionLinks } from 'database'
|
||||
|
||||
/*
|
||||
export const contributionLinks: ContributionLinkInterface[] = [
|
||||
{
|
||||
name: 'Dokumenta 2017',
|
||||
@ -16,3 +17,4 @@ export const contributionLinks: ContributionLinkInterface[] = [
|
||||
validTo: new Date(2022, 8, 25),
|
||||
},
|
||||
]
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { CreationInterface } from 'database'
|
||||
/*
|
||||
export interface CreationInterface {
|
||||
email: string
|
||||
amount: number
|
||||
@ -7,3 +9,4 @@ export interface CreationInterface {
|
||||
// number of months to move the confirmed creation to the past
|
||||
moveCreationDate?: number
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,7 @@
|
||||
|
||||
export { creations } from 'database'
|
||||
|
||||
/*
|
||||
import { nMonthsBefore } from '@/seeds/factory/creation'
|
||||
|
||||
import { CreationInterface } from './CreationInterface'
|
||||
@ -153,3 +157,4 @@ export const creations: CreationInterface[] = [
|
||||
confirmed: true,
|
||||
},
|
||||
]
|
||||
*/
|
||||
@ -1,32 +1,15 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import {
|
||||
contributionLinkFactory as contributionLinkFactoryDb,
|
||||
ContributionLinkInterface
|
||||
} from 'database'
|
||||
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
|
||||
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
|
||||
import { createContributionLink, login } from '@/seeds/graphql/mutations'
|
||||
export { ContributionLinkInterface }
|
||||
|
||||
export const contributionLinkFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
export async function contributionLinkFactory (
|
||||
_client: any,
|
||||
contributionLink: ContributionLinkInterface,
|
||||
): Promise<ContributionLink> => {
|
||||
const { mutate } = client
|
||||
|
||||
// login as admin
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
const variables = {
|
||||
amount: contributionLink.amount,
|
||||
memo: contributionLink.memo,
|
||||
name: contributionLink.name,
|
||||
cycle: 'ONCE',
|
||||
maxPerCycle: 1,
|
||||
maxAmountPerMonth: 200,
|
||||
validFrom: contributionLink.validFrom ? contributionLink.validFrom.toISOString() : undefined,
|
||||
validTo: contributionLink.validTo ? contributionLink.validTo.toISOString() : undefined,
|
||||
}
|
||||
|
||||
const result = await mutate({ mutation: createContributionLink, variables })
|
||||
return result.data.createContributionLink
|
||||
): Promise<ContributionLink> {
|
||||
return new ContributionLink(await contributionLinkFactoryDb(contributionLink))
|
||||
}
|
||||
|
||||
@ -1,58 +1,15 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Contribution, Transaction } from 'database'
|
||||
import {
|
||||
Contribution,
|
||||
creationFactory as creationFactoryDb,
|
||||
CreationInterface,
|
||||
nMonthsBefore
|
||||
} from 'database'
|
||||
|
||||
import { findUserByEmail } from '@/graphql/resolver/UserResolver'
|
||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||
import { confirmContribution, createContribution, login } from '@/seeds/graphql/mutations'
|
||||
|
||||
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
return new Date(date.getFullYear(), date.getMonth() - months, 1).toISOString()
|
||||
}
|
||||
export { CreationInterface, nMonthsBefore }
|
||||
|
||||
export const creationFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
_client: any,
|
||||
creation: CreationInterface,
|
||||
): Promise<Contribution> => {
|
||||
const { mutate } = client
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: creation.email, password: 'Aa12345_' },
|
||||
})
|
||||
const {
|
||||
data: { createContribution: contribution },
|
||||
} = await mutate({ mutation: createContribution, variables: { ...creation } })
|
||||
|
||||
if (creation.confirmed) {
|
||||
const user = await findUserByEmail(creation.email) // userContact.user
|
||||
|
||||
await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
await mutate({ mutation: confirmContribution, variables: { id: contribution.id } })
|
||||
const confirmedContribution = await Contribution.findOneOrFail({
|
||||
where: { id: contribution.id },
|
||||
})
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
where: { userId: user.id, creationDate: new Date(creation.contributionDate) },
|
||||
order: { balanceDate: 'DESC' },
|
||||
})
|
||||
|
||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||
confirmedContribution.contributionDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
transaction.creationDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
transaction.balanceDate = new Date(
|
||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||
)
|
||||
await transaction.save()
|
||||
await confirmedContribution.save()
|
||||
}
|
||||
}
|
||||
return confirmedContribution
|
||||
} else {
|
||||
return contribution
|
||||
}
|
||||
return creationFactoryDb(creation)
|
||||
}
|
||||
|
||||
@ -1,46 +1,13 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { TransactionLink } from 'database'
|
||||
import {
|
||||
transactionLinkFactory as transactionLinkFactoryDb,
|
||||
TransactionLinkInterface
|
||||
} from 'database'
|
||||
|
||||
import { transactionLinkExpireDate } from '@/graphql/resolver/TransactionLinkResolver'
|
||||
import { createTransactionLink, login } from '@/seeds/graphql/mutations'
|
||||
import { TransactionLinkInterface } from '@/seeds/transactionLink/TransactionLinkInterface'
|
||||
export { TransactionLinkInterface }
|
||||
|
||||
export const transactionLinkFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
export async function transactionLinkFactory (
|
||||
_client: any,
|
||||
transactionLink: TransactionLinkInterface,
|
||||
): Promise<void> => {
|
||||
const { mutate } = client
|
||||
|
||||
// login
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: transactionLink.email, password: 'Aa12345_' },
|
||||
})
|
||||
|
||||
const variables = {
|
||||
amount: transactionLink.amount,
|
||||
memo: transactionLink.memo,
|
||||
}
|
||||
|
||||
// get the transaction links's id
|
||||
const {
|
||||
data: {
|
||||
createTransactionLink: { id },
|
||||
},
|
||||
} = await mutate({ mutation: createTransactionLink, variables })
|
||||
|
||||
if (transactionLink.createdAt || transactionLink.deletedAt) {
|
||||
const dbTransactionLink = await TransactionLink.findOneOrFail({ where: { id } })
|
||||
|
||||
if (transactionLink.createdAt) {
|
||||
dbTransactionLink.createdAt = transactionLink.createdAt
|
||||
dbTransactionLink.validUntil = transactionLinkExpireDate(transactionLink.createdAt)
|
||||
await dbTransactionLink.save()
|
||||
}
|
||||
|
||||
if (transactionLink.deletedAt) {
|
||||
dbTransactionLink.deletedAt = new Date(dbTransactionLink.createdAt.getTime() + 1000)
|
||||
await dbTransactionLink.save()
|
||||
}
|
||||
}
|
||||
): Promise<void> {
|
||||
await transactionLinkFactoryDb(transactionLink)
|
||||
}
|
||||
|
||||
@ -1,77 +1,35 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { User } from 'database'
|
||||
import { User, userFactory as userFactoryDb, userFactoryBulk as userFactoryBulkDb, Community } from 'database'
|
||||
|
||||
import { RoleNames } from '@enum/RoleNames'
|
||||
|
||||
import { setUserRole } from '@/graphql/resolver/util/modifyUserRole'
|
||||
import { writeHomeCommunityEntry } from '@/seeds/community'
|
||||
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
||||
import { UserInterface } from '@/seeds/users/UserInterface'
|
||||
import { encryptPassword } from '@/password/PasswordEncryptor'
|
||||
|
||||
export const userFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
_client: any,
|
||||
user: UserInterface,
|
||||
): Promise<User> => {
|
||||
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) {
|
||||
// console.log(JSON.stringify(response, null, 2))
|
||||
throw new Error('createUser mutation returned unexpected response')
|
||||
}
|
||||
const {
|
||||
data: {
|
||||
createUser: { id },
|
||||
},
|
||||
} = response
|
||||
// get user from database
|
||||
let dbUser = await User.findOneOrFail({ where: { id }, relations: ['emailContact', 'userRoles'] })
|
||||
|
||||
const emailContact = dbUser.emailContact
|
||||
const dbUser = await userFactoryDb(user, homeCom)
|
||||
|
||||
if (user.emailChecked) {
|
||||
await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { password: 'Aa12345_', code: emailContact.emailVerificationCode },
|
||||
})
|
||||
}
|
||||
|
||||
// get last changes of user from database
|
||||
dbUser = await User.findOneOrFail({ where: { id }, relations: { userRoles: true, emailContact: true } })
|
||||
|
||||
if (user.createdAt || user.deletedAt || user.role) {
|
||||
if (user.createdAt) {
|
||||
dbUser.createdAt = user.createdAt
|
||||
// make sure emailContact is also updated for e2e test, prevent failing when time between seeding and test run is < 1 minute
|
||||
dbUser.emailContact.createdAt = user.createdAt
|
||||
dbUser.emailContact.updatedAt = user.createdAt
|
||||
await dbUser.emailContact.save()
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
dbUser.deletedAt = user.deletedAt
|
||||
}
|
||||
const userRole = user.role as RoleNames
|
||||
if (userRole && (userRole === RoleNames.ADMIN || userRole === RoleNames.MODERATOR)) {
|
||||
await setUserRole(dbUser, user.role)
|
||||
}
|
||||
const passwortHash = await encryptPassword(dbUser, 'Aa12345_')
|
||||
dbUser.password = passwortHash
|
||||
await dbUser.save()
|
||||
}
|
||||
try {
|
||||
if (homeCom.communityUuid) {
|
||||
dbUser.communityUuid = homeCom.communityUuid
|
||||
await User.save(dbUser)
|
||||
}
|
||||
} catch (_err) {
|
||||
// no homeCommunity exists
|
||||
}
|
||||
|
||||
// get last changes of user from database
|
||||
dbUser = await User.findOneOrFail({
|
||||
where: { id },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact', 'userRoles'],
|
||||
})
|
||||
return dbUser
|
||||
}
|
||||
|
||||
export async function userFactoryBulk(users: UserInterface[], homeCommunity?: Community | null) {
|
||||
if (!homeCommunity) {
|
||||
homeCommunity = await writeHomeCommunityEntry()
|
||||
}
|
||||
const dbUsers = await userFactoryBulkDb(users, homeCommunity)
|
||||
for (const dbUser of dbUsers) {
|
||||
if (dbUser.emailContact.emailChecked) {
|
||||
const passwortHash = await encryptPassword(dbUser, 'Aa12345_')
|
||||
dbUser.password = passwortHash
|
||||
await dbUser.save()
|
||||
}
|
||||
}
|
||||
return dbUsers
|
||||
}
|
||||
@ -1,10 +1,13 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import { entities } from 'database'
|
||||
import { datatype, internet, name } from 'faker'
|
||||
import {
|
||||
AppDatabase,
|
||||
User,
|
||||
UserInterface,
|
||||
creationFactoryBulk,
|
||||
transactionLinkFactoryBulk
|
||||
} from 'database'
|
||||
import { internet, name } from 'faker'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { CONFIG as CORE_CONFIG } from 'core'
|
||||
import { createServer } from '@/server/createServer'
|
||||
|
||||
import { initLogging } from '@/server/logger'
|
||||
import { getLogger } from 'log4js'
|
||||
@ -12,95 +15,87 @@ import { writeHomeCommunityEntry } from './community'
|
||||
import { contributionLinks } from './contributionLink/index'
|
||||
import { creations } from './creation/index'
|
||||
import { contributionLinkFactory } from './factory/contributionLink'
|
||||
import { creationFactory } from './factory/creation'
|
||||
import { transactionLinkFactory } from './factory/transactionLink'
|
||||
import { userFactory } from './factory/user'
|
||||
import { userFactoryBulk } from './factory/user'
|
||||
import { transactionLinks } from './transactionLink/index'
|
||||
import { users } from './users/index'
|
||||
|
||||
CORE_CONFIG.EMAIL = false
|
||||
const RANDOM_USER_COUNT = 100
|
||||
const logger = getLogger('seed')
|
||||
|
||||
const context = {
|
||||
token: '',
|
||||
setHeaders: {
|
||||
push: (value: { key: string; value: string }): void => {
|
||||
context.token = value.value
|
||||
},
|
||||
|
||||
forEach: (): void => {
|
||||
// do nothing
|
||||
},
|
||||
},
|
||||
clientTimezoneOffset: 0,
|
||||
}
|
||||
|
||||
export const cleanDB = async () => {
|
||||
// this only works as long we do not have foreign key constraints
|
||||
for (const entity of entities) {
|
||||
if (entity.name !== 'Migration') {
|
||||
await resetEntity(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resetEntity = async (entity: any) => {
|
||||
const items = await entity.find({ withDeleted: true })
|
||||
if (items.length > 0) {
|
||||
const ids = items.map((e: any) => e.id)
|
||||
await entity.delete(ids)
|
||||
}
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
initLogging()
|
||||
const server = await createServer(getLogger('apollo'), context)
|
||||
const seedClient = createTestClient(server.apollo)
|
||||
const { con } = server
|
||||
await cleanDB()
|
||||
logger.info('##seed## clean database successful...')
|
||||
const db = AppDatabase.getInstance()
|
||||
await db.init()
|
||||
await clearDatabase(db)
|
||||
logger.info('clean database successful...')
|
||||
logger.info(`crypto worker enabled: ${CONFIG.USE_CRYPTO_WORKER}`)
|
||||
|
||||
// seed home community
|
||||
await writeHomeCommunityEntry()
|
||||
const homeCommunity = await writeHomeCommunityEntry()
|
||||
|
||||
// seed the standard users
|
||||
for (const user of users) {
|
||||
await userFactory(seedClient, user)
|
||||
// put into map for later direct access
|
||||
const userCreationIndexedByEmail = new Map<string, User>()
|
||||
const defaultUsers = await userFactoryBulk(users, homeCommunity)
|
||||
for (const dbUser of defaultUsers) {
|
||||
userCreationIndexedByEmail.set(dbUser.emailContact.email, dbUser)
|
||||
}
|
||||
logger.info('##seed## seeding all standard users successful...')
|
||||
logger.info('seeding all standard users successful...')
|
||||
|
||||
// seed 100 random users
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await userFactory(seedClient, {
|
||||
const randomUsers = new Array<UserInterface>(RANDOM_USER_COUNT)
|
||||
for (let i = 0; i < RANDOM_USER_COUNT; i++) {
|
||||
randomUsers[i] = {
|
||||
firstName: name.firstName(),
|
||||
lastName: name.lastName(),
|
||||
email: internet.email(),
|
||||
language: datatype.boolean() ? 'en' : 'de',
|
||||
})
|
||||
logger.info(`##seed## seed ${i}. random user`)
|
||||
language: Math.random() < 0.5 ? 'en' : 'de',
|
||||
}
|
||||
}
|
||||
logger.info('##seed## seeding all random users successful...')
|
||||
await userFactoryBulk(randomUsers, homeCommunity)
|
||||
logger.info('seeding all random users successful...')
|
||||
|
||||
// create GDD
|
||||
for (const creation of creations) {
|
||||
await creationFactory(seedClient, creation)
|
||||
}
|
||||
logger.info('##seed## seeding all creations successful...')
|
||||
const moderatorUser = userCreationIndexedByEmail.get('peter@lustig.de')!
|
||||
await creationFactoryBulk(creations, userCreationIndexedByEmail, moderatorUser)
|
||||
logger.info('seeding all creations successful...')
|
||||
|
||||
// create Transaction Links
|
||||
for (const transactionLink of transactionLinks) {
|
||||
await transactionLinkFactory(seedClient, transactionLink)
|
||||
}
|
||||
logger.info('##seed## seeding all transactionLinks successful...')
|
||||
const movedTransactionLinks = transactionLinks.map(transactionLink => {
|
||||
let createdAt = new Date(new Date().getTime() + 1000)
|
||||
if (transactionLink.createdAt) {
|
||||
createdAt = transactionLink.createdAt
|
||||
}
|
||||
return {
|
||||
...transactionLink,
|
||||
createdAt: createdAt,
|
||||
}
|
||||
})
|
||||
await transactionLinkFactoryBulk(movedTransactionLinks, userCreationIndexedByEmail)
|
||||
logger.info('seeding all transactionLinks successful...')
|
||||
|
||||
// create Contribution Links
|
||||
for (const contributionLink of contributionLinks) {
|
||||
await contributionLinkFactory(seedClient, contributionLink)
|
||||
await contributionLinkFactory(null, contributionLink)
|
||||
}
|
||||
logger.info('##seed## seeding all contributionLinks successful...')
|
||||
logger.info('seeding all contributionLinks successful...')
|
||||
|
||||
await con.destroy()
|
||||
await db.destroy()
|
||||
}
|
||||
|
||||
async function clearDatabase(db: AppDatabase) {
|
||||
await db.getDataSource().transaction(async trx => {
|
||||
await trx.query(`SET FOREIGN_KEY_CHECKS = 0`)
|
||||
await trx.query(`TRUNCATE TABLE contributions`)
|
||||
await trx.query(`TRUNCATE TABLE contribution_links`)
|
||||
await trx.query(`TRUNCATE TABLE users`)
|
||||
await trx.query(`TRUNCATE TABLE user_contacts`)
|
||||
await trx.query(`TRUNCATE TABLE user_roles`)
|
||||
await trx.query(`TRUNCATE TABLE transactions`)
|
||||
await trx.query(`TRUNCATE TABLE transaction_links`)
|
||||
await trx.query(`TRUNCATE TABLE communities`)
|
||||
await trx.query(`SET FOREIGN_KEY_CHECKS = 1`)
|
||||
})
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
export { TransactionLinkInterface } from 'database'
|
||||
/*
|
||||
export interface TransactionLinkInterface {
|
||||
email: string
|
||||
amount: number
|
||||
@ -8,3 +10,4 @@ export interface TransactionLinkInterface {
|
||||
// redeemedBy?: number
|
||||
deletedAt?: boolean
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { transactionLinks } from 'database'
|
||||
/*
|
||||
import { TransactionLinkInterface } from './TransactionLinkInterface'
|
||||
|
||||
export const transactionLinks: TransactionLinkInterface[] = [
|
||||
@ -53,3 +55,4 @@ bei Gradidio sei dabei!`,
|
||||
deletedAt: true,
|
||||
},
|
||||
]
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { UserInterface } from 'database'
|
||||
/*
|
||||
export interface UserInterface {
|
||||
alias?: string
|
||||
email?: string
|
||||
@ -11,3 +13,4 @@ export interface UserInterface {
|
||||
publisherId?: number
|
||||
role?: string
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { bibiBloxberg } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const bibiBloxberg: UserInterface = {
|
||||
@ -12,3 +14,4 @@ export const bibiBloxberg: UserInterface = {
|
||||
// move user createdAt before transaction link
|
||||
createdAt: new Date(2021, 9, 17),
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { bobBaumeister } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const bobBaumeister: UserInterface = {
|
||||
@ -9,3 +11,4 @@ export const bobBaumeister: UserInterface = {
|
||||
emailChecked: true,
|
||||
language: 'de',
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,6 @@
|
||||
|
||||
export { garrickOllivander } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const garrickOllivander: UserInterface = {
|
||||
@ -10,3 +13,4 @@ export const garrickOllivander: UserInterface = {
|
||||
emailChecked: false,
|
||||
language: 'en',
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { peterLustig } from 'database'
|
||||
/*
|
||||
import { RoleNames } from '@enum/RoleNames'
|
||||
|
||||
import { UserInterface } from './UserInterface'
|
||||
@ -12,3 +14,4 @@ export const peterLustig: UserInterface = {
|
||||
language: 'de',
|
||||
role: RoleNames.ADMIN,
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,6 @@
|
||||
|
||||
export { raeuberHotzenplotz } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const raeuberHotzenplotz: UserInterface = {
|
||||
@ -8,3 +11,4 @@ export const raeuberHotzenplotz: UserInterface = {
|
||||
emailChecked: true,
|
||||
language: 'de',
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { stephenHawking } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const stephenHawking: UserInterface = {
|
||||
@ -10,3 +12,4 @@ export const stephenHawking: UserInterface = {
|
||||
deletedAt: new Date('2018-03-14T09:17:52'),
|
||||
language: 'en',
|
||||
}
|
||||
*/
|
||||
@ -9,12 +9,10 @@ import { slowDown } from 'express-slow-down'
|
||||
import helmet from 'helmet'
|
||||
import { Logger, getLogger } from 'log4js'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
import { GRADIDO_REALM, 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 { plugins } from './plugins'
|
||||
import { jwks, openidConfiguration } from '@/openIDConnect'
|
||||
// TODO implement
|
||||
@ -24,6 +22,7 @@ interface ServerDef {
|
||||
apollo: ApolloServer
|
||||
app: Express
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
export const createServer = async (
|
||||
@ -73,9 +72,6 @@ export const createServer = async (
|
||||
app.use(json())
|
||||
// bodyparser urlencoded for elopage
|
||||
app.use(urlencoded({ extended: true }))
|
||||
|
||||
// i18n
|
||||
app.use(i18n.init)
|
||||
|
||||
// Elopage Webhook
|
||||
|
||||
@ -104,5 +100,5 @@ export const createServer = async (
|
||||
)
|
||||
logger.debug('createServer...successful')
|
||||
|
||||
return { apollo, app, con: db.getDataSource() }
|
||||
return { apollo, app, con: db.getDataSource(), db }
|
||||
}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import i18n from 'i18n'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.localization`)
|
||||
|
||||
i18n.configure({
|
||||
locales: ['en', 'de'],
|
||||
defaultLocale: 'en',
|
||||
retryInDefaultLocale: false,
|
||||
staticCatalog: {
|
||||
en: { general: { decimalSeparator: "." } },
|
||||
de: { general: { decimalSeparator: "," } },
|
||||
},
|
||||
// autoReload: true, // if this is activated the seeding hangs at the very end
|
||||
updateFiles: false,
|
||||
objectNotation: true,
|
||||
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
|
||||
__n: 'tn', // and req.__n can be called as req.tn
|
||||
},
|
||||
register: global,
|
||||
mustacheConfig: {
|
||||
tags: ['{', '}'],
|
||||
disable: false,
|
||||
},
|
||||
})
|
||||
|
||||
export { i18n }
|
||||
@ -1,6 +1,7 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Event as DbEvent } from 'database'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
|
||||
@ -19,22 +20,26 @@ jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await DbEvent.clear()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('klicktipp', () => {
|
||||
|
||||
@ -33,7 +33,7 @@ export const testEnvironment = async (testLogger = getLogger('apollo')) => {
|
||||
const testClient = createTestClient(server.apollo)
|
||||
const mutate = testClient.mutate
|
||||
const query = testClient.query
|
||||
return { mutate, query, con }
|
||||
return { mutate, query, con, db: server.db }
|
||||
}
|
||||
|
||||
export const resetEntity = async (entity: any) => {
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
},
|
||||
// "rootDirs": [], /* 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",
|
||||
"../@types",
|
||||
"node_modules/@types",
|
||||
"../node_modules/@types",
|
||||
],
|
||||
@ -87,7 +87,9 @@
|
||||
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
||||
},
|
||||
"include": [
|
||||
"../core/src/types"
|
||||
"../core/src/types",
|
||||
"src",
|
||||
"test"
|
||||
],
|
||||
"ts-node": {
|
||||
"swc": true
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "config-schema",
|
||||
"version": "2.7.1",
|
||||
"version": "2.7.3",
|
||||
"description": "Gradido Config for validate config",
|
||||
"main": "./build/index.js",
|
||||
"types": "./src/index.ts",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "core",
|
||||
"version": "2.7.1",
|
||||
"version": "2.7.3",
|
||||
"description": "Gradido Core Code, High-Level Shared Code, with dependencies on other modules",
|
||||
"main": "./build/index.js",
|
||||
"types": "./src/index.ts",
|
||||
@ -22,8 +22,8 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check --error-on-warnings .",
|
||||
"lint:fix": "biome check --error-on-warnings . --write",
|
||||
"locales": "scripts/sort.sh src/emails/locales",
|
||||
"locales:fix": "scripts/sort.sh src/emails/locales --fix",
|
||||
"locales": "scripts/sort.sh src/locales",
|
||||
"locales:fix": "scripts/sort.sh src/locales --fix",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -36,6 +36,7 @@
|
||||
"log4js": "^6.9.1",
|
||||
"nodemailer": "^6.6.5",
|
||||
"pug": "^3.0.2",
|
||||
"redis-semaphore": "^5.6.2",
|
||||
"shared": "*",
|
||||
"sodium-native": "^3.4.1",
|
||||
"zod": "^3.25.61"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createTransport } from 'nodemailer'
|
||||
import { CONFIG } from '../config'
|
||||
import { i18n } from './localization'
|
||||
import { i18n } from '../locales/localization'
|
||||
import { getLogger } from '../../../config-schema/test/testSetup.bun'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
|
||||
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||
@ -41,7 +41,7 @@ const spySetLocale = jest.spyOn(i18n, 'setLocale')
|
||||
const spyTranslate = jest.spyOn(i18n, '__')
|
||||
|
||||
describe('sendEmailTranslated', () => {
|
||||
let result: Record<string, unknown> | boolean | null
|
||||
let result: Record<string, unknown> | boolean | null | Error
|
||||
|
||||
describe('config email is false', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import path from 'path'
|
||||
import Email from 'email-templates'
|
||||
import { i18n } from './localization'
|
||||
import { i18n } from '../locales/localization'
|
||||
import { createTransport } from 'nodemailer'
|
||||
import { CONFIG } from '../config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
|
||||
@ -113,7 +113,6 @@ export const sendEmailTranslated = async ({
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
logger.error('Error sending notification email', error)
|
||||
return error
|
||||
})
|
||||
|
||||
return resultSend
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { CONFIG } from '../config'
|
||||
import { mock, jest, describe, it, expect, beforeAll, afterEach } from 'bun:test'
|
||||
|
||||
import * as sendEmailTranslatedApi from './sendEmailTranslated'
|
||||
import {
|
||||
@ -25,7 +26,7 @@ CONFIG.EMAIL_SMTP_HOST = testMailServerHost
|
||||
CONFIG.EMAIL_SMTP_PORT = testMailServerPort
|
||||
CONFIG.EMAIL_TLS = testMailTLS
|
||||
|
||||
jest.mock('nodemailer', () => {
|
||||
mock.module('nodemailer', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
createTransport: jest.fn(() => {
|
||||
@ -47,6 +48,10 @@ describe('sendEmailVariants', () => {
|
||||
const contributionFrontendLink =
|
||||
'https://gradido.net/contributions/own-contributions/1#contributionListItem-1'
|
||||
|
||||
afterEach(() => {
|
||||
sendEmailTranslatedSpy.mockClear()
|
||||
})
|
||||
|
||||
describe('sendAddedContributionMessageEmail', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendAddedContributionMessageEmail({
|
||||
@ -162,7 +167,7 @@ describe('sendEmailVariants', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
|
||||
describe('sendAccountMultiRegistrationEmail', () => {
|
||||
beforeAll(async () => {
|
||||
@ -181,20 +186,22 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'accountMultiRegistration',
|
||||
locals: {
|
||||
locals: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
language: 'en',
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
// bun testrunner bug, toMatchObject mess with 'result'
|
||||
const resultClone = JSON.parse(JSON.stringify(result))
|
||||
expect(resultClone).toMatchObject({
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -235,24 +242,26 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'contributionConfirmed',
|
||||
locals: {
|
||||
locals: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
contributionAmount: '23.54',
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
contributionFrontendLink,
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
// bun testrunner bug, toMatchObject mess with 'result'
|
||||
const resultClone = JSON.parse(JSON.stringify(result))
|
||||
expect(resultClone).toMatchObject({
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -292,24 +301,26 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'contributionChangedByModerator',
|
||||
locals: {
|
||||
locals: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
contributionMemoUpdated: 'This is a better contribution memo.',
|
||||
contributionFrontendLink,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
// bun testrunner bug, toMatchObject mess with 'result'
|
||||
const resultClone = JSON.parse(JSON.stringify(result))
|
||||
expect(resultClone).toMatchObject({
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -348,23 +359,25 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'contributionDenied',
|
||||
locals: {
|
||||
locals: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
contributionFrontendLink,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('result', () => {
|
||||
it('has expected result', () => {
|
||||
expect(result).toMatchObject({
|
||||
// bun testrunner bug, toMatchObject mess with 'result'
|
||||
const resultClone = JSON.parse(JSON.stringify(result))
|
||||
expect(resultClone).toMatchObject({
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -403,23 +416,25 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'contributionDeleted',
|
||||
locals: {
|
||||
locals: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
contributionFrontendLink,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
// bun testrunner bug, toMatchObject mess with 'result'
|
||||
const resultClone = JSON.parse(JSON.stringify(result))
|
||||
expect(resultClone).toMatchObject({
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -456,23 +471,25 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'resetPassword',
|
||||
locals: {
|
||||
locals: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
language: 'en',
|
||||
resetLink: 'http://localhost/reset-password/3762660021544901417',
|
||||
timeDurationObject: { hours: 23, minutes: 30 },
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
// bun testrunner bug, toMatchObject mess with 'result'
|
||||
const resultClone = JSON.parse(JSON.stringify(result))
|
||||
expect(resultClone).toMatchObject({
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -512,10 +529,10 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'transactionLinkRedeemed',
|
||||
locals: {
|
||||
locals: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
@ -523,14 +540,16 @@ describe('sendEmailVariants', () => {
|
||||
transactionAmount: '17.65',
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
// bun testrunner bug, toMatchObject mess with 'result'
|
||||
const resultClone = JSON.parse(JSON.stringify(result))
|
||||
expect(resultClone).toMatchObject({
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -570,10 +589,10 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'transactionReceived',
|
||||
locals: {
|
||||
locals: expect.objectContaining({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
language: 'en',
|
||||
memo: 'Du bist schon lustiger ;)',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
@ -581,14 +600,16 @@ describe('sendEmailVariants', () => {
|
||||
transactionAmount: '37.40',
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
// bun testrunner bug, toMatchObject mess with 'result'
|
||||
const resultClone = JSON.parse(JSON.stringify(result))
|
||||
expect(resultClone).toMatchObject({
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -605,5 +626,4 @@ describe('sendEmailVariants', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum TransactionTypeId {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
// This is a virtual property, never occurring on the database
|
||||
DECAY = 4,
|
||||
LINK_SUMMARY = 5,
|
||||
}
|
||||
import { TransactionTypeId } from 'database'
|
||||
export { TransactionTypeId }
|
||||
|
||||
registerEnumType(TransactionTypeId, {
|
||||
name: 'TransactionTypeId', // this one is mandatory
|
||||
|
||||
@ -14,8 +14,10 @@ import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
|
||||
import { PendingTransactionState } from 'shared'
|
||||
// import { LogError } from '@/server/LogError'
|
||||
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 { Mutex } from 'redis-semaphore'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
const logger = getLogger(
|
||||
@ -29,7 +31,10 @@ export async function settlePendingSenderTransaction(
|
||||
): Promise<boolean> {
|
||||
// TODO: synchronisation with TRANSACTION_LOCK of federation-modul necessary!!!
|
||||
// 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()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
@ -121,7 +126,8 @@ export async function settlePendingSenderTransaction(
|
||||
throw new Error('X-Com: send Transaction was not successful')
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
releaseLock()
|
||||
// releaseLock()
|
||||
await mutex.release()
|
||||
}
|
||||
/*
|
||||
void sendTransactionReceivedEmail({
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import en from './locales/en.json'
|
||||
import de from './locales/de.json'
|
||||
import en from './en.json'
|
||||
import de from './de.json'
|
||||
import { I18n } from 'i18n'
|
||||
import { getLogger } from 'log4js'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.emails.localization`)
|
||||
|
||||
function flatten(obj: any, prefix: string = ''): any {
|
||||
const result: any = {}
|
||||
@ -18,6 +22,9 @@ export const i18n = new I18n({
|
||||
locales: ['en', 'de'],
|
||||
defaultLocale: 'en',
|
||||
staticCatalog: { en: flatten(en), de: flatten(de) },
|
||||
logDebugFn: (msg) => logger.debug(msg),
|
||||
logWarnFn: (msg) => logger.info(msg),
|
||||
logErrorFn: (msg) => logger.error(msg),
|
||||
api: {
|
||||
__: 't', // now req.__ becomes req.t
|
||||
__n: 'tn', // and req.__n can be called as req.tn
|
||||
@ -1,7 +1,8 @@
|
||||
import { promisify } from 'util'
|
||||
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import i18n from 'i18n'
|
||||
import { i18n } from '../locales/localization'
|
||||
export { fullName } from 'shared'
|
||||
|
||||
export const objectValuesToArray = (obj: Record<string, string>): string[] =>
|
||||
Object.keys(obj).map((key) => obj[key])
|
||||
@ -14,11 +15,7 @@ export const decimalSeparatorByLanguage = (a: Decimal, language: string): string
|
||||
return result
|
||||
}
|
||||
|
||||
export const fullName = (firstName: string, lastName: string): string =>
|
||||
[firstName, lastName].filter(Boolean).join(' ')
|
||||
|
||||
// Function to reset an interface by chatGPT
|
||||
|
||||
export function resetInterface<T extends Record<string, any>>(obj: T): T {
|
||||
// Iterate over all properties of the object
|
||||
for (const key in obj) {
|
||||
|
||||
@ -49,8 +49,12 @@
|
||||
// "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. */
|
||||
/* List of folders to include type definitions from. */
|
||||
"typeRoots": ["./node_modules/@types", "../node_modules/@types"],
|
||||
// "types": ["bun-types"], /* Type declaration files to be included in compilation. */
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"../node_modules/@types",
|
||||
"../@types"
|
||||
],
|
||||
// "types": ["bun-types", "../@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. */
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "database",
|
||||
"version": "2.7.1",
|
||||
"version": "2.7.3",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "./build/index.js",
|
||||
"types": "./src/index.ts",
|
||||
@ -37,8 +37,7 @@
|
||||
"@types/geojson": "^7946.0.13",
|
||||
"@types/mysql": "^2.15.27",
|
||||
"@types/node": "^18.7.14",
|
||||
"await-semaphore": "^0.1.3",
|
||||
"crypto-random-bigint": "^2.1.1",
|
||||
"random-bigint": "^0.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
@ -49,6 +48,7 @@
|
||||
"dotenv": "^10.0.0",
|
||||
"esbuild": "^0.25.2",
|
||||
"geojson": "^0.5.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"log4js": "^6.9.1",
|
||||
"mysql": "^2.18.1",
|
||||
"mysql2": "^3.15.3",
|
||||
|
||||
@ -5,12 +5,14 @@ import { getLogger } from 'log4js'
|
||||
import { latestDbVersion } from '.'
|
||||
import { CONFIG } from './config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from './config/const'
|
||||
import Redis from 'ioredis'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.AppDatabase`)
|
||||
|
||||
export class AppDatabase {
|
||||
private static instance: AppDatabase
|
||||
private dataSource: DBDataSource | undefined
|
||||
private redisClient: Redis | undefined
|
||||
|
||||
/**
|
||||
* The Singleton's constructor should always be private to prevent direct
|
||||
@ -88,10 +90,24 @@ export class AppDatabase {
|
||||
}
|
||||
// check for correct database version
|
||||
await this.checkDBVersion()
|
||||
|
||||
this.redisClient = new Redis(CONFIG.REDIS_URL)
|
||||
logger.info('Redis status=', this.redisClient.status)
|
||||
}
|
||||
|
||||
public async destroy(): Promise<void> {
|
||||
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
|
||||
}
|
||||
|
||||
// ######################################
|
||||
|
||||
@ -24,5 +24,6 @@ const database = {
|
||||
}
|
||||
const PRODUCTION = process.env.NODE_ENV === 'production' || false
|
||||
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 }
|
||||
|
||||
22
database/src/enum/ContributionCycleType.ts
Normal file
22
database/src/enum/ContributionCycleType.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// lowercase values are not implemented yet
|
||||
export enum ContributionCycleType {
|
||||
ONCE = 'ONCE',
|
||||
HOUR = 'hour',
|
||||
TWO_HOURS = 'two_hours',
|
||||
FOUR_HOURS = 'four_hours',
|
||||
EIGHT_HOURS = 'eight_hours',
|
||||
HALF_DAY = 'half_day',
|
||||
DAILY = 'DAILY',
|
||||
TWO_DAYS = 'two_days',
|
||||
THREE_DAYS = 'three_days',
|
||||
FOUR_DAYS = 'four_days',
|
||||
FIVE_DAYS = 'five_days',
|
||||
SIX_DAYS = 'six_days',
|
||||
WEEK = 'week',
|
||||
TWO_WEEKS = 'two_weeks',
|
||||
MONTH = 'month',
|
||||
TWO_MONTH = 'two_month',
|
||||
QUARTER = 'quarter',
|
||||
HALF_YEAR = 'half_year',
|
||||
YEAR = 'year',
|
||||
}
|
||||
7
database/src/enum/ContributionStatus.ts
Normal file
7
database/src/enum/ContributionStatus.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum ContributionStatus {
|
||||
PENDING = 'PENDING',
|
||||
DELETED = 'DELETED',
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
DENIED = 'DENIED',
|
||||
CONFIRMED = 'CONFIRMED',
|
||||
}
|
||||
5
database/src/enum/ContributionType.ts
Normal file
5
database/src/enum/ContributionType.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum ContributionType {
|
||||
ADMIN = 'ADMIN',
|
||||
USER = 'USER',
|
||||
LINK = 'LINK',
|
||||
}
|
||||
8
database/src/enum/RoleNames.ts
Normal file
8
database/src/enum/RoleNames.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export enum RoleNames {
|
||||
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||
USER = 'USER',
|
||||
MODERATOR = 'MODERATOR',
|
||||
MODERATOR_AI = 'MODERATOR_AI',
|
||||
ADMIN = 'ADMIN',
|
||||
DLT_CONNECTOR = 'DLT_CONNECTOR_ROLE',
|
||||
}
|
||||
8
database/src/enum/TransactionTypeId.ts
Normal file
8
database/src/enum/TransactionTypeId.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export enum TransactionTypeId {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
// This is a virtual property, never occurring on the database
|
||||
DECAY = 4,
|
||||
LINK_SUMMARY = 5,
|
||||
}
|
||||
@ -1 +1,6 @@
|
||||
export * from './CommunityHandshakeStateType'
|
||||
export * from './CommunityHandshakeStateType'
|
||||
export * from './ContributionType'
|
||||
export * from './ContributionStatus'
|
||||
export * from './TransactionTypeId'
|
||||
export * from './ContributionCycleType'
|
||||
export * from './RoleNames'
|
||||
@ -4,6 +4,6 @@ export { latestDbVersion }
|
||||
export * from './entity'
|
||||
export * from './logging'
|
||||
export * from './queries'
|
||||
export * from './util'
|
||||
export * from './seeds'
|
||||
export * from './enum'
|
||||
export { AppDatabase } from './AppDatabase'
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
export interface ContributionLinkInterface {
|
||||
amount: number
|
||||
name: string
|
||||
memo: string
|
||||
validFrom?: Date
|
||||
validTo?: Date
|
||||
}
|
||||
19
database/src/seeds/contributionLink/index.ts
Normal file
19
database/src/seeds/contributionLink/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ContributionLinkInterface } from './ContributionLinkInterface'
|
||||
export type { ContributionLinkInterface }
|
||||
|
||||
export const contributionLinks: ContributionLinkInterface[] = [
|
||||
{
|
||||
name: 'Dokumenta 2017',
|
||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2017',
|
||||
amount: 200,
|
||||
validFrom: new Date(2017, 3, 8),
|
||||
validTo: new Date(2017, 6, 16),
|
||||
},
|
||||
{
|
||||
name: 'Dokumenta 2022',
|
||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
||||
amount: 200,
|
||||
validFrom: new Date(2022, 5, 18),
|
||||
validTo: new Date(2022, 8, 25),
|
||||
},
|
||||
]
|
||||
9
database/src/seeds/creation/CreationInterface.ts
Normal file
9
database/src/seeds/creation/CreationInterface.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface CreationInterface {
|
||||
email: string
|
||||
amount: number
|
||||
memo: string
|
||||
contributionDate: string
|
||||
confirmed?: boolean
|
||||
// number of months to move the confirmed creation to the past
|
||||
moveCreationDate?: number
|
||||
}
|
||||
156
database/src/seeds/creation/index.ts
Normal file
156
database/src/seeds/creation/index.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { nMonthsBefore } from '../factory/creation'
|
||||
|
||||
import { CreationInterface } from './CreationInterface'
|
||||
|
||||
export type { CreationInterface }
|
||||
|
||||
const bobsSendings = [
|
||||
{
|
||||
amount: 10,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
},
|
||||
{
|
||||
amount: 10,
|
||||
memo: 'für deine Hilfe, Betty',
|
||||
},
|
||||
{
|
||||
amount: 23.37,
|
||||
memo: 'für deine Hilfe, David',
|
||||
},
|
||||
{
|
||||
amount: 47,
|
||||
memo: 'für deine Hilfe, Frau Holle',
|
||||
},
|
||||
{
|
||||
amount: 1.02,
|
||||
memo: 'für deine Hilfe, Herr Müller',
|
||||
},
|
||||
{
|
||||
amount: 5.67,
|
||||
memo: 'für deine Hilfe, Maier',
|
||||
},
|
||||
{
|
||||
amount: 72.93,
|
||||
memo: 'für deine Hilfe, Elsbeth',
|
||||
},
|
||||
{
|
||||
amount: 5.6,
|
||||
memo: 'für deine Hilfe, Daniel',
|
||||
},
|
||||
{
|
||||
amount: 8.87,
|
||||
memo: 'für deine Hilfe, Yoda',
|
||||
},
|
||||
{
|
||||
amount: 7.56,
|
||||
memo: 'für deine Hilfe, Sabine',
|
||||
},
|
||||
{
|
||||
amount: 7.89,
|
||||
memo: 'für deine Hilfe, Karl',
|
||||
},
|
||||
{
|
||||
amount: 8.9,
|
||||
memo: 'für deine Hilfe, Darth Vader',
|
||||
},
|
||||
{
|
||||
amount: 56.79,
|
||||
memo: 'für deine Hilfe, Luci',
|
||||
},
|
||||
{
|
||||
amount: 3.45,
|
||||
memo: 'für deine Hilfe, Hanne',
|
||||
},
|
||||
{
|
||||
amount: 8.74,
|
||||
memo: 'für deine Hilfe, Luise',
|
||||
},
|
||||
{
|
||||
amount: 7.85,
|
||||
memo: 'für deine Hilfe, Annegred',
|
||||
},
|
||||
{
|
||||
amount: 32.7,
|
||||
memo: 'für deine Hilfe, Prinz von Zamunda',
|
||||
},
|
||||
{
|
||||
amount: 44.2,
|
||||
memo: 'für deine Hilfe, Charly Brown',
|
||||
},
|
||||
{
|
||||
amount: 38.17,
|
||||
memo: 'für deine Hilfe, Michael',
|
||||
},
|
||||
{
|
||||
amount: 5.72,
|
||||
memo: 'für deine Hilfe, Kaja',
|
||||
},
|
||||
{
|
||||
amount: 3.99,
|
||||
memo: 'für deine Hilfe, Maja',
|
||||
},
|
||||
{
|
||||
amount: 4.5,
|
||||
memo: 'für deine Hilfe, Martha',
|
||||
},
|
||||
{
|
||||
amount: 8.3,
|
||||
memo: 'für deine Hilfe, Ursula',
|
||||
},
|
||||
{
|
||||
amount: 2.9,
|
||||
memo: 'für deine Hilfe, Urs',
|
||||
},
|
||||
{
|
||||
amount: 4.6,
|
||||
memo: 'für deine Hilfe, Mecedes',
|
||||
},
|
||||
{
|
||||
amount: 74.1,
|
||||
memo: 'für deine Hilfe, Heidi',
|
||||
},
|
||||
{
|
||||
amount: 4.5,
|
||||
memo: 'für deine Hilfe, Peter',
|
||||
},
|
||||
{
|
||||
amount: 5.8,
|
||||
memo: 'für deine Hilfe, Fräulein Rottenmeier',
|
||||
},
|
||||
]
|
||||
const bobsTransactions: CreationInterface[] = []
|
||||
bobsSendings.forEach((sending) => {
|
||||
bobsTransactions.push({
|
||||
email: 'bob@baumeister.de',
|
||||
amount: sending.amount,
|
||||
memo: sending.memo,
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
})
|
||||
})
|
||||
|
||||
export const creations: CreationInterface[] = [
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 1000,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
moveCreationDate: 12,
|
||||
},
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 1000,
|
||||
memo: '#Hexen',
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
},
|
||||
...bobsTransactions,
|
||||
{
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
amount: 1000,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
contributionDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
},
|
||||
]
|
||||
31
database/src/seeds/factory/contributionLink.ts
Normal file
31
database/src/seeds/factory/contributionLink.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { ContributionLink } from '../../entity'
|
||||
|
||||
import { ContributionLinkInterface } from '../contributionLink/ContributionLinkInterface'
|
||||
import { transactionLinkCode } from './transactionLink'
|
||||
import { ContributionCycleType } from '../../enum'
|
||||
|
||||
export function contributionLinkFactory(contributionLink: ContributionLinkInterface): Promise<ContributionLink> {
|
||||
return createContributionLink(contributionLink)
|
||||
}
|
||||
|
||||
export function createContributionLink(contributionLinkData: ContributionLinkInterface): Promise<ContributionLink> {
|
||||
const contributionLink = new ContributionLink()
|
||||
contributionLink.amount = new Decimal(contributionLinkData.amount)
|
||||
contributionLink.name = contributionLinkData.name
|
||||
contributionLink.memo = contributionLinkData.memo
|
||||
contributionLink.createdAt = new Date()
|
||||
contributionLink.code = transactionLinkCode(new Date())
|
||||
contributionLink.cycle = ContributionCycleType.ONCE
|
||||
if (contributionLinkData.validFrom) {
|
||||
contributionLink.validFrom = contributionLinkData.validFrom
|
||||
}
|
||||
if (contributionLinkData.validTo) {
|
||||
contributionLink.validTo = contributionLinkData.validTo
|
||||
}
|
||||
contributionLink.maxAmountPerMonth = new Decimal(200)
|
||||
contributionLink.maxPerCycle = 1
|
||||
|
||||
return contributionLink.save()
|
||||
}
|
||||
|
||||
134
database/src/seeds/factory/creation.ts
Normal file
134
database/src/seeds/factory/creation.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { Contribution, Transaction, User } from '../../entity'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { CreationInterface } from '../creation/CreationInterface'
|
||||
import { ContributionType, ContributionStatus, TransactionTypeId } from '../../enum'
|
||||
import { findUserByIdentifier } from '../../queries'
|
||||
import { createTransaction } from './transaction'
|
||||
import { AppDatabase } from '../../AppDatabase'
|
||||
|
||||
export function nMonthsBefore(date: Date, months = 1): string {
|
||||
return new Date(date.getFullYear(), date.getMonth() - months, 1).toISOString()
|
||||
}
|
||||
|
||||
export async function creationFactory(
|
||||
creation: CreationInterface,
|
||||
user?: User | null,
|
||||
moderatorUser?: User | null,
|
||||
): Promise<Contribution> {
|
||||
if (!user) {
|
||||
user = await findUserByIdentifier(creation.email)
|
||||
}
|
||||
if (!user) {
|
||||
throw new Error(`User ${creation.email} not found`)
|
||||
}
|
||||
let contribution = await createContribution(creation, user)
|
||||
if (creation.confirmed) {
|
||||
if (!moderatorUser) {
|
||||
moderatorUser = await findUserByIdentifier('peter@lustig.de')
|
||||
}
|
||||
if (!moderatorUser) {
|
||||
throw new Error('Moderator user not found')
|
||||
}
|
||||
await confirmTransaction(creation, contribution, moderatorUser)
|
||||
}
|
||||
return contribution
|
||||
}
|
||||
|
||||
export async function creationFactoryBulk(
|
||||
creations: CreationInterface[],
|
||||
userCreationIndexedByEmail: Map<string, User>,
|
||||
moderatorUser: User,
|
||||
): Promise<Contribution[]> {
|
||||
const lastTransaction = await Transaction.findOne({ order: { id: 'DESC' }, select: ['id'], where: {} })
|
||||
let transactionId = lastTransaction ? lastTransaction.id + 1 : 1
|
||||
const dbContributions: Contribution[] = []
|
||||
const dbTransactions: Transaction[] = []
|
||||
|
||||
for (const creation of creations) {
|
||||
const user = userCreationIndexedByEmail.get(creation.email)
|
||||
if (!user) {
|
||||
throw new Error(`User ${creation.email} not found`)
|
||||
}
|
||||
let contribution = await createContribution(creation, user, false)
|
||||
if (creation.confirmed) {
|
||||
const { contribution: _, transaction } = await confirmTransaction(
|
||||
creation,
|
||||
contribution,
|
||||
moderatorUser,
|
||||
transactionId,
|
||||
false
|
||||
)
|
||||
dbTransactions.push(transaction)
|
||||
transactionId++
|
||||
}
|
||||
dbContributions.push(contribution)
|
||||
}
|
||||
const dataSource = AppDatabase.getInstance().getDataSource()
|
||||
await dataSource.transaction(async (transaction) => {
|
||||
await dataSource.getRepository(Contribution).insert(dbContributions)
|
||||
await dataSource.getRepository(Transaction).insert(dbTransactions)
|
||||
})
|
||||
return dbContributions
|
||||
}
|
||||
|
||||
export async function createContribution(creation: CreationInterface, user: User, store: boolean = true): Promise<Contribution> {
|
||||
const contribution = new Contribution()
|
||||
contribution.user = user
|
||||
contribution.userId = user.id
|
||||
contribution.amount = new Decimal(creation.amount)
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = getContributionDate(creation)
|
||||
contribution.memo = creation.memo
|
||||
contribution.contributionType = ContributionType.USER
|
||||
contribution.contributionStatus = ContributionStatus.PENDING
|
||||
|
||||
return store ? contribution.save() : contribution
|
||||
}
|
||||
|
||||
export async function confirmTransaction(
|
||||
creation: CreationInterface,
|
||||
contribution: Contribution,
|
||||
moderatorUser: User,
|
||||
transactionId?: number,
|
||||
store: boolean = true
|
||||
): Promise<{ contribution: Contribution, transaction: Transaction }> {
|
||||
const balanceDate = getBalanceDate(creation)
|
||||
const transaction = await createTransaction(
|
||||
contribution.amount,
|
||||
contribution.memo,
|
||||
contribution.user,
|
||||
moderatorUser,
|
||||
TransactionTypeId.CREATION,
|
||||
balanceDate,
|
||||
contribution.contributionDate,
|
||||
transactionId,
|
||||
store,
|
||||
)
|
||||
contribution.confirmedAt = balanceDate
|
||||
contribution.confirmedBy = moderatorUser.id
|
||||
contribution.transactionId = transaction.id
|
||||
contribution.transaction = transaction
|
||||
contribution.contributionStatus = ContributionStatus.CONFIRMED
|
||||
|
||||
if (store) {
|
||||
await contribution.save()
|
||||
await transaction.save()
|
||||
}
|
||||
|
||||
return { contribution, transaction }
|
||||
}
|
||||
|
||||
function getContributionDate(creation: CreationInterface): Date {
|
||||
if (creation.moveCreationDate) {
|
||||
return new Date(nMonthsBefore(new Date(creation.contributionDate), creation.moveCreationDate))
|
||||
}
|
||||
return new Date(creation.contributionDate)
|
||||
}
|
||||
|
||||
function getBalanceDate(creation: CreationInterface): Date {
|
||||
const now = new Date()
|
||||
if (creation.moveCreationDate) {
|
||||
return new Date(nMonthsBefore(now, creation.moveCreationDate))
|
||||
}
|
||||
return now
|
||||
}
|
||||
6
database/src/seeds/factory/index.ts
Normal file
6
database/src/seeds/factory/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './contributionLink'
|
||||
export * from './creation'
|
||||
export * from './pendingTransaction'
|
||||
export * from './transaction'
|
||||
export * from './transactionLink'
|
||||
export * from './user'
|
||||
59
database/src/seeds/factory/transaction.ts
Normal file
59
database/src/seeds/factory/transaction.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { User, Transaction } from '../../entity'
|
||||
import { TransactionTypeId } from '../../enum'
|
||||
import { fullName } from 'shared'
|
||||
import { getLastTransaction } from '../../queries'
|
||||
import { calculateDecay, Decay } from 'shared'
|
||||
|
||||
export async function createTransaction(
|
||||
amount: Decimal,
|
||||
memo: string,
|
||||
user: User,
|
||||
linkedUser: User,
|
||||
type: TransactionTypeId,
|
||||
balanceDate: Date,
|
||||
creationDate?: Date,
|
||||
id?: number,
|
||||
store: boolean = true,
|
||||
): Promise<Transaction> {
|
||||
|
||||
const lastTransaction = await getLastTransaction(user.id)
|
||||
// balance and decay calculation
|
||||
let newBalance = new Decimal(0)
|
||||
let decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
decay = calculateDecay(
|
||||
lastTransaction.balance,
|
||||
lastTransaction.balanceDate,
|
||||
balanceDate,
|
||||
)
|
||||
newBalance = decay.balance
|
||||
}
|
||||
newBalance = newBalance.add(amount.toString())
|
||||
|
||||
const transaction = new Transaction()
|
||||
if (id) {
|
||||
transaction.id = id
|
||||
}
|
||||
transaction.typeId = type
|
||||
transaction.memo = memo
|
||||
transaction.userId = user.id
|
||||
transaction.userGradidoID = user.gradidoID
|
||||
transaction.userName = fullName(user.firstName, user.lastName)
|
||||
transaction.userCommunityUuid = user.communityUuid
|
||||
transaction.linkedUserId = linkedUser.id
|
||||
transaction.linkedUserGradidoID = linkedUser.gradidoID
|
||||
transaction.linkedUserName = fullName(linkedUser.firstName, linkedUser.lastName)
|
||||
transaction.linkedUserCommunityUuid = linkedUser.communityUuid
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = amount
|
||||
if (creationDate) {
|
||||
transaction.creationDate = creationDate
|
||||
}
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = balanceDate
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
|
||||
return store ? transaction.save() : transaction
|
||||
}
|
||||
77
database/src/seeds/factory/transactionLink.ts
Normal file
77
database/src/seeds/factory/transactionLink.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { TransactionLinkInterface } from '../transactionLink/TransactionLinkInterface'
|
||||
import { TransactionLink, User } from '../../entity'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { findUserByIdentifier } from '../../queries'
|
||||
import { compoundInterest } from 'shared'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { AppDatabase } from '../../AppDatabase'
|
||||
|
||||
export async function transactionLinkFactory(
|
||||
transactionLinkData: TransactionLinkInterface,
|
||||
userId?: number,
|
||||
): Promise<TransactionLink> {
|
||||
if (!userId) {
|
||||
const user = await findUserByIdentifier(transactionLinkData.email)
|
||||
if (!user) {
|
||||
throw new Error(`User ${transactionLinkData.email} not found`)
|
||||
}
|
||||
userId = user.id
|
||||
}
|
||||
return createTransactionLink(transactionLinkData, userId)
|
||||
}
|
||||
|
||||
export async function transactionLinkFactoryBulk(
|
||||
transactionLinks: TransactionLinkInterface[],
|
||||
userCreationIndexedByEmail: Map<string, User>
|
||||
): Promise<TransactionLink[]> {
|
||||
const dbTransactionLinks: TransactionLink[] = []
|
||||
for (const transactionLink of transactionLinks) {
|
||||
const user = userCreationIndexedByEmail.get(transactionLink.email)
|
||||
if (!user) {
|
||||
throw new Error(`User ${transactionLink.email} not found`)
|
||||
}
|
||||
dbTransactionLinks.push(await createTransactionLink(transactionLink, user.id, false))
|
||||
}
|
||||
const dataSource = AppDatabase.getInstance().getDataSource()
|
||||
await dataSource.getRepository(TransactionLink).insert(dbTransactionLinks)
|
||||
return dbTransactionLinks
|
||||
}
|
||||
|
||||
export async function createTransactionLink(transactionLinkData: TransactionLinkInterface, userId: number, store: boolean = true): Promise<TransactionLink> {
|
||||
const holdAvailableAmount = compoundInterest(new Decimal(transactionLinkData.amount.toString()), CODE_VALID_DAYS_DURATION * 24 * 60 * 60)
|
||||
let createdAt = transactionLinkData.createdAt || new Date()
|
||||
const validUntil = transactionLinkExpireDate(createdAt)
|
||||
|
||||
const transactionLink = new TransactionLink()
|
||||
transactionLink.userId = userId
|
||||
transactionLink.amount = new Decimal(transactionLinkData.amount)
|
||||
transactionLink.memo = transactionLinkData.memo
|
||||
transactionLink.holdAvailableAmount = holdAvailableAmount
|
||||
transactionLink.code = transactionLinkCode(createdAt)
|
||||
transactionLink.createdAt = createdAt
|
||||
transactionLink.validUntil = validUntil
|
||||
|
||||
if (transactionLinkData.deletedAt) {
|
||||
transactionLink.deletedAt = new Date(createdAt.getTime() + 1000)
|
||||
}
|
||||
|
||||
return store ? transactionLink.save() : transactionLink
|
||||
}
|
||||
|
||||
////// Transaction Link BUSINESS LOGIC //////
|
||||
// TODO: move business logic to shared
|
||||
export const CODE_VALID_DAYS_DURATION = 14
|
||||
|
||||
export const transactionLinkExpireDate = (date: Date): Date => {
|
||||
const validUntil = new Date(date)
|
||||
return new Date(validUntil.setDate(date.getDate() + CODE_VALID_DAYS_DURATION))
|
||||
}
|
||||
|
||||
export const transactionLinkCode = (date: Date): string => {
|
||||
const time = date.getTime().toString(16)
|
||||
return (
|
||||
randomBytes(12)
|
||||
.toString('hex')
|
||||
.substring(0, 24 - time.length) + time
|
||||
)
|
||||
}
|
||||
@ -1,16 +1,77 @@
|
||||
import { UserInterface } from '../users/UserInterface'
|
||||
import { User, UserContact } from '../../entity'
|
||||
import { User, UserContact, UserRole } from '../../entity'
|
||||
import { v4 } from 'uuid'
|
||||
import { UserContactType, OptInType, PasswordEncryptionType } from 'shared'
|
||||
import { getHomeCommunity } from '../../queries/communities'
|
||||
import random from 'crypto-random-bigint'
|
||||
import random from 'random-bigint'
|
||||
import { Community } from '../../entity'
|
||||
import { AppDatabase } from '../..'
|
||||
import { RoleNames } from '../../enum/RoleNames'
|
||||
|
||||
export const userFactory = async (user: UserInterface): Promise<User> => {
|
||||
let dbUserContact = new UserContact()
|
||||
export async function userFactory(user: UserInterface, homeCommunity?: Community | null): Promise<User> {
|
||||
// TODO: improve with cascade
|
||||
let dbUser = await createUser(user, homeCommunity)
|
||||
let dbUserContact = await createUserContact(user, dbUser.id)
|
||||
dbUser.emailId = dbUserContact.id
|
||||
dbUser.emailContact = dbUserContact
|
||||
dbUser = await dbUser.save()
|
||||
|
||||
dbUserContact.email = user.email ?? ''
|
||||
dbUserContact.type = UserContactType.USER_CONTACT_EMAIL
|
||||
const userRole = user.role as RoleNames
|
||||
if (userRole && (userRole === RoleNames.ADMIN || userRole === RoleNames.MODERATOR)) {
|
||||
dbUser.userRoles = [await createUserRole(dbUser.id, userRole)]
|
||||
}
|
||||
|
||||
return dbUser
|
||||
}
|
||||
|
||||
// only use in non-parallel environment (seeding for example)
|
||||
export async function userFactoryBulk(users: UserInterface[], homeCommunity?: Community | null): Promise<User[]> {
|
||||
const dbUsers: User[] = []
|
||||
const dbUserContacts: UserContact[] = []
|
||||
const dbUserRoles: UserRole[] = []
|
||||
const lastUser = await User.findOne({ order: { id: 'DESC' }, select: ['id'], where: {} })
|
||||
const lastUserContact = await UserContact.findOne({ order: { id: 'DESC' }, select: ['id'], where: {} })
|
||||
let userId = lastUser ? lastUser.id + 1 : 1
|
||||
let emailId = lastUserContact ? lastUserContact.id + 1 : 1
|
||||
// console.log(`start with userId: ${userId} and emailId: ${emailId}`)
|
||||
for(const user of users) {
|
||||
const dbUser = await createUser(user, homeCommunity, false)
|
||||
dbUser.id = userId
|
||||
dbUser.emailId = emailId
|
||||
|
||||
const dbUserContact = await createUserContact(user, userId, false)
|
||||
dbUserContact.id = emailId
|
||||
dbUserContact.userId = userId
|
||||
dbUser.emailContact = dbUserContact
|
||||
|
||||
dbUsers.push(dbUser)
|
||||
dbUserContacts.push(dbUserContact)
|
||||
|
||||
const userRole = user.role as RoleNames
|
||||
if (userRole && (userRole === RoleNames.ADMIN || userRole === RoleNames.MODERATOR)) {
|
||||
dbUserRoles.push(await createUserRole(dbUser.id, userRole, false))
|
||||
}
|
||||
|
||||
userId++
|
||||
emailId++
|
||||
}
|
||||
const dataSource = AppDatabase.getInstance().getDataSource()
|
||||
await dataSource.transaction(async transaction => {
|
||||
// typeorm change my data what I don't want
|
||||
// because of manuel id assignment
|
||||
const dbUsersCopy = dbUsers.map(user => ({ ...user }))
|
||||
const dbUserContactsCopy = dbUserContacts.map(userContact => ({ ...userContact }))
|
||||
const dbUserRolesCopy = dbUserRoles.map(userRole => ({ ...userRole }))
|
||||
await Promise.all([
|
||||
transaction.getRepository(User).insert(dbUsersCopy),
|
||||
transaction.getRepository(UserContact).insert(dbUserContactsCopy),
|
||||
transaction.getRepository(UserRole).insert(dbUserRolesCopy)
|
||||
])
|
||||
})
|
||||
return dbUsers
|
||||
}
|
||||
|
||||
export async function createUser(user: UserInterface, homeCommunity?: Community | null, store: boolean = true): Promise<User> {
|
||||
let dbUser = new User()
|
||||
dbUser.firstName = user.firstName ?? ''
|
||||
dbUser.lastName = user.lastName ?? ''
|
||||
@ -21,25 +82,50 @@ export const userFactory = async (user: UserInterface): Promise<User> => {
|
||||
dbUser.createdAt = user.createdAt ?? new Date()
|
||||
dbUser.deletedAt = user.deletedAt ?? null
|
||||
dbUser.publisherId = user.publisherId ?? 0
|
||||
dbUser.humhubAllowed = true
|
||||
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.password =
|
||||
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
}
|
||||
const homeCommunity = await getHomeCommunity()
|
||||
if (!homeCommunity) {
|
||||
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()
|
||||
|
||||
return store ? dbUser.save() : dbUser
|
||||
}
|
||||
|
||||
export async function createUserContact(user: UserInterface, userId?: number, store: boolean = true): Promise<UserContact> {
|
||||
let dbUserContact = new UserContact()
|
||||
|
||||
dbUserContact.email = user.email ?? ''
|
||||
dbUserContact.type = UserContactType.USER_CONTACT_EMAIL
|
||||
|
||||
if (user.createdAt) {
|
||||
dbUserContact.createdAt = user.createdAt
|
||||
dbUserContact.updatedAt = user.createdAt
|
||||
}
|
||||
if (user.emailChecked) {
|
||||
dbUserContact.emailVerificationCode = random(64).toString()
|
||||
dbUserContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||
dbUserContact.emailChecked = true
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
dbUserContact.userId = userId
|
||||
}
|
||||
|
||||
return store ? dbUserContact.save() : dbUserContact
|
||||
}
|
||||
|
||||
export async function createUserRole(userId: number, role: RoleNames, store: boolean = true): Promise<UserRole> {
|
||||
let dbUserRole = new UserRole()
|
||||
dbUserRole.userId = userId
|
||||
dbUserRole.role = role
|
||||
return store ? dbUserRole.save() : dbUserRole
|
||||
}
|
||||
5
database/src/seeds/index.ts
Normal file
5
database/src/seeds/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './contributionLink'
|
||||
export * from './creation'
|
||||
export * from './factory'
|
||||
export * from './transactionLink'
|
||||
export * from './users'
|
||||
@ -0,0 +1,10 @@
|
||||
export interface TransactionLinkInterface {
|
||||
email: string
|
||||
amount: number
|
||||
memo: string
|
||||
createdAt?: Date
|
||||
// TODO: for testing
|
||||
// redeemedAt?: Date
|
||||
// redeemedBy?: number
|
||||
deletedAt?: boolean
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user