From 97b4e16f40de609d626223bab273265050cedabb Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Sat, 26 Feb 2022 17:17:43 +0100 Subject: [PATCH] new graphql interface, things build --- backend/src/graphql/enum/TransactionTypeId.ts | 2 + backend/src/graphql/model/Decay.ts | 45 ++++---- backend/src/graphql/model/Transaction.ts | 86 +++++++++------ backend/src/graphql/model/TransactionList.ts | 24 +++-- backend/src/graphql/model/User.ts | 95 ++++++++-------- .../graphql/resolver/TransactionResolver.ts | 101 +++++++----------- backend/src/graphql/resolver/UserResolver.ts | 19 +--- backend/src/util/communityUser.ts | 44 ++++++++ backend/src/util/decay.ts | 8 +- backend/src/util/virtualDecayTransaction.ts | 52 +++++++++ 10 files changed, 279 insertions(+), 197 deletions(-) create mode 100644 backend/src/util/communityUser.ts create mode 100644 backend/src/util/virtualDecayTransaction.ts diff --git a/backend/src/graphql/enum/TransactionTypeId.ts b/backend/src/graphql/enum/TransactionTypeId.ts index b5b75aa20..497ad5055 100644 --- a/backend/src/graphql/enum/TransactionTypeId.ts +++ b/backend/src/graphql/enum/TransactionTypeId.ts @@ -4,6 +4,8 @@ export enum TransactionTypeId { CREATION = 1, SEND = 2, RECEIVE = 3, + // This is a virtual property, never occurring on the database + DECAY = 4, } registerEnumType(TransactionTypeId, { diff --git a/backend/src/graphql/model/Decay.ts b/backend/src/graphql/model/Decay.ts index a09f7c97e..a76e66939 100644 --- a/backend/src/graphql/model/Decay.ts +++ b/backend/src/graphql/model/Decay.ts @@ -1,33 +1,36 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { ObjectType, Field, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' +import { ObjectType, Field } from 'type-graphql' @ObjectType() export class Decay { - constructor(json?: any) { - if (json) { - this.balance = Number(json.balance) - this.decayStart = json.decay_start - this.decayEnd = json.decay_end - this.decayDuration = json.decay_duration - this.decayStartBlock = json.decay_start_block - } + constructor( + balance: Decimal, + decay: Decimal | null, + start: Date | null, + end: Date | null, + duration: number | null, + ) { + this.balance = balance + this.decay = decay + this.start = start + this.end = end + this.duration = duration } - @Field(() => Number) - balance: number + @Field(() => Decimal) + balance: Decimal - // timestamp in seconds - @Field(() => Int, { nullable: true }) - decayStart: string + @Field(() => Decimal, { nullable: true }) + decay: Decimal | null - // timestamp in seconds - @Field(() => Int, { nullable: true }) - decayEnd: string + @Field(() => Date, { nullable: true }) + start: Date | null - @Field(() => String, { nullable: true }) - decayDuration?: number + @Field(() => Date, { nullable: true }) + end: Date | null - @Field(() => Int, { nullable: true }) - decayStartBlock?: string + @Field(() => Number, { nullable: true }) + duration: number | null } diff --git a/backend/src/graphql/model/Transaction.ts b/backend/src/graphql/model/Transaction.ts index e1b5fb625..4c8dbbe38 100644 --- a/backend/src/graphql/model/Transaction.ts +++ b/backend/src/graphql/model/Transaction.ts @@ -3,54 +3,70 @@ import Decimal from 'decimal.js-light' import { ObjectType, Field } from 'type-graphql' import { Decay } from './Decay' - -// we need a better solution for the decay block: -// the first transaction on the first page shows the decay since the last transaction -// the format is actually a Decay and not a Transaction. -// Therefore we have a lot of nullable fields, which should be always present +import { Transaction as dbTransaction } from '@entity/Transaction' +import { TransactionTypeId } from '../enum/TransactionTypeId' +import { User } from './User' @ObjectType() export class Transaction { - constructor() { - this.type = '' - this.balance = new Decimal(0) - this.totalBalance = new Decimal(0) - this.memo = '' + constructor(transaction: dbTransaction, user: User, linkedUser: User | null = null) { + this.id = transaction.id + this.user = user + this.previous = transaction.previous + this.typeId = transaction.typeId + this.amount = transaction.amount + this.balance = transaction.balance + this.balanceDate = transaction.balanceDate + if (!transaction.decayStart) { + this.decay = new Decay(transaction.balance, null, null, null, null) + } else { + this.decay = new Decay( + transaction.balance, + transaction.decay, + transaction.decayStart, + transaction.balanceDate, + (transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000, + ) + } + this.memo = transaction.memo + this.creationDate = transaction.creationDate + this.linkedUser = linkedUser + this.linkedTransactionId = transaction.linkedTransactionId } - @Field(() => String) - type: string + @Field(() => Number) + id: number + + @Field(() => User) + user: User + + @Field(() => Number, { nullable: true }) + previous: number | null + + @Field(() => TransactionTypeId) + typeId: TransactionTypeId + + @Field(() => Decimal) + amount: Decimal @Field(() => Decimal) balance: Decimal - @Field(() => Decimal) - totalBalance: Decimal + @Field(() => Date) + balanceDate: Date - @Field({ nullable: true }) - decayStart?: string - - @Field({ nullable: true }) - decayEnd?: string - - @Field({ nullable: true }) - decayDuration?: number + @Field(() => Decay) + decay: Decay @Field(() => String) memo: string + @Field(() => Date, { nullable: true }) + creationDate: Date | null + + @Field(() => User, { nullable: true }) + linkedUser: User | null + @Field(() => Number, { nullable: true }) - transactionId?: number - - @Field({ nullable: true }) - name?: string - - @Field({ nullable: true }) - email?: string - - @Field({ nullable: true }) - date?: string - - @Field({ nullable: true }) - decay?: Decay + linkedTransactionId?: number | null } diff --git a/backend/src/graphql/model/TransactionList.ts b/backend/src/graphql/model/TransactionList.ts index b9d9cb950..470eca7ef 100644 --- a/backend/src/graphql/model/TransactionList.ts +++ b/backend/src/graphql/model/TransactionList.ts @@ -2,19 +2,27 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import Decimal from 'decimal.js-light' import { ObjectType, Field } from 'type-graphql' +import CONFIG from '../../config' import { Transaction } from './Transaction' @ObjectType() export class TransactionList { - constructor() { - this.gdtSum = 0 - this.count = 0 - this.balance = new Decimal(0) - this.decayStartBlock = null + constructor( + balance: Decimal, + transactions: Transaction[], + count: number, + balanceGDT?: number | null, + decayStartBlock: Date = CONFIG.DECAY_START_TIME, + ) { + this.balance = balance + this.transactions = transactions + this.count = count + this.balanceGDT = balanceGDT || null + this.decayStartBlock = decayStartBlock } @Field(() => Number, { nullable: true }) - gdtSum: number | null + balanceGDT: number | null @Field(() => Number) count: number @@ -22,8 +30,8 @@ export class TransactionList { @Field(() => Number) balance: Decimal - @Field(() => Date, { nullable: true }) - decayStartBlock: Date | null + @Field(() => Date) + decayStartBlock: Date @Field(() => [Transaction]) transactions: Transaction[] diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index ed538bcfe..c23ea0a58 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -1,75 +1,76 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { ObjectType, Field, Int } from 'type-graphql' +import { ObjectType, Field } from 'type-graphql' import { KlickTipp } from './KlickTipp' +import { User as dbUser } from '@entity/User' @ObjectType() export class User { - /* - @Field(() => ID) - @PrimaryGeneratedColumn() - id: number - */ - constructor(json?: any) { - if (json) { - this.id = json.id - this.email = json.email - this.firstName = json.first_name - this.lastName = json.last_name - this.pubkey = json.public_hex - this.language = json.language - this.publisherId = json.publisher_id - this.isAdmin = json.isAdmin - } + constructor(user: dbUser) { + this.id = user.id + this.email = user.email + this.firstName = user.firstName + this.lastName = user.lastName + this.deletedAt = user.deletedAt + this.createdAt = user.createdAt + this.emailChecked = user.emailChecked + this.language = user.language + this.publisherId = user.publisherId + // TODO + this.isAdmin = null + this.coinanimation = null + this.klickTipp = null + this.hasElopage = null } @Field(() => Number) id: number + // `public_key` binary(32) DEFAULT NULL, + // `privkey` binary(80) DEFAULT NULL, + + // TODO privacy issue here @Field(() => String) email: string - @Field(() => String) - firstName: string + @Field(() => String, { nullable: true }) + firstName: string | null - @Field(() => String) - lastName: string + @Field(() => String, { nullable: true }) + lastName: string | null - @Field(() => String) - pubkey: string - /* - @Field(() => String) - pubkey: string + @Field(() => Date, { nullable: true }) + deletedAt: Date | null - // not sure about the type here. Maybe better to have a string - @Field(() => number) - created: number + // `password` bigint(20) unsigned DEFAULT 0, + // `email_hash` binary(32) DEFAULT NULL, - @Field(() =>>> Boolean) + @Field(() => Date) + createdAt: Date + + @Field(() => Boolean) emailChecked: boolean - */ @Field(() => String) language: string - /* - @Field(() => Boolean) - disabled: boolean - */ + // This is not the users publisherId, but the one of the users who recommend him + @Field(() => Number, { nullable: true }) + publisherId: number | null - // what is publisherId? - @Field(() => Int, { nullable: true }) - publisherId?: number + // `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, - @Field(() => Boolean) - isAdmin: boolean - - @Field(() => Boolean) - coinanimation: boolean - - @Field(() => KlickTipp) - klickTipp: KlickTipp + // TODO this is a bit inconsistent with what we query from the database + // therefore all those fields are now nullable with default value null + @Field(() => Boolean, { nullable: true }) + isAdmin: boolean | null @Field(() => Boolean, { nullable: true }) - hasElopage?: boolean + coinanimation: boolean | null + + @Field(() => KlickTipp, { nullable: true }) + klickTipp: KlickTipp | null + + @Field(() => Boolean, { nullable: true }) + hasElopage: boolean | null } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 0bc3a5a32..3e84646dd 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -20,15 +20,17 @@ import { Order } from '../enum/Order' import { UserRepository } from '../../typeorm/repository/User' import { TransactionRepository } from '../../typeorm/repository/Transaction' -import { User as dbUser, User } from '@entity/User' +import { User as dbUser } from '@entity/User' import { Transaction as dbTransaction } from '@entity/Transaction' import { apiPost } from '../../apis/HttpRequest' import { calculateDecay } from '../../util/decay' import { TransactionTypeId } from '../enum/TransactionTypeId' -import { TransactionType } from '../enum/TransactionType' import { calculateBalance, isHexPublicKey } from '../../util/validate' import { RIGHTS } from '../../auth/RIGHTS' +import { User } from '../model/User' +import { communityUser } from '../../util/communityUser' +import { virtualDecayTransaction } from '../../util/virtualDecayTransaction' @Resolver() export class TransactionResolver { @@ -87,88 +89,57 @@ export class TransactionResolver { // remove duplicates involvedUserIds = involvedUserIds.filter((value, index, self) => self.indexOf(value) === index) // We need to show the name for deleted users for old transactions - const involvedUsers = await User.createQueryBuilder() + const involvedDbUsers = await dbUser + .createQueryBuilder() .withDeleted() .where('user.id IN (:...userIds)', { involvedUserIds }) .getMany() + const involvedUsers = involvedDbUsers.map((u) => new User(u)) + const self = new User(user) const transactions: Transaction[] = [] // decay transaction if (currentPage === 1 && order === Order.DESC) { const now = new Date() - const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now) - const balance = decay.balance.minus(lastTransaction.balance) - - const decayTransaction = new Transaction() - decayTransaction.type = 'decay' - decayTransaction.balance = balance - // TODO - // decayTransaction.decayDuration = decay.duration - // decayTransaction.decayStart = decay.start - // decayTransaction.decayEnd = decay.end - transactions.push(decayTransaction) + transactions.push( + virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self), + ) } - if (userTransactions.length) { - for (let i = 0; i < userTransactions.length; i++) { - const userTransaction = userTransactions[i] - const finalTransaction = new Transaction() - finalTransaction.transactionId = userTransaction.id - finalTransaction.date = userTransaction.balanceDate.toISOString() - finalTransaction.memo = userTransaction.memo - finalTransaction.totalBalance = userTransaction.balance - finalTransaction.balance = userTransaction.amount - - const otherUser = involvedUsers.find((u) => u.id === userTransaction.linkedUserId) - switch (userTransaction.typeId) { - case TransactionTypeId.CREATION: - finalTransaction.name = 'Gradido Akademie' - finalTransaction.type = TransactionType.CREATION - break - case TransactionTypeId.SEND: - finalTransaction.type = TransactionType.SEND - if (otherUser) { - finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName - finalTransaction.email = otherUser.email - } - break - case TransactionTypeId.RECEIVE: - finalTransaction.type = TransactionType.RECIEVE - if (otherUser) { - finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName - finalTransaction.email = otherUser.email - } - break - default: - throw new Error('invalid transaction') - } - transactions.push(finalTransaction) + for (let i = 0; i < userTransactions.length; i++) { + const userTransaction = userTransactions[i] + let linkedUser = null + if (userTransaction.typeId === TransactionTypeId.CREATION) { + linkedUser = communityUser + } else { + linkedUser = involvedUsers.find((u) => u.id === userTransaction.linkedUserId) } + transactions.push(new Transaction(userTransaction, self, linkedUser)) } - const transactionList = new TransactionList() - transactionList.count = userTransactionsCount - transactionList.transactions = transactions - - // get gdt sum - transactionList.gdtSum = null + // get GDT + let balanceGDT = null try { const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, { email: user.email, }) - if (resultGDTSum.success) transactionList.gdtSum = Number(resultGDTSum.data.sum) || 0 - } catch (err: any) {} + if (!resultGDTSum.success) { + throw new Error('Call not successful') + } + balanceGDT = Number(resultGDTSum.data.sum) || 0 + } catch (err: any) { + // eslint-disable-next-line no-console + console.log('Could not query GDT Server', err) + } - // get balance - transactionList.balance = lastTransaction.balance - transactionList.decayStartBlock = CONFIG.DECAY_START_TIME - // const now = new Date() - // TODO this seems duplicated - // transactionList.decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now) - // transactionList.decayDate = now.toString() - - return transactionList + // Construct Result + return new TransactionList( + lastTransaction.balance, + transactions, + userTransactionsCount, + balanceGDT, + ) } @Authorized([RIGHTS.SEND_COINS]) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index dfa685ed0..fa358ad1f 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -216,14 +216,8 @@ export class UserResolver { // TODO refactor and do not have duplicate code with login(see below) const userRepository = getCustomRepository(UserRepository) const userEntity = await userRepository.findByPubkeyHex(context.pubKey) - const user = new User() - user.id = userEntity.id - user.email = userEntity.email - user.firstName = userEntity.firstName - user.lastName = userEntity.lastName - user.pubkey = userEntity.pubKey.toString('hex') - user.language = userEntity.language - + const user = new User(userEntity) + // user.pubkey = userEntity.pubKey.toString('hex') // Elopage Status & Stored PublisherId user.hasElopage = await this.hasElopage(context) @@ -271,12 +265,9 @@ export class UserResolver { throw new Error('No user with this credentials') } - const user = new User() - user.id = dbUser.id - user.email = email - user.firstName = dbUser.firstName - user.lastName = dbUser.lastName - user.pubkey = dbUser.pubKey.toString('hex') + const user = new User(dbUser) + // user.email = email + // user.pubkey = dbUser.pubKey.toString('hex') user.language = dbUser.language // Elopage Status & Stored PublisherId diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts new file mode 100644 index 000000000..5dfec9327 --- /dev/null +++ b/backend/src/util/communityUser.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' +import { User as dbUser } from '@entity/User' +import { User } from '../graphql/model/User' + +const communityDbUser: dbUser = { + id: -1, + email: 'support@gradido.net', + firstName: 'Gradido', + lastName: 'Akademie', + pubKey: Buffer.from(''), + privKey: Buffer.from(''), + deletedAt: null, + password: BigInt(0), + emailHash: Buffer.from(''), + createdAt: new Date(), + emailChecked: false, + language: '', + publisherId: 0, + passphrase: '', + settings: [], + hasId: function (): boolean { + throw new Error('Function not implemented.') + }, + save: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + remove: function (options?: RemoveOptions): Promise { + throw new Error('Function not implemented.') + }, + softRemove: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + recover: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + reload: function (): Promise { + throw new Error('Function not implemented.') + }, +} +const communityUser = new User(communityDbUser) + +export { communityDbUser, communityUser } diff --git a/backend/src/util/decay.ts b/backend/src/util/decay.ts index 21ecfa151..f0f1d181a 100644 --- a/backend/src/util/decay.ts +++ b/backend/src/util/decay.ts @@ -1,14 +1,8 @@ import Decimal from 'decimal.js-light' import CONFIG from '../config' +import { Decay } from '../graphql/model/Decay' // TODO: externalize all those definitions and functions into an external decay library -interface Decay { - balance: Decimal - decay: Decimal | null - start: Date | null - end: Date | null - duration: number | null -} function decayFormula(value: Decimal, seconds: number): Decimal { return value.mul(new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds)) diff --git a/backend/src/util/virtualDecayTransaction.ts b/backend/src/util/virtualDecayTransaction.ts new file mode 100644 index 000000000..f5f35823f --- /dev/null +++ b/backend/src/util/virtualDecayTransaction.ts @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import Decimal from 'decimal.js-light' +import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' +import { Transaction as dbTransaction } from '@entity/Transaction' +import { calculateDecay } from './decay' +import { TransactionTypeId } from '../graphql/enum/TransactionTypeId' +import { Transaction } from '../graphql/model/Transaction' +import { User } from '../graphql/model/User' + +const virtualDecayTransaction = ( + balance: Decimal, + balanceDate: Date, + time: Date = new Date(), + user: User, +): Transaction => { + const decay = calculateDecay(balance, balanceDate, time) + // const balance = decay.balance.minus(lastTransaction.balance) + const decayDbTransaction: dbTransaction = { + id: -1, + userId: -1, + previous: -1, + typeId: TransactionTypeId.DECAY, + amount: new Decimal(0), + balance: decay.balance, + balanceDate: time, + decay: decay.decay ? decay.decay : new Decimal(0), + decayStart: decay.start, + memo: '', + creationDate: null, + hasId: function (): boolean { + throw new Error('Function not implemented.') + }, + save: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + remove: function (options?: RemoveOptions): Promise { + throw new Error('Function not implemented.') + }, + softRemove: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + recover: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + reload: function (): Promise { + throw new Error('Function not implemented.') + }, + } + return new Transaction(decayDbTransaction, user) +} + +export { virtualDecayTransaction }