Merge remote-tracking branch 'origin/master' into

2913-feature-mirgate-transactions-table-for-x-community-sendcoins
This commit is contained in:
Claus-Peter Huebner 2023-05-04 20:39:53 +02:00
commit 3434c6a34f
42 changed files with 606 additions and 131 deletions

View File

@ -18,6 +18,7 @@ export const klicktippSignIn = async (
firstName?: string, firstName?: string,
lastName?: string, lastName?: string,
): Promise<boolean> => { ): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const fields = { const fields = {
fieldFirstName: firstName, fieldFirstName: firstName,
fieldLastName: lastName, fieldLastName: lastName,
@ -28,12 +29,14 @@ export const klicktippSignIn = async (
} }
export const signout = async (email: string, language: string): Promise<boolean> => { export const signout = async (email: string, language: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN
const result = await klicktippConnector.signoff(apiKey, email) const result = await klicktippConnector.signoff(apiKey, email)
return result return result
} }
export const unsubscribe = async (email: string): Promise<boolean> => { export const unsubscribe = async (email: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
if (isLogin) { if (isLogin) {
return await klicktippConnector.unsubscribe(email) return await klicktippConnector.unsubscribe(email)
@ -42,6 +45,7 @@ export const unsubscribe = async (email: string): Promise<boolean> => {
} }
export const getKlickTippUser = async (email: string): Promise<any> => { export const getKlickTippUser = async (email: string): Promise<any> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
if (isLogin) { if (isLogin) {
const subscriberId = await klicktippConnector.subscriberSearch(email) const subscriberId = await klicktippConnector.subscriberSearch(email)
@ -52,14 +56,17 @@ export const getKlickTippUser = async (email: string): Promise<any> => {
} }
export const loginKlicktippUser = async (): Promise<boolean> => { export const loginKlicktippUser = async (): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD)
} }
export const logoutKlicktippUser = async (): Promise<boolean> => { export const logoutKlicktippUser = async (): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
return await klicktippConnector.logout() return await klicktippConnector.logout()
} }
export const untagUser = async (email: string, tagId: string): Promise<boolean> => { export const untagUser = async (email: string, tagId: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
if (isLogin) { if (isLogin) {
return await klicktippConnector.untag(email, tagId) return await klicktippConnector.untag(email, tagId)
@ -68,6 +75,7 @@ export const untagUser = async (email: string, tagId: string): Promise<boolean>
} }
export const tagUser = async (email: string, tagIds: string): Promise<boolean> => { export const tagUser = async (email: string, tagIds: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
if (isLogin) { if (isLogin) {
return await klicktippConnector.tag(email, tagIds) return await klicktippConnector.tag(email, tagIds)
@ -76,6 +84,7 @@ export const tagUser = async (email: string, tagIds: string): Promise<boolean> =
} }
export const getKlicktippTagMap = async () => { export const getKlicktippTagMap = async () => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
if (isLogin) { if (isLogin) {
return await klicktippConnector.tagIndex() return await klicktippConnector.tagIndex()

View File

@ -5,8 +5,6 @@ export enum RIGHTS {
COMMUNITIES = 'COMMUNITIES', COMMUNITIES = 'COMMUNITIES',
LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES', LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES',
EXIST_PID = 'EXIST_PID', EXIST_PID = 'EXIST_PID',
GET_KLICKTIPP_USER = 'GET_KLICKTIPP_USER',
GET_KLICKTIPP_TAG_MAP = 'GET_KLICKTIPP_TAG_MAP',
UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER', UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER',
SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER', SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER',
TRANSACTION_LIST = 'TRANSACTION_LIST', TRANSACTION_LIST = 'TRANSACTION_LIST',

View File

@ -9,8 +9,6 @@ export const ROLE_USER = new Role('user', [
RIGHTS.BALANCE, RIGHTS.BALANCE,
RIGHTS.LIST_GDT_ENTRIES, RIGHTS.LIST_GDT_ENTRIES,
RIGHTS.EXIST_PID, RIGHTS.EXIST_PID,
RIGHTS.GET_KLICKTIPP_USER,
RIGHTS.GET_KLICKTIPP_TAG_MAP,
RIGHTS.UNSUBSCRIBE_NEWSLETTER, RIGHTS.UNSUBSCRIBE_NEWSLETTER,
RIGHTS.SUBSCRIBE_NEWSLETTER, RIGHTS.SUBSCRIBE_NEWSLETTER,
RIGHTS.TRANSACTION_LIST, RIGHTS.TRANSACTION_LIST,

View File

@ -12,7 +12,7 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0065-x-community-sendcoins-transactions_table', DB_VERSION: '0066-x-community-sendcoins-transactions_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info

View File

@ -0,0 +1,8 @@
import { Event as DbEvent } from '@entity/Event'
import { User as DbUser } from '@entity/User'
import { Event } from './Event'
import { EventType } from './EventType'
export const EVENT_NEWSLETTER_SUBSCRIBE = async (user: DbUser): Promise<DbEvent> =>
Event(EventType.NEWSLETTER_SUBSCRIBE, user, user).save()

View File

@ -0,0 +1,8 @@
import { Event as DbEvent } from '@entity/Event'
import { User as DbUser } from '@entity/User'
import { Event } from './Event'
import { EventType } from './EventType'
export const EVENT_NEWSLETTER_UNSUBSCRIBE = async (user: DbUser): Promise<DbEvent> =>
Event(EventType.NEWSLETTER_UNSUBSCRIBE, user, user).save()

View File

@ -21,6 +21,8 @@ export enum EventType {
EMAIL_ADMIN_CONFIRMATION = 'EMAIL_ADMIN_CONFIRMATION', EMAIL_ADMIN_CONFIRMATION = 'EMAIL_ADMIN_CONFIRMATION',
EMAIL_CONFIRMATION = 'EMAIL_CONFIRMATION', EMAIL_CONFIRMATION = 'EMAIL_CONFIRMATION',
EMAIL_FORGOT_PASSWORD = 'EMAIL_FORGOT_PASSWORD', EMAIL_FORGOT_PASSWORD = 'EMAIL_FORGOT_PASSWORD',
NEWSLETTER_SUBSCRIBE = 'NEWSLETTER_SUBSCRIBE',
NEWSLETTER_UNSUBSCRIBE = 'NEWSLETTER_UNSUBSCRIBE',
TRANSACTION_SEND = 'TRANSACTION_SEND', TRANSACTION_SEND = 'TRANSACTION_SEND',
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE', TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
TRANSACTION_LINK_CREATE = 'TRANSACTION_LINK_CREATE', TRANSACTION_LINK_CREATE = 'TRANSACTION_LINK_CREATE',

View File

@ -21,6 +21,8 @@ export { EVENT_EMAIL_ACCOUNT_MULTIREGISTRATION } from './EVENT_EMAIL_ACCOUNT_MUL
export { EVENT_EMAIL_ADMIN_CONFIRMATION } from './EVENT_EMAIL_ADMIN_CONFIRMATION' export { EVENT_EMAIL_ADMIN_CONFIRMATION } from './EVENT_EMAIL_ADMIN_CONFIRMATION'
export { EVENT_EMAIL_CONFIRMATION } from './EVENT_EMAIL_CONFIRMATION' export { EVENT_EMAIL_CONFIRMATION } from './EVENT_EMAIL_CONFIRMATION'
export { EVENT_EMAIL_FORGOT_PASSWORD } from './EVENT_EMAIL_FORGOT_PASSWORD' export { EVENT_EMAIL_FORGOT_PASSWORD } from './EVENT_EMAIL_FORGOT_PASSWORD'
export { EVENT_NEWSLETTER_SUBSCRIBE } from './EVENT_NEWSLETTER_SUBSCRIBE'
export { EVENT_NEWSLETTER_UNSUBSCRIBE } from './EVENT_NEWSLETTER_UNSUBSCRIBE'
export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND' export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND'
export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE' export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE'
export { EVENT_TRANSACTION_LINK_CREATE } from './EVENT_TRANSACTION_LINK_CREATE' export { EVENT_TRANSACTION_LINK_CREATE } from './EVENT_TRANSACTION_LINK_CREATE'

View File

@ -1,14 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { gql } from 'graphql-request' import { gql } from 'graphql-request'
import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient' import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient'
import { LogError } from '@/server/LogError' import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string | undefined> { export async function requestGetPublicKey(
dbCom: DbFederatedCommunity,
): Promise<string | undefined> {
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/' let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
endpoint = `${endpoint}${dbCom.apiVersion}/` endpoint = `${endpoint}${dbCom.apiVersion}/`
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`) logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)

View File

@ -1,14 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { gql } from 'graphql-request' import { gql } from 'graphql-request'
import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient' import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient'
import { LogError } from '@/server/LogError' import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string | undefined> { export async function requestGetPublicKey(
dbCom: DbFederatedCommunity,
): Promise<string | undefined> {
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/' let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
endpoint = `${endpoint}${dbCom.apiVersion}/` endpoint = `${endpoint}${dbCom.apiVersion}/`
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`) logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)

View File

@ -1,5 +1,12 @@
/* 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 { 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 { ApolloServerTestClient } from 'apollo-server-testing'
import { testEnvironment, cleanDB } from '@test/helpers' import { testEnvironment, cleanDB } from '@test/helpers'
@ -58,9 +65,9 @@ describe('validate Communities', () => {
endPoint: 'http//localhost:5001/api/', endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(), lastAnnouncedAt: new Date(),
} }
await DbCommunity.createQueryBuilder() await DbFederatedCommunity.createQueryBuilder()
.insert() .insert()
.into(DbCommunity) .into(DbFederatedCommunity)
.values(variables1) .values(variables1)
.orUpdate({ .orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
@ -89,9 +96,9 @@ describe('validate Communities', () => {
endPoint: 'http//localhost:5001/api/', endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(), lastAnnouncedAt: new Date(),
} }
await DbCommunity.createQueryBuilder() await DbFederatedCommunity.createQueryBuilder()
.insert() .insert()
.into(DbCommunity) .into(DbFederatedCommunity)
.values(variables2) .values(variables2)
.orUpdate({ .orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
@ -117,7 +124,7 @@ describe('validate Communities', () => {
}) })
}) })
describe('with three Communities of api 1_0, 1_1 and 2_0', () => { describe('with three Communities of api 1_0, 1_1 and 2_0', () => {
let dbCom: DbCommunity let dbCom: DbFederatedCommunity
beforeEach(async () => { beforeEach(async () => {
const variables3 = { const variables3 = {
publicKey: Buffer.from('11111111111111111111111111111111'), publicKey: Buffer.from('11111111111111111111111111111111'),
@ -125,16 +132,16 @@ describe('validate Communities', () => {
endPoint: 'http//localhost:5001/api/', endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(), lastAnnouncedAt: new Date(),
} }
await DbCommunity.createQueryBuilder() await DbFederatedCommunity.createQueryBuilder()
.insert() .insert()
.into(DbCommunity) .into(DbFederatedCommunity)
.values(variables3) .values(variables3)
.orUpdate({ .orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'], overwrite: ['end_point', 'last_announced_at'],
}) })
.execute() .execute()
dbCom = await DbCommunity.findOneOrFail({ dbCom = await DbFederatedCommunity.findOneOrFail({
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion }, where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
}) })
jest.clearAllMocks() jest.clearAllMocks()

View File

@ -1,5 +1,7 @@
/** eslint-disable @typescript-eslint/no-unsafe-call */
/** eslint-disable @typescript-eslint/no-unsafe-assignment */
import { IsNull } from '@dbTools/typeorm' import { IsNull } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { LogError } from '@/server/LogError' import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
@ -23,13 +25,14 @@ export function startValidateCommunities(timerInterval: number): void {
} }
export async function validateCommunities(): Promise<void> { export async function validateCommunities(): Promise<void> {
const dbCommunities: DbCommunity[] = await DbCommunity.createQueryBuilder() const dbFederatedCommunities: DbFederatedCommunity[] =
.where({ foreign: true, verifiedAt: IsNull() }) await DbFederatedCommunity.createQueryBuilder()
.orWhere('verified_at < last_announced_at') .where({ foreign: true, verifiedAt: IsNull() })
.getMany() .orWhere('verified_at < last_announced_at')
.getMany()
logger.debug(`Federation: found ${dbCommunities.length} dbCommunities`) logger.debug(`Federation: found ${dbFederatedCommunities.length} dbCommunities`)
for (const dbCom of dbCommunities) { for (const dbCom of dbFederatedCommunities) {
logger.debug('Federation: dbCom', dbCom) logger.debug('Federation: dbCom', dbCom)
const apiValueStrings: string[] = Object.values(ApiVersionType) const apiValueStrings: string[] = Object.values(ApiVersionType)
logger.debug(`suppported ApiVersions=`, apiValueStrings) logger.debug(`suppported ApiVersions=`, apiValueStrings)
@ -46,7 +49,7 @@ export async function validateCommunities(): Promise<void> {
) )
if (pubKey && pubKey === dbCom.publicKey.toString()) { if (pubKey && pubKey === dbCom.publicKey.toString()) {
logger.info(`Federation: matching publicKey: ${pubKey}`) logger.info(`Federation: matching publicKey: ${pubKey}`)
await DbCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`) logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`)
} else { } else {
logger.warn( logger.warn(
@ -74,7 +77,9 @@ function isLogError(err: unknown) {
return err instanceof LogError return err instanceof LogError
} }
async function invokeVersionedRequestGetPublicKey(dbCom: DbCommunity): Promise<string | undefined> { async function invokeVersionedRequestGetPublicKey(
dbCom: DbFederatedCommunity,
): Promise<string | undefined> {
switch (dbCom.apiVersion) { switch (dbCom.apiVersion) {
case ApiVersionType.V1_0: case ApiVersionType.V1_0:
return v1_0_requestGetPublicKey(dbCom) return v1_0_requestGetPublicKey(dbCom)

View File

@ -6,14 +6,12 @@ export class Community {
constructor(dbCom: DbCommunity) { constructor(dbCom: DbCommunity) {
this.id = dbCom.id this.id = dbCom.id
this.foreign = dbCom.foreign this.foreign = dbCom.foreign
this.publicKey = dbCom.publicKey.toString() this.name = dbCom.name
this.url = this.description = dbCom.description
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion this.url = dbCom.url
this.lastAnnouncedAt = dbCom.lastAnnouncedAt this.creationDate = dbCom.creationDate
this.verifiedAt = dbCom.verifiedAt this.uuid = dbCom.communityUuid
this.lastErrorAt = dbCom.lastErrorAt this.authenticatedAt = dbCom.authenticatedAt
this.createdAt = dbCom.createdAt
this.updatedAt = dbCom.updatedAt
} }
@Field(() => Int) @Field(() => Int)
@ -22,24 +20,21 @@ export class Community {
@Field(() => Boolean) @Field(() => Boolean)
foreign: boolean foreign: boolean
@Field(() => String) @Field(() => String, { nullable: true })
publicKey: string name: string | null
@Field(() => String, { nullable: true })
description: string | null
@Field(() => String) @Field(() => String)
url: string url: string
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
lastAnnouncedAt: Date | null creationDate: Date | null
@Field(() => String, { nullable: true })
uuid: string | null
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
verifiedAt: Date | null authenticatedAt: Date | null
@Field(() => Date, { nullable: true })
lastErrorAt: Date | null
@Field(() => Date, { nullable: true })
createdAt: Date | null
@Field(() => Date, { nullable: true })
updatedAt: Date | null
} }

View File

@ -0,0 +1,45 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ObjectType, Field, Int } from 'type-graphql'
@ObjectType()
export class FederatedCommunity {
constructor(dbCom: DbFederatedCommunity) {
this.id = dbCom.id
this.foreign = dbCom.foreign
this.publicKey = dbCom.publicKey.toString()
this.url =
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
this.verifiedAt = dbCom.verifiedAt
this.lastErrorAt = dbCom.lastErrorAt
this.createdAt = dbCom.createdAt
this.updatedAt = dbCom.updatedAt
}
@Field(() => Int)
id: number
@Field(() => Boolean)
foreign: boolean
@Field(() => String)
publicKey: string
@Field(() => String)
url: string
@Field(() => Date, { nullable: true })
lastAnnouncedAt: Date | null
@Field(() => Date, { nullable: true })
verifiedAt: Date | null
@Field(() => Date, { nullable: true })
lastErrorAt: Date | null
@Field(() => Date, { nullable: true })
createdAt: Date | null
@Field(() => Date, { nullable: true })
updatedAt: Date | null
}

View File

@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Connection } from '@dbTools/typeorm' 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 { ApolloServerTestClient } from 'apollo-server-testing'
import { testEnvironment } from '@test/helpers' import { testEnvironment } from '@test/helpers'
@ -19,7 +25,7 @@ beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment()
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
await DbCommunity.clear() await DbFederatedCommunity.clear()
}) })
afterAll(async () => { afterAll(async () => {
@ -28,12 +34,12 @@ afterAll(async () => {
describe('CommunityResolver', () => { describe('CommunityResolver', () => {
describe('getCommunities', () => { describe('getCommunities', () => {
let homeCom1: DbCommunity let homeCom1: DbFederatedCommunity
let homeCom2: DbCommunity let homeCom2: DbFederatedCommunity
let homeCom3: DbCommunity let homeCom3: DbFederatedCommunity
let foreignCom1: DbCommunity let foreignCom1: DbFederatedCommunity
let foreignCom2: DbCommunity let foreignCom2: DbFederatedCommunity
let foreignCom3: DbCommunity let foreignCom3: DbFederatedCommunity
describe('with empty list', () => { describe('with empty list', () => {
it('returns no community entry', async () => { it('returns no community entry', async () => {
@ -51,29 +57,29 @@ describe('CommunityResolver', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
homeCom1 = DbCommunity.create() homeCom1 = DbFederatedCommunity.create()
homeCom1.foreign = false homeCom1.foreign = false
homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity') homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom1.apiVersion = '1_0' homeCom1.apiVersion = '1_0'
homeCom1.endPoint = 'http://localhost/api' homeCom1.endPoint = 'http://localhost/api'
homeCom1.createdAt = new Date() homeCom1.createdAt = new Date()
await DbCommunity.insert(homeCom1) await DbFederatedCommunity.insert(homeCom1)
homeCom2 = DbCommunity.create() homeCom2 = DbFederatedCommunity.create()
homeCom2.foreign = false homeCom2.foreign = false
homeCom2.publicKey = Buffer.from('publicKey-HomeCommunity') homeCom2.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom2.apiVersion = '1_1' homeCom2.apiVersion = '1_1'
homeCom2.endPoint = 'http://localhost/api' homeCom2.endPoint = 'http://localhost/api'
homeCom2.createdAt = new Date() homeCom2.createdAt = new Date()
await DbCommunity.insert(homeCom2) await DbFederatedCommunity.insert(homeCom2)
homeCom3 = DbCommunity.create() homeCom3 = DbFederatedCommunity.create()
homeCom3.foreign = false homeCom3.foreign = false
homeCom3.publicKey = Buffer.from('publicKey-HomeCommunity') homeCom3.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom3.apiVersion = '2_0' homeCom3.apiVersion = '2_0'
homeCom3.endPoint = 'http://localhost/api' homeCom3.endPoint = 'http://localhost/api'
homeCom3.createdAt = new Date() homeCom3.createdAt = new Date()
await DbCommunity.insert(homeCom3) await DbFederatedCommunity.insert(homeCom3)
}) })
it('returns 3 home-community entries', async () => { it('returns 3 home-community entries', async () => {
@ -123,29 +129,29 @@ describe('CommunityResolver', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
foreignCom1 = DbCommunity.create() foreignCom1 = DbFederatedCommunity.create()
foreignCom1.foreign = true foreignCom1.foreign = true
foreignCom1.publicKey = Buffer.from('publicKey-ForeignCommunity') foreignCom1.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom1.apiVersion = '1_0' foreignCom1.apiVersion = '1_0'
foreignCom1.endPoint = 'http://remotehost/api' foreignCom1.endPoint = 'http://remotehost/api'
foreignCom1.createdAt = new Date() foreignCom1.createdAt = new Date()
await DbCommunity.insert(foreignCom1) await DbFederatedCommunity.insert(foreignCom1)
foreignCom2 = DbCommunity.create() foreignCom2 = DbFederatedCommunity.create()
foreignCom2.foreign = true foreignCom2.foreign = true
foreignCom2.publicKey = Buffer.from('publicKey-ForeignCommunity') foreignCom2.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom2.apiVersion = '1_1' foreignCom2.apiVersion = '1_1'
foreignCom2.endPoint = 'http://remotehost/api' foreignCom2.endPoint = 'http://remotehost/api'
foreignCom2.createdAt = new Date() foreignCom2.createdAt = new Date()
await DbCommunity.insert(foreignCom2) await DbFederatedCommunity.insert(foreignCom2)
foreignCom3 = DbCommunity.create() foreignCom3 = DbFederatedCommunity.create()
foreignCom3.foreign = true foreignCom3.foreign = true
foreignCom3.publicKey = Buffer.from('publicKey-ForeignCommunity') foreignCom3.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom3.apiVersion = '1_2' foreignCom3.apiVersion = '1_2'
foreignCom3.endPoint = 'http://remotehost/api' foreignCom3.endPoint = 'http://remotehost/api'
foreignCom3.createdAt = new Date() foreignCom3.createdAt = new Date()
await DbCommunity.insert(foreignCom3) await DbFederatedCommunity.insert(foreignCom3)
}) })
it('returns 3 home community and 3 foreign community entries', async () => { it('returns 3 home community and 3 foreign community entries', async () => {

View File

@ -1,22 +1,37 @@
import { Community as DbCommunity } from '@entity/Community' import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { Resolver, Query, Authorized } from 'type-graphql' import { Resolver, Query, Authorized } from 'type-graphql'
import { Community } from '@model/Community' import { Community } from '@model/Community'
import { FederatedCommunity } from '@model/FederatedCommunity'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
@Resolver() @Resolver()
export class CommunityResolver { export class CommunityResolver {
@Authorized([RIGHTS.COMMUNITIES]) @Authorized([RIGHTS.COMMUNITIES])
@Query(() => [Community]) @Query(() => [FederatedCommunity])
async getCommunities(): Promise<Community[]> { async getCommunities(): Promise<FederatedCommunity[]> {
const dbCommunities: DbCommunity[] = await DbCommunity.find({ const dbFederatedCommunities: DbFederatedCommunity[] = await DbFederatedCommunity.find({
order: { order: {
foreign: 'ASC', foreign: 'ASC',
createdAt: 'DESC', createdAt: 'DESC',
lastAnnouncedAt: 'DESC', lastAnnouncedAt: 'DESC',
}, },
}) })
return dbFederatedCommunities.map(
(dbCom: DbFederatedCommunity) => new FederatedCommunity(dbCom),
)
}
@Authorized([RIGHTS.COMMUNITIES])
@Query(() => [Community])
async getCommunitySelections(): Promise<Community[]> {
const dbCommunities: DbCommunity[] = await DbCommunity.find({
order: {
name: 'ASC',
},
})
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom)) return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
} }
} }

View File

@ -0,0 +1,138 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Event as DbEvent } from '@entity/Event'
import { UserContact } from '@entity/UserContact'
import { GraphQLError } from 'graphql'
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup'
import { EventType } from '@/event/Events'
import { userFactory } from '@/seeds/factory/user'
import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
let testEnv: any, mutate: any, con: any
beforeAll(async () => {
testEnv = await testEnvironment(logger, localization)
mutate = testEnv.mutate
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('KlicktippResolver', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
})
afterAll(async () => {
await cleanDB()
})
describe('subscribeNewsletter', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({
mutation: subscribeNewsletter,
})
expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')])
})
})
describe('authenticated', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
it('calls API', async () => {
const {
data: { subscribeNewsletter: isSubscribed },
}: { data: { subscribeNewsletter: boolean } } = await mutate({
mutation: subscribeNewsletter,
})
expect(isSubscribed).toEqual(true)
})
it('stores the NEWSLETTER_SUBSCRIBE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.NEWSLETTER_SUBSCRIBE,
affectedUserId: userConatct.user.id,
actingUserId: userConatct.user.id,
}),
)
})
})
})
describe('unsubscribeNewsletter', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({
mutation: unsubscribeNewsletter,
})
expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')])
})
})
describe('authenticated', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
it('calls API', async () => {
const {
data: { unsubscribeNewsletter: isUnsubscribed },
}: { data: { unsubscribeNewsletter: boolean } } = await mutate({
mutation: unsubscribeNewsletter,
})
expect(isUnsubscribed).toEqual(true)
})
it('stores the NEWSLETTER_UNSUBSCRIBE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.NEWSLETTER_UNSUBSCRIBE,
affectedUserId: userConatct.user.id,
actingUserId: userConatct.user.id,
}),
)
})
})
})
})

View File

@ -1,33 +1,17 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */ import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql'
import { Resolver, Query, Authorized, Arg, Mutation, Ctx } from 'type-graphql'
import { import { unsubscribe, klicktippSignIn } from '@/apis/KlicktippController'
getKlickTippUser,
getKlicktippTagMap,
unsubscribe,
klicktippSignIn,
} from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
@Resolver() @Resolver()
export class KlicktippResolver { export class KlicktippResolver {
@Authorized([RIGHTS.GET_KLICKTIPP_USER])
@Query(() => String)
async getKlicktippUser(@Arg('email') email: string): Promise<string> {
return await getKlickTippUser(email)
}
@Authorized([RIGHTS.GET_KLICKTIPP_TAG_MAP])
@Query(() => String)
async getKlicktippTagMap(): Promise<string> {
return await getKlicktippTagMap()
}
@Authorized([RIGHTS.UNSUBSCRIBE_NEWSLETTER]) @Authorized([RIGHTS.UNSUBSCRIBE_NEWSLETTER])
@Mutation(() => Boolean) @Mutation(() => Boolean)
async unsubscribeNewsletter(@Ctx() context: Context): Promise<boolean> { async unsubscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
const user = getUser(context) const user = getUser(context)
await EVENT_NEWSLETTER_UNSUBSCRIBE(user)
return unsubscribe(user.emailContact.email) return unsubscribe(user.emailContact.email)
} }
@ -35,6 +19,7 @@ export class KlicktippResolver {
@Mutation(() => Boolean) @Mutation(() => Boolean)
async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> { async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
const user = getUser(context) const user = getUser(context)
await EVENT_NEWSLETTER_SUBSCRIBE(user)
return klicktippSignIn(user.emailContact.email, user.language) return klicktippSignIn(user.emailContact.email, user.language)
} }
} }

View File

@ -7,8 +7,7 @@ import { MiddlewareFn } from 'type-graphql'
import { KlickTipp } from '@model/KlickTipp' import { KlickTipp } from '@model/KlickTipp'
import { /* klicktippSignIn, */ getKlickTippUser } from '@/apis/KlicktippController' import { getKlickTippUser } from '@/apis/KlicktippController'
import { CONFIG } from '@/config'
import { klickTippLogger as logger } from '@/server/logger' import { klickTippLogger as logger } from '@/server/logger'
// export const klicktippRegistrationMiddleware: MiddlewareFn = async ( // export const klicktippRegistrationMiddleware: MiddlewareFn = async (
@ -32,15 +31,13 @@ export const klicktippNewsletterStateMiddleware: MiddlewareFn = async (
// eslint-disable-next-line n/callback-return // eslint-disable-next-line n/callback-return
const result = await next() const result = await next()
let klickTipp = new KlickTipp({ status: 'Unsubscribed' }) let klickTipp = new KlickTipp({ status: 'Unsubscribed' })
if (CONFIG.KLICKTIPP) { try {
try { const klickTippUser = await getKlickTippUser(result.email)
const klickTippUser = await getKlickTippUser(result.email) if (klickTippUser) {
if (klickTippUser) { klickTipp = new KlickTipp(klickTippUser)
klickTipp = new KlickTipp(klickTippUser)
}
} catch (err) {
logger.error(`There is no user for (email='${result.email}') ${err}`)
} }
} catch (err) {
logger.error(`There is no user for (email='${result.email}') ${err}`)
} }
result.klickTipp = klickTipp result.klickTipp = klickTipp
return result return result

View File

@ -1,14 +1,14 @@
import { gql } from 'graphql-tag' import { gql } from 'graphql-tag'
export const subscribeNewsletter = gql` export const subscribeNewsletter = gql`
mutation ($email: String!, $language: String!) { mutation {
subscribeNewsletter(email: $email, language: $language) subscribeNewsletter
} }
` `
export const unsubscribeNewsletter = gql` export const unsubscribeNewsletter = gql`
mutation ($email: String!) { mutation {
unsubscribeNewsletter(email: $email) unsubscribeNewsletter
} }
` `

View File

@ -0,0 +1,60 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm'
@Entity('communities')
export class Community extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
foreign: boolean
@Column({ name: 'url', length: 255, nullable: false })
url: string
@Column({ name: 'public_key', type: 'binary', length: 64, nullable: false })
publicKey: Buffer
@Column({
name: 'community_uuid',
type: 'char',
length: 36,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
communityUuid: string | null
@Column({ name: 'authenticated_at', type: 'datetime', nullable: true })
authenticatedAt: Date | null
@Column({ name: 'name', type: 'varchar', length: 40, nullable: true })
name: string | null
@Column({ name: 'description', type: 'varchar', length: 255, nullable: true })
description: string | null
@CreateDateColumn({ name: 'creation_date', type: 'datetime', nullable: true })
creationDate: Date | null
@CreateDateColumn({
name: 'created_at',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP(3)',
nullable: false,
})
createdAt: Date
@UpdateDateColumn({
name: 'updated_at',
type: 'datetime',
onUpdate: 'CURRENT_TIMESTAMP(3)',
nullable: true,
})
updatedAt: Date | null
}

View File

@ -0,0 +1,51 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm'
@Entity('federated_communities')
export class FederatedCommunity extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
foreign: boolean
@Column({ name: 'public_key', type: 'binary', length: 64, default: null, nullable: true })
publicKey: Buffer
@Column({ name: 'api_version', length: 10, nullable: false })
apiVersion: string
@Column({ name: 'end_point', length: 255, nullable: false })
endPoint: string
@Column({ name: 'last_announced_at', type: 'datetime', nullable: true })
lastAnnouncedAt: Date | null
@Column({ name: 'verified_at', type: 'datetime', nullable: true })
verifiedAt: Date | null
@Column({ name: 'last_error_at', type: 'datetime', nullable: true })
lastErrorAt: Date | null
@CreateDateColumn({
name: 'created_at',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP(3)',
nullable: false,
})
createdAt: Date
@UpdateDateColumn({
name: 'updated_at',
type: 'datetime',
onUpdate: 'CURRENT_TIMESTAMP(3)',
nullable: true,
})
updatedAt: Date | null
}

View File

@ -1 +1 @@
export { Community } from './0060-update_communities_table/Community' export { Community } from './0065-refactor_communities_table/Community'

View File

@ -0,0 +1 @@
export { FederatedCommunity } from './0065-refactor_communities_table/FederatedCommunity'

View File

@ -10,6 +10,7 @@ import { Contribution } from './Contribution'
import { Event } from './Event' import { Event } from './Event'
import { ContributionMessage } from './ContributionMessage' import { ContributionMessage } from './ContributionMessage'
import { Community } from './Community' import { Community } from './Community'
import { FederatedCommunity } from './FederatedCommunity'
export const entities = [ export const entities = [
Community, Community,
@ -17,6 +18,7 @@ export const entities = [
ContributionLink, ContributionLink,
ContributionMessage, ContributionMessage,
Event, Event,
FederatedCommunity,
LoginElopageBuys, LoginElopageBuys,
LoginEmailOptIn, LoginEmailOptIn,
Migration, Migration,

View File

@ -0,0 +1,36 @@
/* MIGRATION TO CREATE THE FEDERATION COMMUNITY TABLES
*
* This migration creates the `community` and 'communityfederation' tables in the `apollo` database (`gradido_community`).
*/
/* 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(`RENAME TABLE communities TO federated_communities;`)
await queryFn(`
CREATE TABLE communities (
\`id\` int unsigned NOT NULL AUTO_INCREMENT,
\`foreign\` tinyint(4) NOT NULL DEFAULT 1,
\`url\` varchar(255) NOT NULL,
\`public_key\` binary(64) NOT NULL,
\`community_uuid\` char(36) NULL,
\`authenticated_at\` datetime(3) NULL,
\`name\` varchar(40) NULL,
\`description\` varchar(255) NULL,
\`creation_date\` datetime(3) NULL,
\`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
\`updated_at\` datetime(3),
PRIMARY KEY (id),
UNIQUE KEY url_key (url),
UNIQUE KEY uuid_key (community_uuid),
UNIQUE KEY public_key_key (public_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// write downgrade logic as parameter of queryFn
await queryFn(`DROP TABLE communities;`)
await queryFn(`RENAME TABLE federated_communities TO communities;`)
}

View File

@ -3,7 +3,7 @@ import dotenv from 'dotenv'
dotenv.config() dotenv.config()
const constants = { const constants = {
DB_VERSION: '0064-event_rename', DB_VERSION: '0065-refactor_communities_table',
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',

View File

@ -5,7 +5,7 @@ import { startDHT } from './index'
import DHT from '@hyperswarm/dht' import DHT from '@hyperswarm/dht'
import CONFIG from '@/config' import CONFIG from '@/config'
import { logger } from '@test/testSetup' import { logger } from '@test/testSetup'
import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { testEnvironment, cleanDB } from '@test/helpers' import { testEnvironment, cleanDB } from '@test/helpers'
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f' CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
@ -261,7 +261,7 @@ describe('federation', () => {
describe('with receiving wrong but tolerated property data', () => { describe('with receiving wrong but tolerated property data', () => {
let jsonArray: any[] let jsonArray: any[]
let result: DbCommunity[] = [] let result: DbFederatedCommunity[] = []
beforeAll(async () => { beforeAll(async () => {
jest.clearAllMocks() jest.clearAllMocks()
jsonArray = [ jsonArray = [
@ -277,7 +277,7 @@ describe('federation', () => {
}, },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find({ foreign: true }) result = await DbFederatedCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -523,7 +523,7 @@ describe('federation', () => {
describe('with receiving data of exact max allowed properties length', () => { describe('with receiving data of exact max allowed properties length', () => {
let jsonArray: any[] let jsonArray: any[]
let result: DbCommunity[] = [] let result: DbFederatedCommunity[] = []
beforeAll(async () => { beforeAll(async () => {
jest.clearAllMocks() jest.clearAllMocks()
jsonArray = [ jsonArray = [
@ -538,7 +538,7 @@ describe('federation', () => {
{ api: 'toolong api', url: 'some valid url' }, { api: 'toolong api', url: 'some valid url' },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find({ foreign: true }) result = await DbFederatedCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -570,7 +570,7 @@ describe('federation', () => {
describe('with receiving data of exact max allowed buffer length', () => { describe('with receiving data of exact max allowed buffer length', () => {
let jsonArray: any[] let jsonArray: any[]
let result: DbCommunity[] = [] let result: DbFederatedCommunity[] = []
beforeAll(async () => { beforeAll(async () => {
jest.clearAllMocks() jest.clearAllMocks()
jsonArray = [ jsonArray = [
@ -592,7 +592,7 @@ describe('federation', () => {
}, },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find({ foreign: true }) result = await DbFederatedCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -711,7 +711,7 @@ describe('federation', () => {
}) })
describe('with proper data', () => { describe('with proper data', () => {
let result: DbCommunity[] = [] let result: DbFederatedCommunity[] = []
beforeAll(async () => { beforeAll(async () => {
jest.clearAllMocks() jest.clearAllMocks()
await socketEventMocks.data( await socketEventMocks.data(
@ -728,7 +728,7 @@ describe('federation', () => {
]), ]),
), ),
) )
result = await DbCommunity.find({ foreign: true }) result = await DbFederatedCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {

View File

@ -3,7 +3,7 @@
import DHT from '@hyperswarm/dht' import DHT from '@hyperswarm/dht'
import { logger } from '@/server/logger' import { logger } from '@/server/logger'
import CONFIG from '@/config' import CONFIG from '@/config'
import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
const KEY_SECRET_SEEDBYTES = 32 const KEY_SECRET_SEEDBYTES = 32
const getSeed = (): Buffer | null => const getSeed = (): Buffer | null =>
@ -31,7 +31,7 @@ export const startDHT = async (topic: string): Promise<void> => {
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
const ownApiVersions = await writeHomeCommunityEnries(keyPair.publicKey) const ownApiVersions = await writeFederatedHomeCommunityEnries(keyPair.publicKey)
logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`) logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair }) const node = new DHT({ keyPair })
@ -92,9 +92,9 @@ export const startDHT = async (topic: string): Promise<void> => {
} }
logger.debug(`upsert with variables=${JSON.stringify(variables)}`) logger.debug(`upsert with variables=${JSON.stringify(variables)}`)
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement // this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
await DbCommunity.createQueryBuilder() await DbFederatedCommunity.createQueryBuilder()
.insert() .insert()
.into(DbCommunity) .into(DbFederatedCommunity)
.values(variables) .values(variables)
.orUpdate({ .orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
@ -179,7 +179,7 @@ export const startDHT = async (topic: string): Promise<void> => {
} }
} }
async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> { async function writeFederatedHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) { const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) {
const comApi: CommunityApi = { const comApi: CommunityApi = {
api: apiEnum, api: apiEnum,
@ -189,17 +189,17 @@ async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
}) })
try { try {
// first remove privious existing homeCommunity entries // first remove privious existing homeCommunity entries
DbCommunity.createQueryBuilder().delete().where({ foreign: false }).execute() DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
homeApiVersions.forEach(async function (homeApi) { homeApiVersions.forEach(async function (homeApi) {
const homeCom = new DbCommunity() const homeCom = new DbFederatedCommunity()
homeCom.foreign = false homeCom.foreign = false
homeCom.apiVersion = homeApi.api homeCom.apiVersion = homeApi.api
homeCom.endPoint = homeApi.url homeCom.endPoint = homeApi.url
homeCom.publicKey = pubKey.toString('hex') homeCom.publicKey = pubKey.toString('hex')
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement // this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
await DbCommunity.insert(homeCom) await DbFederatedCommunity.insert(homeCom)
logger.info(`federation home-community inserted successfully: ${JSON.stringify(homeCom)}`) logger.info(`federation home-community inserted successfully: ${JSON.stringify(homeCom)}`)
}) })
} catch (err) { } catch (err) {

View File

@ -50,6 +50,52 @@ Before starting in describing the details of the federation handshake, some prer
With the federation additional data tables/entities have to be created. With the federation additional data tables/entities have to be created.
##### 1st Draft
The following diagramms shows the first draft of the possible database-model base on the migration 0063-event_link_fields.ts with 3 steps of migration to reach the required entities. All three diagramms are not exhaustive and are still a base for discussions:
![img](./image/classdiagramm_x-community-readyness_step1.svg)
In the first step the current communities table will be renamed to communities_federation. A new table communities is created. Because of the dynamic in the communities_federation data during dht-federation the relation between both entities will be on the collumn communities.communities_federation_public_key. This relation will allow to read a community-entry including its relation to the multi federation entries per api-version with the public key as identifier.
![img](./image/classdiagramm_x-community-readyness_step2.svg)
The 2nd step is an introduction of the entity accounts between the users and the transactions table. This will cause a separation of the transactions from the users, to avoid possible conflicts or dependencies between local users of the community and remote users of foreign users, who will be part of x-communitiy-transactions.
![img](./image/classdiagramm_x-community-readyness_step3.svg)
The 3rd step will introduce an additional foreign-users and a users_favorites table. A foreign_user could be stored in the existing users-table, but he will not need all the attributes of a home-user, especially he will never gets an AGE-account in this community. The user_favorites entity is designed to buildup the relations between users and foreign_users or in general between all users. This is simply a first idea for a future discussion.
##### 2nd Draft
After team discussion in architecture meeting a second vision draft for database migration is shown in the following picture. Only the concerned tables of the database migration are presented. The three elliptical surroundings shows the different steps, which should be done in separate issues. The model, table namings and columns are not exhaustive and are still a base for further discussions.
![img](./image/class-diagramm_vision-draft2.svg)
**The first step** with renaming the current `communities` table in `communities_federation` and creating a new `communities` table is not changed. More details about motivation and arguments are described above.
**The second step** is changed to migrate the `users `table by creating a new `users_settings` table and shift the most existing attributes from `users `table to it. The criterium for shifting a column to `user_settings` is to keep only those columns in the `users `table, which will be used for "home-users" and "foreign-users". A foreign-user at this point of time is a user of an other community, who is involved in a x-community-transaction as `linked_user`. He will not have the possibility to login to the home-community, because after a x-community-tx only the attributes of the `users `table will be exchanged during the handshake of transaction processing of both communities. Even the `transactions `table will be ready for x-community-tx with this `users `and `user_settings` migration, because it contains a reference to both participants of the transaction. For easier displaying and because of historical reasons it will be a good idea to add the columns `linked_user `and `user `(not shown in diagramm) to the `transactions `table with type varchar(512) to store the valid firstname and lastname of the participants at transaction-date. If one of the participants will change his names, there will be no migration of the transaction data necessary and a transaction-list will present always the currect names even over a long distance.
**The third step** contains a migration for handling accounts and user_correlations. With the new table `gdd_accounts `a new entity will be introduced to support the different types of gradido accounts AGE, GMW and AUF. The second new table `user_correlations `is to buildup the different correlations a user will have:
* user to user correlation like favorites, trustee, etc
* user to account correlation for cashier of a GMW and AUF community account, trustee of children or seniors, etc.
The previous idea to replace the `user_id `with an `account_id` in the `transactions `table will be not necessary with this draft, because it will not cause a benefit and saves a lot refactoring efforts.
##### 3rd Draft
After further discussions about the database-model and the necessary migration steps the team decided to integrate an additional migration step for X-Community-Transaction. The decision base on keeping the goal focus on implementation of the x-community sendCoins feature as soon as possible.
![img](./image/class-diagramm_vision-draft3.svg)
The additional migration step 2 will simply concentrated on the `transactions `table by adding all necessary columns to handle a *x-community-tx* without a previous migration of the `users `table, shown as step 3 in the picture above.
In concequence of these additional columns in the `transactions `table the database-model will be denormalized by containing redundanten columns. But this migration step will reduce the necessary efforts to reach the *x-community sendCoins* feature as soon as possible. On the other side it offers more possibilities to ensure data consitency, because of internal data checks in conjunction with the redundant columns.
The feature *x-community-tx* per *sendCoins* will create several challenges, because there is no technical transaction bracket, which will ensure consistent data in both community databases after all write access are finished. The most favorite concept about handling a x-community-transaction and the upcoming handshake to ensure valid send- and receive-transaction entries in both databases is to follow the two-phase-commit protocol. To avoid blocking the transactions table or user dependent transactions-entries during the two-phase-commit processing the idea of pending_transactions is born. This additional pending_transactions table contains the same columns than the transactions table plus one column named `x_transactions_state`.
##### Community-Entity ##### Community-Entity
Create the new *Community* table to store attributes of the own community. This table is used more like a frame for own community data in the future like the list of federated foreign communities, own users, own futher accounts like AUF- and Welfare-account and the profile data of the own community: Create the new *Community* table to store attributes of the own community. This table is used more like a frame for own community data in the future like the list of federated foreign communities, own users, own futher accounts like AUF- and Welfare-account and the profile data of the own community:

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 301 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 300 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 300 KiB

View File

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

View File

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer' import createServer from '@/server/createServer'
import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
let query: any let query: any
@ -13,7 +13,7 @@ beforeAll(async () => {
const server = await createServer() const server = await createServer()
con = server.con con = server.con
query = createTestClient(server.apollo).query query = createTestClient(server.apollo).query
DbCommunity.clear() DbFederatedCommunity.clear()
}) })
afterAll(async () => { afterAll(async () => {
@ -32,12 +32,12 @@ describe('PublicKeyResolver', () => {
describe('getPublicKey', () => { describe('getPublicKey', () => {
beforeEach(async () => { beforeEach(async () => {
const homeCom = new DbCommunity() const homeCom = new DbFederatedCommunity()
homeCom.foreign = false homeCom.foreign = false
homeCom.apiVersion = '1_0' homeCom.apiVersion = '1_0'
homeCom.endPoint = 'endpoint-url' homeCom.endPoint = 'endpoint-url'
homeCom.publicKey = Buffer.from('homeCommunity-publicKey') homeCom.publicKey = Buffer.from('homeCommunity-publicKey')
await DbCommunity.insert(homeCom) await DbFederatedCommunity.insert(homeCom)
}) })
it('returns homeCommunity-publicKey', async () => { it('returns homeCommunity-publicKey', async () => {

View File

@ -1,7 +1,7 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Query, Resolver } from 'type-graphql' import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger' import { federationLogger as logger } from '@/server/logger'
import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { GetPublicKeyResult } from '../model/GetPublicKeyResult' import { GetPublicKeyResult } from '../model/GetPublicKeyResult'
@Resolver() @Resolver()
@ -10,7 +10,7 @@ export class PublicKeyResolver {
@Query(() => GetPublicKeyResult) @Query(() => GetPublicKeyResult)
async getPublicKey(): Promise<GetPublicKeyResult> { async getPublicKey(): Promise<GetPublicKeyResult> {
logger.debug(`getPublicKey() via apiVersion=1_0 ...`) logger.debug(`getPublicKey() via apiVersion=1_0 ...`)
const homeCom = await DbCommunity.findOneOrFail({ const homeCom = await DbFederatedCommunity.findOneOrFail({
foreign: false, foreign: false,
apiVersion: '1_0', apiVersion: '1_0',
}) })