mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into refactor-simple-validation-with-typed-graphql
This commit is contained in:
commit
81083c0569
@ -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)
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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: '',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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/
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
174
backend/src/apis/DltConnectorClient.test.ts
Normal file
174
backend/src/apis/DltConnectorClient.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
*/
|
||||
104
backend/src/apis/DltConnectorClient.ts
Normal file
104
backend/src/apis/DltConnectorClient.ts
Normal 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...')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export interface PublicCommunityInfo {
|
||||
name: string
|
||||
description: string
|
||||
creationDate: Date
|
||||
publicKey: string
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const getPublicCommunityInfo = gql`
|
||||
query {
|
||||
getPublicCommunityInfo {
|
||||
name
|
||||
description
|
||||
creationDate
|
||||
publicKey
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -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', () => {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,15 +6,15 @@ import { Order } from '@enum/Order'
|
||||
|
||||
@ArgsType()
|
||||
export class Paginated {
|
||||
@Field(() => Int, { nullable: true })
|
||||
@Field(() => Int, { defaultValue: 1 })
|
||||
@IsPositive()
|
||||
currentPage?: number
|
||||
currentPage: number
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
@Field(() => Int, { defaultValue: 3 })
|
||||
@IsPositive()
|
||||
pageSize?: number
|
||||
pageSize: number
|
||||
|
||||
@Field(() => Order, { nullable: true })
|
||||
@Field(() => Order, { defaultValue: Order.DESC })
|
||||
@IsEnum(Order)
|
||||
order?: Order
|
||||
order: Order
|
||||
}
|
||||
|
||||
18
backend/src/graphql/arg/SearchContributionsFilterArgs.ts
Normal file
18
backend/src/graphql/arg/SearchContributionsFilterArgs.ts
Normal 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
|
||||
}
|
||||
@ -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(),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
50
backend/src/util/Monitor.ts
Normal file
50
backend/src/util/Monitor.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
145
database/entity/0070-add_dlt_transactions_table/Transaction.ts
Normal file
145
database/entity/0070-add_dlt_transactions_table/Transaction.ts
Normal 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
|
||||
}
|
||||
1
database/entity/DltTransaction.ts
Normal file
1
database/entity/DltTransaction.ts
Normal file
@ -0,0 +1 @@
|
||||
export { DltTransaction } from './0070-add_dlt_transactions_table/DltTransaction'
|
||||
@ -1 +1 @@
|
||||
export { Transaction } from './0066-x-community-sendcoins-transactions_table/Transaction'
|
||||
export { Transaction } from './0070-add_dlt_transactions_table/Transaction'
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
19
database/migrations/0070-add_dlt_transactions_table.ts
Normal file
19
database/migrations/0070-add_dlt_transactions_table.ts
Normal 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;`)
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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/",
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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/",
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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'),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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. */
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user