Merge branch 'master' into refactor-simple-validation-with-typed-graphql

This commit is contained in:
einhorn_b 2023-08-19 09:55:42 +02:00
commit afed1b90fb
51 changed files with 2045 additions and 107 deletions

View File

@ -4,8 +4,16 @@ 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).
#### [1.23.2](https://github.com/gradido/gradido/compare/1.23.1...1.23.2)
- feat(admin): contribution filtering by memo [`#3174`](https://github.com/gradido/gradido/pull/3174)
- feat(backend): after transaction creations trigger to send them to dlt-connector [`#3152`](https://github.com/gradido/gradido/pull/3152)
#### [1.23.1](https://github.com/gradido/gradido/compare/1.23.0...1.23.1)
> 10 August 2023
- chore(release): v1.23.1 [`#3173`](https://github.com/gradido/gradido/pull/3173)
- feat(dlt): add dlt-connector to release-script [`#3170`](https://github.com/gradido/gradido/pull/3170)
- fix(backend): update logfiles clearance-script [`#3168`](https://github.com/gradido/gradido/pull/3168)
- fix(backend): too much logoutput on production [`#3160`](https://github.com/gradido/gradido/pull/3160)

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "1.23.1",
"version": "1.23.2",
"license": "Apache-2.0",
"private": false,
"scripts": {

View File

@ -5,7 +5,7 @@
type="text"
class="test-input-criteria"
v-model="currentValue"
:placeholder="$t('user_search')"
:placeholder="placeholderText"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="currentValue = ''">
<b-input-group-text class="pointer">
@ -20,12 +20,18 @@ export default {
name: 'UserQuery',
props: {
value: { type: String, default: '' },
placeholder: { type: String, default: '' },
},
data() {
return {
currentValue: this.value,
}
},
computed: {
placeholderText() {
return this.placeholder || this.$t('user_search')
},
},
watch: {
currentValue() {
if (this.value !== this.currentValue) {

View File

@ -8,6 +8,7 @@ export const adminListContributions = gql`
$statusFilter: [ContributionStatus!]
$userId: Int
$query: String
$noHashtag: Boolean
) {
adminListContributions(
currentPage: $currentPage
@ -16,6 +17,7 @@ export const adminListContributions = gql`
statusFilter: $statusFilter
userId: $userId
query: $query
noHashtag: $noHashtag
) {
contributionCount
contributionList {

View File

@ -89,6 +89,7 @@
"submit": "Senden"
},
"GDD": "GDD",
"hashtag_symbol": "#",
"help": {
"help": "Hilfe",
"transactionlist": {
@ -124,6 +125,10 @@
"user_search": "Nutzersuche"
},
"not_open_creations": "Keine offenen Schöpfungen",
"no_filter": "Keine Filterung",
"no_filter_tooltip": "Es wird nicht nach Hashtags gefiltert",
"no_hashtag": "Ohne Hashtag",
"no_hashtag_tooltip": "Zeigt nur Schöpfungen ohne Hashtag im Kommentar an",
"open": "offen",
"open_creations": "Offene Schöpfungen",
"overlay": {
@ -216,6 +221,7 @@
"tabTitle": "Nutzer-Rolle"
},
"user_deleted": "Nutzer ist gelöscht.",
"user_memo_search": "Nutzer-Kommentar-Suche",
"user_recovered": "Nutzer ist wiederhergestellt.",
"user_search": "Nutzer-Suche"
}

View File

@ -89,6 +89,7 @@
"submit": "Send"
},
"GDD": "GDD",
"hashtag_symbol": "#",
"help": {
"help": "Help",
"transactionlist": {
@ -124,6 +125,10 @@
"user_search": "User search"
},
"not_open_creations": "No open creations",
"no_filter": "No Filter",
"no_filter_tooltip": "It is not filtered by hashtags",
"no_hashtag": "No Hashtag",
"no_hashtag_tooltip": "Displays only contributions without hashtag in comment",
"open": "open",
"open_creations": "Open creations",
"overlay": {
@ -216,6 +221,7 @@
"tabTitle": "User Role"
},
"user_deleted": "User is deleted.",
"user_memo_search": "User and Memo search",
"user_recovered": "User is recovered.",
"user_search": "User search"
}

View File

@ -339,6 +339,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -355,6 +356,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -372,6 +374,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -389,6 +392,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -406,6 +410,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -427,6 +432,7 @@ describe('CreationConfirm', () => {
it('calls the API again', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 2,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -443,6 +449,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter and current page = 1', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
@ -465,6 +472,7 @@ describe('CreationConfirm', () => {
it('calls the API with query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: 'query',
@ -480,6 +488,7 @@ describe('CreationConfirm', () => {
it('calls the API with empty query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',

View File

@ -1,7 +1,13 @@
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys -->
<template>
<div class="creation-confirm">
<user-query class="mb-4 mt-2" v-model="query" />
<user-query class="mb-2 mt-2" v-model="query" :placeholder="$t('user_memo_search')" />
<div class="mb-4">
<b-button class="noHashtag" variant="light" @click="swapNoHashtag" v-b-tooltip="tooltipText">
<span :style="hashtagColor">{{ $t('hashtag_symbol') }}</span>
{{ noHashtag ? $t('no_hashtag') : $t('no_filter') }}
</b-button>
</div>
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
@ -118,6 +124,7 @@ export default {
currentPage: 1,
pageSize: 25,
query: '',
noHashtag: null,
}
},
watch: {
@ -126,6 +133,10 @@ export default {
},
},
methods: {
swapNoHashtag() {
this.noHashtag = !!(this.noHashtag === null || this.noHashtag === false)
this.query()
},
deleteCreation() {
this.$apollo
.mutate({
@ -198,6 +209,12 @@ export default {
},
},
computed: {
hashtagColor() {
return this.noHashtag ? 'color: red' : 'color: black'
},
tooltipText() {
return this.noHashtag ? this.$t('no_hashtag_tooltip') : this.$t('no_filter_tooltip')
},
fields() {
return [
[
@ -414,6 +431,7 @@ export default {
pageSize: this.pageSize,
statusFilter: this.statusFilter,
query: this.query,
noHashtag: this.noHashtag,
}
},
fetchPolicy: 'no-cache',

View File

@ -21,6 +21,10 @@ KLICKTIPP_PASSWORD=secret321
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
# DltConnector
DLT_CONNECTOR=true
DLT_CONNECTOR_URL=http://localhost:6010
# Community
COMMUNITY_NAME=Gradido Entwicklung
COMMUNITY_URL=http://localhost/

View File

@ -22,6 +22,10 @@ KLICKTIPP_PASSWORD=$KLICKTIPP_PASSWORD
KLICKTIPP_APIKEY_DE=$KLICKTIPP_APIKEY_DE
KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN
# DltConnector
DLT_CONNECTOR=$DLT_CONNECTOR
DLT_CONNECTOR_URL=$DLT_CONNECTOR_URL
# Community
COMMUNITY_NAME=$COMMUNITY_NAME
COMMUNITY_URL=$COMMUNITY_URL

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.23.1",
"version": "1.23.2",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",

View File

@ -0,0 +1,174 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable security/detect-object-injection */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Connection } from '@dbTools/typeorm'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { Decimal } from 'decimal.js-light'
import { cleanDB, testEnvironment } from '@test/helpers'
import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { DltConnectorClient } from './DltConnectorClient'
let con: Connection
let testEnv: {
con: Connection
}
// Mock the GraphQLClient
jest.mock('graphql-request', () => {
const originalModule = jest.requireActual('graphql-request')
let testCursor = 0
return {
__esModule: true,
...originalModule,
GraphQLClient: jest.fn().mockImplementation((url: string) => {
if (url === 'invalid') {
throw new Error('invalid url')
}
return {
// why not using mockResolvedValueOnce or mockReturnValueOnce?
// I have tried, but it didn't work and return every time the first value
request: jest.fn().mockImplementation(() => {
testCursor++
if (testCursor === 4) {
return Promise.resolve(
// invalid, is 33 Bytes long as binary
{
transmitTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A',
},
},
)
} else if (testCursor === 5) {
throw Error('Connection error')
} else {
return Promise.resolve(
// valid, is 32 Bytes long as binary
{
transmitTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
},
},
)
}
}),
}
}),
}
})
describe('undefined DltConnectorClient', () => {
it('invalid url', () => {
CONFIG.DLT_CONNECTOR_URL = 'invalid'
CONFIG.DLT_CONNECTOR = true
const result = DltConnectorClient.getInstance()
expect(result).toBeUndefined()
CONFIG.DLT_CONNECTOR_URL = 'http://dlt-connector:6010'
})
it('DLT_CONNECTOR is false', () => {
CONFIG.DLT_CONNECTOR = false
const result = DltConnectorClient.getInstance()
expect(result).toBeUndefined()
CONFIG.DLT_CONNECTOR = true
})
})
/*
describe.skip('transmitTransaction, without db connection', () => {
const transaction = new DbTransaction()
transaction.typeId = 2 // Example transaction type ID
transaction.amount = new Decimal('10.00') // Example amount
transaction.balanceDate = new Date() // Example creation date
transaction.id = 1 // Example transaction ID
it('cannot query for transaction id', async () => {
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(false)
})
})
*/
describe('transmitTransaction', () => {
beforeAll(async () => {
testEnv = await testEnvironment(logger)
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
const transaction = new DbTransaction()
transaction.typeId = 2 // Example transaction type ID
transaction.amount = new Decimal('10.00') // Example amount
transaction.balanceDate = new Date() // Example creation date
transaction.id = 1 // Example transaction ID
// data needed to let save succeed
transaction.memo = "I'm a dummy memo"
transaction.userId = 1
transaction.userGradidoID = 'dummy gradido id'
/*
it.skip('cannot find transaction in db', async () => {
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(false)
})
*/
it('invalid transaction type', async () => {
const localTransaction = new DbTransaction()
localTransaction.typeId = 12
try {
await DltConnectorClient.getInstance()?.transmitTransaction(localTransaction)
} catch (e) {
expect(e).toMatchObject(
new LogError('invalid transaction type id: ' + localTransaction.typeId.toString()),
)
}
})
/*
it.skip('should transmit the transaction and update the dltTransactionId in the database', async () => {
await transaction.save()
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(true)
})
it.skip('invalid dltTransactionId (maximal 32 Bytes in Binary)', async () => {
await transaction.save()
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(false)
})
*/
})
/*
describe.skip('try transmitTransaction but graphql request failed', () => {
it('graphql request should throw', async () => {
const transaction = new DbTransaction()
transaction.typeId = 2 // Example transaction type ID
transaction.amount = new Decimal('10.00') // Example amount
transaction.balanceDate = new Date() // Example creation date
transaction.id = 1 // Example transaction ID
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(false)
})
})
*/

View File

@ -0,0 +1,104 @@
import { Transaction as DbTransaction } from '@entity/Transaction'
import { gql, GraphQLClient } from 'graphql-request'
import { CONFIG } from '@/config'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
const sendTransaction = gql`
mutation ($input: TransactionInput!) {
sendTransaction(data: $input) {
dltTransactionIdHex
}
}
`
// from ChatGPT
function getTransactionTypeString(id: TransactionTypeId): string {
const key = Object.keys(TransactionTypeId).find(
(key) => TransactionTypeId[key as keyof typeof TransactionTypeId] === id,
)
if (key === undefined) {
throw new LogError('invalid transaction type id: ' + id.toString())
}
return key
}
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
// and ../federation/client/FederationClientFactory.ts
/**
* A Singleton class defines the `getInstance` method that lets clients access
* the unique singleton instance.
*/
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class DltConnectorClient {
// eslint-disable-next-line no-use-before-define
private static instance: DltConnectorClient
client: GraphQLClient
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(): DltConnectorClient | undefined {
if (!CONFIG.DLT_CONNECTOR || !CONFIG.DLT_CONNECTOR_URL) {
logger.info(`dlt-connector are disabled via config...`)
return
}
if (!DltConnectorClient.instance) {
DltConnectorClient.instance = new DltConnectorClient()
}
if (!DltConnectorClient.instance.client) {
try {
DltConnectorClient.instance.client = new GraphQLClient(CONFIG.DLT_CONNECTOR_URL, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
} catch (e) {
logger.error("couldn't connect to dlt-connector: ", e)
return
}
}
return DltConnectorClient.instance
}
/**
* transmit transaction via dlt-connector to iota
* and update dltTransactionId of transaction in db with iota message id
*/
public async transmitTransaction(transaction?: DbTransaction | null): Promise<string> {
if (transaction) {
const typeString = getTransactionTypeString(transaction.typeId)
const secondsSinceEpoch = Math.round(transaction.balanceDate.getTime() / 1000)
const amountString = transaction.amount.toString()
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = await this.client.rawRequest(sendTransaction, {
input: {
type: typeString,
amount: amountString,
createdAt: secondsSinceEpoch,
},
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return data.sendTransaction.dltTransactionIdHex
} catch (e) {
throw new LogError('Error send sending transaction to dlt-connector: ', e)
}
} else {
throw new LogError('parameter transaction not set...')
}
}
}

View File

@ -12,14 +12,14 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0069-add_user_roles_table',
DB_VERSION: '0070-add_dlt_transactions_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v17.2023-07-03',
EXPECTED: 'v18.2023-07-10',
CURRENT: '',
},
}
@ -51,6 +51,11 @@ const klicktipp = {
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN',
}
const dltConnector = {
DLT_CONNECTOR: process.env.DLT_CONNECTOR === 'true' || false,
DLT_CONNECTOR_URL: process.env.DLT_CONNECTOR_URL ?? 'http://localhost:6010',
}
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL ?? 'http://localhost/',
@ -126,6 +131,7 @@ export const CONFIG = {
...server,
...database,
...klicktipp,
...dltConnector,
...community,
...email,
...loginServer,

View File

@ -1,9 +1,12 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { GraphQLClient } from 'graphql-request'
import { getPublicCommunityInfo } from '@/federation/client/1_0/query/getPublicCommunityInfo'
import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey'
import { backendLogger as logger } from '@/server/logger'
import { PublicCommunityInfo } from './model/PublicCommunityInfo'
// eslint-disable-next-line camelcase
export class FederationClient {
dbCom: DbFederatedCommunity
@ -46,4 +49,27 @@ export class FederationClient {
logger.warn('Federation: getPublicKey failed for endpoint', this.endpoint)
}
}
getPublicCommunityInfo = async (): Promise<PublicCommunityInfo | undefined> => {
logger.debug(`Federation: getPublicCommunityInfo with endpoint='${this.endpoint}'...`)
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = await this.client.rawRequest(getPublicCommunityInfo, {})
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!data?.getPublicCommunityInfo?.name) {
logger.warn(
'Federation: getPublicCommunityInfo without response data from endpoint',
this.endpoint,
)
return
}
logger.debug(`Federation: getPublicCommunityInfo successful from endpoint=${this.endpoint}`)
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
logger.debug(`publicCommunityInfo:`, data.getPublicCommunityInfo)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return data.getPublicCommunityInfo
} catch (err) {
logger.warn('Federation: getPublicCommunityInfo failed for endpoint', this.endpoint)
}
}
}

View File

@ -0,0 +1,6 @@
export interface PublicCommunityInfo {
name: string
description: string
creationDate: Date
publicKey: string
}

View File

@ -0,0 +1,12 @@
import { gql } from 'graphql-request'
export const getPublicCommunityInfo = gql`
query {
getPublicCommunityInfo {
name
description
creationDate
publicKey
}
}
`

View File

@ -59,6 +59,44 @@ describe('validate Communities', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`)
})
describe('with one Community of api 1_0 but missing pubKey response', () => {
beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return { data: {} } as Response<unknown>
})
const variables1 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
jest.clearAllMocks()
await validateCommunities()
})
it('logs one community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey missing response data ', () => {
expect(logger.warn).toBeCalledWith(
'Federation: getPublicKey without response data from endpoint',
'http//localhost:5001/api/1_0/',
)
})
})
describe('with one Community of api 1_0 and not matching pubKey', () => {
beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/require-await
@ -88,7 +126,37 @@ describe('validate Communities', () => {
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
/*
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicCommunityInfo: {
name: 'Test-Community',
description: 'Description of Test-Community',
createdAt: 'someDate',
publicKey: 'somePubKey',
},
},
} as Response<unknown>
})
const variables2 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
*/
jest.clearAllMocks()
await validateCommunities()
})
@ -155,10 +223,26 @@ describe('validate Communities', () => {
})
it('logs community pubKey verified', () => {
expect(logger.debug).toHaveBeenNthCalledWith(
6,
'Federation: verified community with',
'http//localhost:5001/api/',
5,
'Federation: getPublicKey successful from endpoint',
'http//localhost:5001/api/1_0/',
'11111111111111111111111111111111',
)
/*
await expect(DbCommunity.find()).resolves.toContainEqual(
expect.objectContaining({
foreign: false,
url: 'http://localhost/api',
publicKey: Buffer.from('11111111111111111111111111111111'),
privateKey: expect.any(Buffer),
communityUuid: expect.any(String),
authenticatedAt: expect.any(Date),
name: expect.any(String),
description: expect.any(String),
creationDate: expect.any(Date),
}),
)
*/
})
})
describe('with two Communities of api 1_0 and 1_1', () => {

View File

@ -1,10 +1,12 @@
/** eslint-disable @typescript-eslint/no-unsafe-call */
/** eslint-disable @typescript-eslint/no-unsafe-assignment */
/** eslint-disable @typescript-eslint/no-unsafe-call */
import { IsNull } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
// eslint-disable-next-line camelcase
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo'
import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
import { backendLogger as logger } from '@/server/logger'
@ -48,7 +50,14 @@ export async function validateCommunities(): Promise<void> {
const pubKey = await client.getPublicKey()
if (pubKey && pubKey === dbCom.publicKey.toString()) {
await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.debug('Federation: verified community with', dbCom.endPoint)
logger.info(`Federation: verified community with:`, dbCom.endPoint)
const pubComInfo = await client.getPublicCommunityInfo()
if (pubComInfo) {
await writeForeignCommunity(dbCom, pubComInfo)
logger.info(`Federation: write publicInfo of community: name=${pubComInfo.name}`)
} else {
logger.warn('Federation: missing result of getPublicCommunityInfo')
}
} else {
logger.warn(
'Federation: received not matching publicKey:',
@ -62,3 +71,28 @@ export async function validateCommunities(): Promise<void> {
}
}
}
async function writeForeignCommunity(
dbCom: DbFederatedCommunity,
pubInfo: PublicCommunityInfo,
): Promise<void> {
if (!dbCom || !pubInfo || !(dbCom.publicKey.toString() === pubInfo.publicKey)) {
logger.error(
`Error in writeForeignCommunity: missmatching parameters or publicKey. pubInfo:${JSON.stringify(
pubInfo,
)}`,
)
} else {
let com = await DbCommunity.findOneBy({ publicKey: dbCom.publicKey })
if (!com) {
com = DbCommunity.create()
}
com.creationDate = pubInfo.creationDate
com.description = pubInfo.description
com.foreign = true
com.name = pubInfo.name
com.publicKey = dbCom.publicKey
com.url = dbCom.endPoint
await DbCommunity.save(com)
}
}

View File

@ -6,15 +6,15 @@ import { Order } from '@enum/Order'
@ArgsType()
export class Paginated {
@Field(() => Int, { nullable: true })
@Field(() => Int, { defaultValue: 1 })
@IsPositive()
currentPage?: number
@Field(() => Int, { nullable: true })
@Field(() => Int, { defaultValue: 3 })
@IsPositive()
pageSize?: number
@Field(() => Order, { nullable: true })
@Field(() => Order, { defaultValue: Order.DESC })
@IsEnum(Order)
order?: Order
}

View File

@ -0,0 +1,18 @@
import { Field, ArgsType, Int } from 'type-graphql'
import { ContributionStatus } from '@enum/ContributionStatus'
@ArgsType()
export class SearchContributionsFilterArgs {
@Field(() => [ContributionStatus], { nullable: true, defaultValue: null })
statusFilter?: ContributionStatus[] | null
@Field(() => Int, { nullable: true })
userId?: number | null
@Field(() => String, { nullable: true, defaultValue: '' })
query?: string | null
@Field(() => Boolean, { nullable: true })
noHashtag?: boolean | null
}

View File

@ -6,12 +6,13 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Connection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { testEnvironment } from '@test/helpers'
import { cleanDB, testEnvironment } from '@test/helpers'
import { getCommunities } from '@/seeds/graphql/queries'
import { getCommunities, getCommunitySelections } from '@/seeds/graphql/queries'
// to do: We need a setup for the tests that closes the connection
let query: ApolloServerTestClient['query'], con: Connection
@ -29,6 +30,7 @@ beforeAll(async () => {
})
afterAll(async () => {
await cleanDB()
await con.close()
})
@ -55,6 +57,7 @@ describe('CommunityResolver', () => {
describe('only home-communities entries', () => {
beforeEach(async () => {
await cleanDB()
jest.clearAllMocks()
homeCom1 = DbFederatedCommunity.create()
@ -230,4 +233,147 @@ describe('CommunityResolver', () => {
})
})
})
describe('getCommunitySelections', () => {
let homeCom1: DbCommunity
let foreignCom1: DbCommunity
let foreignCom2: DbCommunity
describe('with empty list', () => {
beforeEach(async () => {
await cleanDB()
jest.clearAllMocks()
})
it('returns no community entry', async () => {
// const result: Community[] = await query({ query: getCommunities })
// expect(result.length).toEqual(0)
await expect(query({ query: getCommunitySelections })).resolves.toMatchObject({
data: {
getCommunitySelections: [],
},
})
})
})
describe('with one home-community entry', () => {
beforeEach(async () => {
await cleanDB()
jest.clearAllMocks()
homeCom1 = DbCommunity.create()
homeCom1.foreign = false
homeCom1.url = 'http://localhost/api'
homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom1.privateKey = Buffer.from('privateKey-HomeCommunity')
homeCom1.communityUuid = 'HomeCom-UUID'
homeCom1.authenticatedAt = new Date()
homeCom1.name = 'HomeCommunity-name'
homeCom1.description = 'HomeCommunity-description'
homeCom1.creationDate = new Date()
await DbCommunity.insert(homeCom1)
})
it('returns 1 home-community entry', async () => {
await expect(query({ query: getCommunitySelections })).resolves.toMatchObject({
data: {
getCommunitySelections: [
{
id: expect.any(Number),
foreign: homeCom1.foreign,
name: homeCom1.name,
description: homeCom1.description,
url: homeCom1.url,
creationDate: homeCom1.creationDate?.toISOString(),
uuid: homeCom1.communityUuid,
authenticatedAt: homeCom1.authenticatedAt?.toISOString(),
},
],
},
})
})
})
describe('with several community entries', () => {
beforeEach(async () => {
await cleanDB()
jest.clearAllMocks()
homeCom1 = DbCommunity.create()
homeCom1.foreign = false
homeCom1.url = 'http://localhost/api'
homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom1.privateKey = Buffer.from('privateKey-HomeCommunity')
homeCom1.communityUuid = 'HomeCom-UUID'
homeCom1.authenticatedAt = new Date()
homeCom1.name = 'HomeCommunity-name'
homeCom1.description = 'HomeCommunity-description'
homeCom1.creationDate = new Date()
await DbCommunity.insert(homeCom1)
foreignCom1 = DbCommunity.create()
foreignCom1.foreign = true
foreignCom1.url = 'http://stage-2.gradido.net/api'
foreignCom1.publicKey = Buffer.from('publicKey-stage-2_Community')
foreignCom1.privateKey = Buffer.from('privateKey-stage-2_Community')
foreignCom1.communityUuid = 'Stage2-Com-UUID'
foreignCom1.authenticatedAt = new Date()
foreignCom1.name = 'Stage-2_Community-name'
foreignCom1.description = 'Stage-2_Community-description'
foreignCom1.creationDate = new Date()
await DbCommunity.insert(foreignCom1)
foreignCom2 = DbCommunity.create()
foreignCom2.foreign = true
foreignCom2.url = 'http://stage-3.gradido.net/api'
foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community')
foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community')
foreignCom2.communityUuid = 'Stage3-Com-UUID'
foreignCom2.authenticatedAt = new Date()
foreignCom2.name = 'Stage-3_Community-name'
foreignCom2.description = 'Stage-3_Community-description'
foreignCom2.creationDate = new Date()
await DbCommunity.insert(foreignCom2)
})
it('returns 3 community entries', async () => {
await expect(query({ query: getCommunitySelections })).resolves.toMatchObject({
data: {
getCommunitySelections: [
{
id: expect.any(Number),
foreign: homeCom1.foreign,
name: homeCom1.name,
description: homeCom1.description,
url: homeCom1.url,
creationDate: homeCom1.creationDate?.toISOString(),
uuid: homeCom1.communityUuid,
authenticatedAt: homeCom1.authenticatedAt?.toISOString(),
},
{
id: expect.any(Number),
foreign: foreignCom1.foreign,
name: foreignCom1.name,
description: foreignCom1.description,
url: foreignCom1.url,
creationDate: foreignCom1.creationDate?.toISOString(),
uuid: foreignCom1.communityUuid,
authenticatedAt: foreignCom1.authenticatedAt?.toISOString(),
},
{
id: expect.any(Number),
foreign: foreignCom2.foreign,
name: foreignCom2.name,
description: foreignCom2.description,
url: foreignCom2.url,
creationDate: foreignCom2.creationDate?.toISOString(),
uuid: foreignCom2.communityUuid,
authenticatedAt: foreignCom2.authenticatedAt?.toISOString(),
},
],
},
})
})
})
})
})

View File

@ -11,10 +11,10 @@ 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 { ContributionMessageType } from '@enum/ContributionMessageType'
import { ContributionStatus } from '@enum/ContributionStatus'
import { ContributionType } from '@enum/ContributionType'
import { Order } from '@enum/Order'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
import { Contribution, ContributionListResult } from '@model/Contribution'
@ -53,6 +53,7 @@ import {
} from './util/creations'
import { findContributions } from './util/findContributions'
import { getLastTransaction } from './util/getLastTransaction'
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
@Resolver()
export class ContributionResolver {
@ -119,20 +120,16 @@ export class ContributionResolver {
async listContributions(
@Ctx() context: Context,
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
paginated: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
): Promise<ContributionListResult> {
const user = getUser(context)
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
withDeleted: true,
relations: { messages: true },
userId: user.id,
statusFilter,
const filter = new SearchContributionsFilterArgs()
filter.statusFilter = statusFilter
filter.userId = user.id
const [dbContributions, count] = await findContributions(paginated, filter, true, {
messages: true,
})
return new ContributionListResult(
@ -151,16 +148,14 @@ export class ContributionResolver {
@Query(() => ContributionListResult)
async listAllContributions(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
paginated: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
relations: { user: true },
statusFilter,
const filter = new SearchContributionsFilterArgs()
filter.statusFilter = statusFilter
const [dbContributions, count] = await findContributions(paginated, filter, false, {
user: true,
})
return new ContributionListResult(
@ -356,29 +351,14 @@ export class ContributionResolver {
@Authorized([RIGHTS.ADMIN_LIST_CONTRIBUTIONS])
@Query(() => ContributionListResult)
async adminListContributions(
@Args()
{ currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
@Arg('userId', () => Int, { nullable: true })
userId?: number | null,
@Arg('query', () => String, { nullable: true })
query?: string | null,
@Args() paginated: Paginated,
@Args() filter: SearchContributionsFilterArgs,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
withDeleted: true,
userId,
relations: {
user: {
emailContact: true,
},
messages: true,
const [dbContributions, count] = await findContributions(paginated, filter, true, {
user: {
emailContact: true,
},
statusFilter,
query,
messages: true,
})
return new ContributionListResult(
@ -518,6 +498,10 @@ export class ContributionResolver {
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
await queryRunner.commitTransaction()
// trigger to send transaction via dlt-connector
void sendTransactionsToDltConnector()
logger.info('creation commited successfuly.')
void sendContributionConfirmedEmail({
firstName: user.firstName,

View File

@ -41,6 +41,7 @@ import { calculateBalance } from '@/util/validate'
import { executeTransaction } from './TransactionResolver'
import { getUserCreation, validateContribution } from './util/creations'
import { getLastTransaction } from './util/getLastTransaction'
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
import { transactionLinkList } from './util/transactionLinkList'
// TODO: do not export, test it inside the resolver
@ -286,6 +287,7 @@ export class TransactionLinkResolver {
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
await queryRunner.commitTransaction()
await EVENT_CONTRIBUTION_LINK_REDEEM(
user,
transaction,
@ -302,6 +304,8 @@ export class TransactionLinkResolver {
} finally {
releaseLock()
}
// trigger to send transaction via dlt-connector
void sendTransactionsToDltConnector()
return true
} else {
const now = new Date()

View File

@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Connection } from '@dbTools/typeorm'
import { Connection, In } from '@dbTools/typeorm'
import { DltTransaction } from '@entity/DltTransaction'
import { Event as DbEvent } from '@entity/Event'
import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User'
@ -391,7 +392,6 @@ describe('send coins', () => {
memo: 'unrepeatable memo',
},
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.TRANSACTION_RECEIVE,
@ -402,6 +402,52 @@ describe('send coins', () => {
}),
)
})
describe('sendTransactionsToDltConnector', () => {
let transaction: Transaction[]
let dltTransactions: DltTransaction[]
beforeAll(async () => {
// Find the previous created transactions of sendCoin mutation
transaction = await Transaction.find({
where: { memo: 'unrepeatable memo' },
order: { balanceDate: 'ASC', id: 'ASC' },
})
// and read aslong as all async created dlt-transactions are finished
do {
dltTransactions = await DltTransaction.find({
where: { transactionId: In([transaction[0].id, transaction[1].id]) },
// relations: ['transaction'],
// order: { createdAt: 'ASC', id: 'ASC' },
})
} while (transaction.length > dltTransactions.length)
})
it('has wait till sendTransactionsToDltConnector created all dlt-transactions', () => {
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
expect(dltTransactions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
transactionId: transaction[0].id,
messageId: null,
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: transaction[1].id,
messageId: null,
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
]),
)
})
})
})
describe('send coins via gradido ID', () => {

View File

@ -36,6 +36,7 @@ import { BalanceResolver } from './BalanceResolver'
import { findUserByIdentifier } from './util/findUserByIdentifier'
import { getLastTransaction } from './util/getLastTransaction'
import { getTransactionList } from './util/getTransactionList'
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
import { transactionLinkSummary } from './util/transactionLinkSummary'
export const executeTransaction = async (
@ -141,6 +142,9 @@ export const executeTransaction = async (
transactionReceive,
transactionReceive.amount,
)
// trigger to send transaction via dlt-connector
void sendTransactionsToDltConnector()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('Transaction was not successful', e)

View File

@ -1,74 +1,68 @@
import { In, Like } from '@dbTools/typeorm'
import { In, Like, Not } from '@dbTools/typeorm'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionStatus } from '@enum/ContributionStatus'
import { Order } from '@enum/Order'
import { Paginated } from '@arg/Paginated'
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
interface Relations {
[key: string]: boolean | Relations
}
interface FindContributionsOptions {
order: Order
currentPage: number
pageSize: number
withDeleted?: boolean
relations?: Relations | undefined
userId?: number | null
statusFilter?: ContributionStatus[] | null
query?: string | null
}
export const findContributions = async (
options: FindContributionsOptions,
paginate: Paginated,
filter: SearchContributionsFilterArgs,
withDeleted = false,
relations: Relations | undefined = undefined,
): Promise<[DbContribution[], number]> => {
const { order, currentPage, pageSize, withDeleted, relations, userId, statusFilter, query } = {
withDeleted: false,
relations: undefined,
query: '',
...options,
}
const requiredWhere = {
...(statusFilter?.length && { contributionStatus: In(statusFilter) }),
...(userId && { userId }),
...(filter.statusFilter?.length && { contributionStatus: In(filter.statusFilter) }),
...(filter.userId && { userId: filter.userId }),
...(filter.noHashtag && { memo: Not(Like(`%#%`)) }),
}
const where =
query && relations?.user
let where =
filter.query && relations?.user
? [
{
...requiredWhere,
...requiredWhere, // And
user: {
firstName: Like(`%${query}%`),
firstName: Like(`%${filter.query}%`),
},
},
}, // Or
{
...requiredWhere,
user: {
lastName: Like(`%${query}%`),
lastName: Like(`%${filter.query}%`),
},
},
}, // Or
{
...requiredWhere,
...requiredWhere, // And
user: {
emailContact: {
email: Like(`%${query}%`),
email: Like(`%${filter.query}%`),
},
},
}, // Or
{
...requiredWhere, // And
memo: Like(`%${filter.query}%`),
},
]
: requiredWhere
if (!relations?.user && filter.query) {
where = [{ ...requiredWhere, memo: Like(`%${filter.query}%`) }]
}
return DbContribution.findAndCount({
relations,
where,
withDeleted,
order: {
createdAt: order,
id: order,
createdAt: paginate.order,
id: paginate.order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
skip: (paginate.currentPage - 1) * paginate.pageSize,
take: paginate.pageSize,
})
}

View File

@ -0,0 +1,778 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Connection } from '@dbTools/typeorm'
import { DltTransaction } from '@entity/DltTransaction'
import { Transaction } from '@entity/Transaction'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { Decimal } from 'decimal.js-light'
// import { GraphQLClient } from 'graphql-request'
// import { Response } from 'graphql-request/dist/types'
import { GraphQLClient } from 'graphql-request'
import { Response } from 'graphql-request/dist/types'
import { testEnvironment, cleanDB } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup'
import { CONFIG } from '@/config'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector'
/*
// Mock the GraphQLClient
jest.mock('graphql-request', () => {
const originalModule = jest.requireActual('graphql-request')
let testCursor = 0
return {
__esModule: true,
...originalModule,
GraphQLClient: jest.fn().mockImplementation((url: string) => {
if (url === 'invalid') {
throw new Error('invalid url')
}
return {
// why not using mockResolvedValueOnce or mockReturnValueOnce?
// I have tried, but it didn't work and return every time the first value
request: jest.fn().mockImplementation(() => {
testCursor++
if (testCursor === 4) {
return Promise.resolve(
// invalid, is 33 Bytes long as binary
{
transmitTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A',
},
},
)
} else if (testCursor === 5) {
throw Error('Connection error')
} else {
return Promise.resolve(
// valid, is 32 Bytes long as binary
{
transmitTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
},
},
)
}
}),
}
}),
}
})
let mutate: ApolloServerTestClient['mutate'],
query: ApolloServerTestClient['query'],
con: Connection
let testEnv: {
mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query']
con: Connection
}
*/
async function createTxCREATION1(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(1000)
tx.balance = new Decimal(100)
tx.balanceDate = new Date('01.01.2023 00:00:00')
tx.memo = 'txCREATION1'
tx.typeId = TransactionTypeId.CREATION
tx.userGradidoID = 'txCREATION1.userGradidoID'
tx.userId = 1
tx.userName = 'txCREATION 1'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('01.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('01.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxCREATION2(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(1000)
tx.balance = new Decimal(200)
tx.balanceDate = new Date('02.01.2023 00:00:00')
tx.memo = 'txCREATION2'
tx.typeId = TransactionTypeId.CREATION
tx.userGradidoID = 'txCREATION2.userGradidoID'
tx.userId = 2
tx.userName = 'txCREATION 2'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('02.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('02.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxCREATION3(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(1000)
tx.balance = new Decimal(300)
tx.balanceDate = new Date('03.01.2023 00:00:00')
tx.memo = 'txCREATION3'
tx.typeId = TransactionTypeId.CREATION
tx.userGradidoID = 'txCREATION3.userGradidoID'
tx.userId = 3
tx.userName = 'txCREATION 3'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('03.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('03.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxSend1ToReceive2(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(100)
tx.balance = new Decimal(1000)
tx.balanceDate = new Date('11.01.2023 00:00:00')
tx.memo = 'txSEND1 to txRECEIVE2'
tx.typeId = TransactionTypeId.SEND
tx.userGradidoID = 'txSEND1.userGradidoID'
tx.userId = 1
tx.userName = 'txSEND 1'
tx.linkedUserGradidoID = 'txRECEIVE2.linkedUserGradidoID'
tx.linkedUserId = 2
tx.linkedUserName = 'txRECEIVE 2'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('11.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a1'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('11.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxReceive2FromSend1(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(100)
tx.balance = new Decimal(1300)
tx.balanceDate = new Date('11.01.2023 00:00:00')
tx.memo = 'txSEND1 to txRECEIVE2'
tx.typeId = TransactionTypeId.RECEIVE
tx.userGradidoID = 'txRECEIVE2.linkedUserGradidoID'
tx.userId = 2
tx.userName = 'txRECEIVE 2'
tx.linkedUserGradidoID = 'txSEND1.userGradidoID'
tx.linkedUserId = 1
tx.linkedUserName = 'txSEND 1'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('11.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b2'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('11.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
/*
async function createTxSend2ToReceive3(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(200)
tx.balance = new Decimal(1100)
tx.balanceDate = new Date('23.01.2023 00:00:00')
tx.memo = 'txSEND2 to txRECEIVE3'
tx.typeId = TransactionTypeId.SEND
tx.userGradidoID = 'txSEND2.userGradidoID'
tx.userId = 2
tx.userName = 'txSEND 2'
tx.linkedUserGradidoID = 'txRECEIVE3.linkedUserGradidoID'
tx.linkedUserId = 3
tx.linkedUserName = 'txRECEIVE 3'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('23.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a2'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('23.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxReceive3FromSend2(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(200)
tx.balance = new Decimal(1500)
tx.balanceDate = new Date('23.01.2023 00:00:00')
tx.memo = 'txSEND2 to txRECEIVE3'
tx.typeId = TransactionTypeId.RECEIVE
tx.userGradidoID = 'txRECEIVE3.linkedUserGradidoID'
tx.userId = 3
tx.userName = 'txRECEIVE 3'
tx.linkedUserGradidoID = 'txSEND2.userGradidoID'
tx.linkedUserId = 2
tx.linkedUserName = 'txSEND 2'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('23.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b3'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('23.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxSend3ToReceive1(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(300)
tx.balance = new Decimal(1200)
tx.balanceDate = new Date('31.01.2023 00:00:00')
tx.memo = 'txSEND3 to txRECEIVE1'
tx.typeId = TransactionTypeId.SEND
tx.userGradidoID = 'txSEND3.userGradidoID'
tx.userId = 3
tx.userName = 'txSEND 3'
tx.linkedUserGradidoID = 'txRECEIVE1.linkedUserGradidoID'
tx.linkedUserId = 1
tx.linkedUserName = 'txRECEIVE 1'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('31.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a3'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('31.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxReceive1FromSend3(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(300)
tx.balance = new Decimal(1300)
tx.balanceDate = new Date('31.01.2023 00:00:00')
tx.memo = 'txSEND3 to txRECEIVE1'
tx.typeId = TransactionTypeId.RECEIVE
tx.userGradidoID = 'txRECEIVE1.linkedUserGradidoID'
tx.userId = 1
tx.userName = 'txRECEIVE 1'
tx.linkedUserGradidoID = 'txSEND3.userGradidoID'
tx.linkedUserId = 3
tx.linkedUserName = 'txSEND 3'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('31.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b1'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('31.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
*/
let con: Connection
let testEnv: {
mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query']
con: Connection
}
beforeAll(async () => {
testEnv = await testEnvironment(logger, localization)
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('create and send Transactions to DltConnector', () => {
let txCREATION1: Transaction
let txCREATION2: Transaction
let txCREATION3: Transaction
let txSEND1to2: Transaction
let txRECEIVE2From1: Transaction
// let txSEND2To3: Transaction
// let txRECEIVE3From2: Transaction
// let txSEND3To1: Transaction
// let txRECEIVE1From3: Transaction
beforeEach(() => {
jest.clearAllMocks()
})
afterEach(async () => {
await cleanDB()
})
describe('with 3 creations but inactive dlt-connector', () => {
it('found 3 dlt-transactions', async () => {
txCREATION1 = await createTxCREATION1(false)
txCREATION2 = await createTxCREATION2(false)
txCREATION3 = await createTxCREATION3(false)
CONFIG.DLT_CONNECTOR = false
await sendTransactionsToDltConnector()
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
// Find the previous created transactions of sendCoin mutation
const transactions = await Transaction.find({
// where: { memo: 'unrepeatable memo' },
order: { balanceDate: 'ASC', id: 'ASC' },
})
const dltTransactions = await DltTransaction.find({
// where: { transactionId: In([transaction[0].id, transaction[1].id]) },
// relations: ['transaction'],
order: { createdAt: 'ASC', id: 'ASC' },
})
expect(dltTransactions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[0].id,
messageId: null,
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[1].id,
messageId: null,
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[2].id,
messageId: null,
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
]),
)
expect(logger.info).nthCalledWith(3, 'sending to DltConnector currently not configured...')
})
})
describe('with 3 creations and active dlt-connector', () => {
it('found 3 dlt-transactions', async () => {
txCREATION1 = await createTxCREATION1(false)
txCREATION2 = await createTxCREATION2(false)
txCREATION3 = await createTxCREATION3(false)
CONFIG.DLT_CONNECTOR = true
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
sendTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
},
},
} as Response<unknown>
})
await sendTransactionsToDltConnector()
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
// Find the previous created transactions of sendCoin mutation
const transactions = await Transaction.find({
// where: { memo: 'unrepeatable memo' },
order: { balanceDate: 'ASC', id: 'ASC' },
})
const dltTransactions = await DltTransaction.find({
// where: { transactionId: In([transaction[0].id, transaction[1].id]) },
// relations: ['transaction'],
order: { createdAt: 'ASC', id: 'ASC' },
})
expect(dltTransactions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[0].id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[1].id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[2].id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
]),
)
})
})
describe('with 3 verified creations, 1 sendCoins and active dlt-connector', () => {
it('found 3 dlt-transactions', async () => {
txCREATION1 = await createTxCREATION1(true)
txCREATION2 = await createTxCREATION2(true)
txCREATION3 = await createTxCREATION3(true)
txSEND1to2 = await createTxSend1ToReceive2(false)
txRECEIVE2From1 = await createTxReceive2FromSend1(false)
/*
txSEND2To3 = await createTxSend2ToReceive3()
txRECEIVE3From2 = await createTxReceive3FromSend2()
txSEND3To1 = await createTxSend3ToReceive1()
txRECEIVE1From3 = await createTxReceive1FromSend3()
*/
CONFIG.DLT_CONNECTOR = true
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
sendTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
},
},
} as Response<unknown>
})
await sendTransactionsToDltConnector()
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
// Find the previous created transactions of sendCoin mutation
/*
const transactions = await Transaction.find({
// where: { memo: 'unrepeatable memo' },
order: { balanceDate: 'ASC', id: 'ASC' },
})
*/
const dltTransactions = await DltTransaction.find({
// where: { transactionId: In([transaction[0].id, transaction[1].id]) },
// relations: ['transaction'],
order: { createdAt: 'ASC', id: 'ASC' },
})
expect(dltTransactions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
transactionId: txCREATION1.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1',
verified: true,
createdAt: new Date('01.01.2023 00:00:10'),
verifiedAt: new Date('01.01.2023 00:01:10'),
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: txCREATION2.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2',
verified: true,
createdAt: new Date('02.01.2023 00:00:10'),
verifiedAt: new Date('02.01.2023 00:01:10'),
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: txCREATION3.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3',
verified: true,
createdAt: new Date('03.01.2023 00:00:10'),
verifiedAt: new Date('03.01.2023 00:01:10'),
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: txSEND1to2.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: txRECEIVE2From1.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
]),
)
})
/*
describe('with one Community of api 1_0 and not matching pubKey', () => {
beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicKey: {
publicKey: 'somePubKey',
},
},
} as Response<unknown>
})
const variables1 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
jest.clearAllMocks()
// await validateCommunities()
})
it('logs one community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs not matching publicKeys', () => {
expect(logger.warn).toBeCalledWith(
'Federation: received not matching publicKey:',
'somePubKey',
expect.stringMatching('11111111111111111111111111111111'),
)
})
})
describe('with one Community of api 1_0 and matching pubKey', () => {
beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicKey: {
publicKey: '11111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables1 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks()
// await validateCommunities()
})
it('logs one community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs community pubKey verified', () => {
expect(logger.info).toHaveBeenNthCalledWith(
3,
'Federation: verified community with',
'http//localhost:5001/api/',
)
})
})
describe('with two Communities of api 1_0 and 1_1', () => {
beforeEach(async () => {
jest.clearAllMocks()
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicKey: {
publicKey: '11111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables2 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_1',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables2)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks()
// await validateCommunities()
})
it('logs two communities found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 2 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
)
})
})
describe('with three Communities of api 1_0, 1_1 and 2_0', () => {
let dbCom: DbFederatedCommunity
beforeEach(async () => {
const variables3 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '2_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables3)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
dbCom = await DbFederatedCommunity.findOneOrFail({
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
})
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks()
// await validateCommunities()
})
it('logs three community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 3 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
)
})
it('logs unsupported api for community with api 2_0 ', () => {
expect(logger.warn).toBeCalledWith(
'Federation: dbCom with unsupported apiVersion',
dbCom.endPoint,
'2_0',
)
})
})
*/
})
})

View File

@ -0,0 +1,80 @@
import { IsNull } from '@dbTools/typeorm'
import { DltTransaction } from '@entity/DltTransaction'
import { Transaction } from '@entity/Transaction'
import { DltConnectorClient } from '@/apis/DltConnectorClient'
import { backendLogger as logger } from '@/server/logger'
import { Monitor, MonitorNames } from '@/util/Monitor'
export async function sendTransactionsToDltConnector(): Promise<void> {
logger.info('sendTransactionsToDltConnector...')
// check if this logic is still occupied, no concurrecy allowed
if (!Monitor.isLocked(MonitorNames.SEND_DLT_TRANSACTIONS)) {
// mark this block for occuption to prevent concurrency
Monitor.lockIt(MonitorNames.SEND_DLT_TRANSACTIONS)
try {
await createDltTransactions()
const dltConnector = DltConnectorClient.getInstance()
if (dltConnector) {
logger.debug('with sending to DltConnector...')
const dltTransactions = await DltTransaction.find({
where: { messageId: IsNull() },
relations: ['transaction'],
order: { createdAt: 'ASC', id: 'ASC' },
})
for (const dltTx of dltTransactions) {
try {
const messageId = await dltConnector.transmitTransaction(dltTx.transaction)
const dltMessageId = Buffer.from(messageId, 'hex')
if (dltMessageId.length !== 32) {
logger.error(
'Error dlt message id is invalid: %s, should by 32 Bytes long in binary after converting from hex',
dltMessageId,
)
return
}
dltTx.messageId = dltMessageId.toString('hex')
await DltTransaction.save(dltTx)
logger.info('store messageId=%s in dltTx=%d', dltTx.messageId, dltTx.id)
} catch (e) {
logger.error(
`error while sending to dlt-connector or writing messageId of dltTx=${dltTx.id}`,
e,
)
}
}
} else {
logger.info('sending to DltConnector currently not configured...')
}
} catch (e) {
logger.error('error on sending transactions to dlt-connector.', e)
} finally {
// releae Monitor occupation
Monitor.releaseIt(MonitorNames.SEND_DLT_TRANSACTIONS)
}
} else {
logger.info('sendTransactionsToDltConnector currently locked by monitor...')
}
}
async function createDltTransactions(): Promise<void> {
const dltqb = DltTransaction.createQueryBuilder().select('transactions_id')
const newTransactions: Transaction[] = await Transaction.createQueryBuilder()
.select('id')
.addSelect('balance_date')
.where('id NOT IN (' + dltqb.getSql() + ')')
// eslint-disable-next-line camelcase
.orderBy({ balance_date: 'ASC', id: 'ASC' })
.getRawMany()
const dltTxArray: DltTransaction[] = []
let idx = 0
while (newTransactions.length > dltTxArray.length) {
// timing problems with for(let idx = 0; idx < newTransactions.length; idx++) {
const dltTx = DltTransaction.create()
dltTx.transactionId = newTransactions[idx++].id
await DltTransaction.save(dltTx)
dltTxArray.push(dltTx)
}
}

View File

@ -157,6 +157,21 @@ export const getCommunities = gql`
}
`
export const getCommunitySelections = gql`
query {
getCommunitySelections {
id
foreign
name
description
url
creationDate
uuid
authenticatedAt
}
}
`
export const queryTransactionLink = gql`
query ($code: String!) {
queryTransactionLink(code: $code) {

View File

@ -0,0 +1,50 @@
import { registerEnumType } from 'type-graphql'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
export enum MonitorNames {
SEND_DLT_TRANSACTIONS = 1,
}
registerEnumType(MonitorNames, {
name: 'MonitorNames', // this one is mandatory
description: 'Name of Monitor-keys', // this one is optional
})
/* @typescript-eslint/no-extraneous-class */
export class Monitor {
private static locks = new Map<MonitorNames, boolean>()
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
private _dummy = `to avoid unexpected class with only static properties`
public get dummy() {
return this._dummy
}
public static isLocked(key: MonitorNames): boolean | undefined {
if (this.locks.has(key)) {
logger.debug(`Monitor isLocked key=${key} = `, this.locks.get(key))
return this.locks.get(key)
}
logger.debug(`Monitor isLocked key=${key} not exists`)
return false
}
public static lockIt(key: MonitorNames): void {
logger.debug(`Monitor lockIt key=`, key)
if (this.locks.has(key)) {
throw new LogError('still existing Monitor with key=', key)
}
this.locks.set(key, true)
}
public static releaseIt(key: MonitorNames): void {
logger.debug(`Monitor releaseIt key=`, key)
if (this.locks.has(key)) {
this.locks.delete(key)
}
}
}

View File

@ -0,0 +1,33 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
import { Transaction } from '../Transaction'
@Entity('dlt_transactions', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class DltTransaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'transactions_id', type: 'int', unsigned: true, nullable: false })
transactionId: number
@Column({
name: 'message_id',
length: 64,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
messageId: string
@Column({ name: 'verified', type: 'bool', nullable: false, default: false })
verified: boolean
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
createdAt: Date
@Column({ name: 'verified_at', nullable: true, default: null, type: 'datetime' })
verifiedAt: Date | null
@OneToOne(() => Transaction, (transaction) => transaction.dltTransaction)
@JoinColumn({ name: 'transactions_id' })
transaction?: Transaction | null
}

View File

@ -0,0 +1,145 @@
/* eslint-disable no-use-before-define */
import { Decimal } from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Contribution } from '../Contribution'
import { DltTransaction } from './DltTransaction'
@Entity('transactions')
export class Transaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null })
previous: number | null
@Column({ name: 'type_id', unsigned: true, nullable: false })
typeId: number
@Column({
name: 'transaction_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
transactionLinkId?: number | null
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
balance: Decimal
@Column({
name: 'balance_date',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
balanceDate: Date
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
decay: Decimal
@Column({
name: 'decay_start',
type: 'datetime',
nullable: true,
default: null,
})
decayStart: Date | null
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
creationDate: Date | null
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({
name: 'user_gradido_id',
type: 'varchar',
length: 36,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
userGradidoID: string
@Column({
name: 'user_name',
type: 'varchar',
length: 512,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
userName: string | null
@Column({
name: 'linked_user_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedUserId?: number | null
@Column({
name: 'linked_user_gradido_id',
type: 'varchar',
length: 36,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
linkedUserGradidoID: string | null
@Column({
name: 'linked_user_name',
type: 'varchar',
length: 512,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
linkedUserName: string | null
@Column({
name: 'linked_transaction_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedTransactionId?: number | null
@OneToOne(() => Contribution, (contribution) => contribution.transaction)
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
contribution?: Contribution | null
@OneToOne(() => DltTransaction, (dlt) => dlt.transactionId)
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
dltTransaction?: DltTransaction | null
@OneToOne(() => Transaction)
@JoinColumn({ name: 'previous' })
previousTransaction?: Transaction | null
}

View File

@ -0,0 +1 @@
export { DltTransaction } from './0070-add_dlt_transactions_table/DltTransaction'

View File

@ -1 +1 @@
export { Transaction } from './0066-x-community-sendcoins-transactions_table/Transaction'
export { Transaction } from './0070-add_dlt_transactions_table/Transaction'

View File

@ -12,12 +12,14 @@ import { ContributionMessage } from './ContributionMessage'
import { Community } from './Community'
import { FederatedCommunity } from './FederatedCommunity'
import { UserRole } from './UserRole'
import { DltTransaction } from './DltTransaction'
export const entities = [
Community,
Contribution,
ContributionLink,
ContributionMessage,
DltTransaction,
Event,
FederatedCommunity,
LoginElopageBuys,

View File

@ -39,4 +39,4 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
await queryFn(
'ALTER TABLE `communities` MODIFY COLUMN `public_key` binary(64) NULL DEFAULT NULL;',
)
}
}

View File

@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE dlt_transactions (
id int unsigned NOT NULL AUTO_INCREMENT,
transactions_id int(10) unsigned NOT NULL,
message_id varchar(64) NULL DEFAULT NULL,
verified tinyint(4) NOT NULL DEFAULT 0,
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
verified_at datetime(3),
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`DROP TABLE dlt_transactions;`)
}

View File

@ -1,6 +1,6 @@
{
"name": "gradido-database",
"version": "1.23.1",
"version": "1.23.2",
"description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database",

View File

@ -1,6 +1,6 @@
{
"name": "gradido-dht-node",
"version": "1.23.1",
"version": "1.23.2",
"description": "Gradido dht-node module",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/",

View File

@ -4,7 +4,7 @@ import dotenv from 'dotenv'
dotenv.config()
const constants = {
DB_VERSION: '0069-add_user_roles_table',
DB_VERSION: '0070-add_dlt_transactions_table',
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',

View File

@ -1,6 +1,6 @@
{
"name": "gradido-dlt-connector",
"version": "1.23.1",
"version": "1.23.2",
"description": "Gradido DLT-Connector",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/",

View File

@ -6,7 +6,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 72,
lines: 76,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -1,6 +1,6 @@
{
"name": "gradido-federation",
"version": "1.23.1",
"version": "1.23.2",
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/federation",

View File

@ -11,7 +11,7 @@ Decimal.set({
*/
const constants = {
DB_VERSION: '0069-add_user_roles_table',
DB_VERSION: '0070-add_dlt_transactions_table',
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info

View File

@ -0,0 +1,27 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Community as DbCommunity } from '@entity/Community'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Field, ObjectType } from 'type-graphql'
@ObjectType()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class GetPublicCommunityInfoResult {
constructor(dbCom: DbCommunity) {
this.publicKey = dbCom.publicKey.toString()
this.name = dbCom.name
this.description = dbCom.description
this.creationDate = dbCom.creationDate
}
@Field(() => String)
name: string | null
@Field(() => String)
description: string | null
@Field(() => Date)
creationDate: Date | null
@Field(() => String)
publicKey: string
}

View File

@ -0,0 +1,65 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import { Community as DbCommunity } from '@entity/Community'
import CONFIG from '@/config'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
CONFIG.FEDERATION_API = '1_0'
beforeAll(async () => {
const server = await createServer()
con = server.con
query = createTestClient(server.apollo).query
DbCommunity.clear()
})
afterAll(async () => {
await con.close()
})
describe('PublicCommunityInfoResolver', () => {
const getPublicCommunityInfoQuery = `
query {
getPublicCommunityInfo
{
name
description
creationDate
publicKey
}
}
`
describe('getPublicCommunityInfo', () => {
let homeCom: DbCommunity
beforeEach(async () => {
homeCom = new DbCommunity()
homeCom.foreign = false
homeCom.url = 'homeCommunity-url'
homeCom.name = 'Community-Name'
homeCom.description = 'Community-Description'
homeCom.creationDate = new Date()
homeCom.publicKey = Buffer.from('homeCommunity-publicKey')
await DbCommunity.insert(homeCom)
})
it('returns public CommunityInfo', async () => {
await expect(query({ query: getPublicCommunityInfoQuery })).resolves.toMatchObject({
data: {
getPublicCommunityInfo: {
name: 'Community-Name',
description: 'Community-Description',
creationDate: homeCom.creationDate?.toISOString(),
publicKey: expect.stringMatching('homeCommunity-publicKey'),
},
},
})
})
})
})

View File

@ -0,0 +1,18 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
import { Community as DbCommunity } from '@entity/Community'
import { GetPublicCommunityInfoResult } from '../model/GetPublicCommunityInfoResult'
@Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class PublicCommunityInfoResolver {
@Query(() => GetPublicCommunityInfoResult)
async getPublicCommunityInfo(): Promise<GetPublicCommunityInfoResult> {
logger.debug(`getPublicCommunityInfo() via apiVersion=1_0 ...`)
const homeCom = await DbCommunity.findOneByOrFail({ foreign: false })
const result = new GetPublicCommunityInfoResult(homeCom)
logger.info(`getPublicCommunityInfo()-1_0... return publicInfo=${JSON.stringify(result)}`)
return result
}
}

View File

@ -4,7 +4,7 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"target": "esNext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */

View File

@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
"version": "1.23.1",
"version": "1.23.2",
"private": true,
"scripts": {
"start": "node run/server.js",

View File

@ -1,6 +1,6 @@
{
"name": "gradido",
"version": "1.23.1",
"version": "1.23.2",
"description": "Gradido",
"main": "index.js",
"repository": "git@github.com:gradido/gradido.git",