Merge branch 'master' into add_yarn_build_on_root

This commit is contained in:
Hannes Heine 2023-03-23 11:12:35 +01:00 committed by GitHub
commit f7f9f28ce3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 810 additions and 398 deletions

View File

@ -40,4 +40,7 @@ federation: &federation
- 'federation/**/*'
frontend: &frontend
- 'frontend/**/*'
- 'frontend/**/*'
nginx: &nginx
- 'nginx/**/*'

32
.github/workflows/test-nginx.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Gradido Nginx Test CI
on: push
jobs:
files-changed:
name: Detect File Changes - Nginx
runs-on: ubuntu-latest
outputs:
nginx: ${{ steps.changes.outputs.nginx }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for nginx file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build_test_nginx:
name: Docker Build Test - Nginx
if: needs.files-changed.outputs.nginx == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: nginx | Build 'test' image
run: docker build -t "gradido/nginx:test" nginx/

View File

@ -81,31 +81,6 @@ jobs:
name: docker-mariadb-test
path: /tmp/mariadb.tar
##############################################################################
# JOB: DOCKER BUILD TEST NGINX ###############################################
##############################################################################
build_test_nginx:
name: Docker Build Test - Nginx
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# BUILD NGINX DOCKER IMAGE ###############################################
##########################################################################
- name: nginx | Build `test` image
run: |
docker build -t "gradido/nginx:test" nginx/
docker save "gradido/nginx:test" > /tmp/nginx.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-nginx-test
path: /tmp/nginx.tar
##############################################################################
# JOB: LINT BACKEND ##########################################################
##############################################################################

View File

@ -1,43 +1,75 @@
import { mount } from '@vue/test-utils'
import CreationTransactionList from './CreationTransactionList'
import { toastErrorSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { adminListContributions } from '../graphql/adminListContributions'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
localVue.use(VueApollo)
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
creationTransactionList: {
const defaultData = () => {
return {
adminListContributions: {
contributionCount: 2,
contributionList: [
{
id: 1,
amount: 5.8,
createdAt: '2022-09-21T11:09:51.000Z',
confirmedAt: null,
contributionDate: '2022-08-01T00:00:00.000Z',
memo: 'für deine Hilfe, Fräulein Rottenmeier',
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
{
id: 2,
amount: '47',
createdAt: '2022-09-21T11:09:28.000Z',
confirmedAt: '2022-09-21T11:09:28.000Z',
contributionDate: '2022-08-01T00:00:00.000Z',
memo: 'für deine Hilfe, Frau Holle',
state: 'CONFIRMED',
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: new Date(),
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
],
},
},
})
}
}
const mocks = {
$d: jest.fn((t) => t),
$t: jest.fn((t) => t),
$apollo: {
query: apolloQueryMock,
},
}
const propsData = {
@ -48,55 +80,53 @@ const propsData = {
describe('CreationTransactionList', () => {
let wrapper
const adminListContributionsMock = jest.fn()
mockClient.setRequestHandler(
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
const Wrapper = () => {
return mount(CreationTransactionList, { localVue, mocks, propsData })
return mount(CreationTransactionList, { localVue, mocks, propsData, apolloProvider })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('sends query to Apollo when created', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
currentPage: 1,
pageSize: 10,
order: 'DESC',
userId: 1,
},
}),
)
})
it('has two values for the transaction', () => {
expect(wrapper.find('tbody').findAll('tr').length).toBe(2)
})
describe('query transaction with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalled()
})
describe('server error', () => {
it('toast error', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
describe('watch currentPage', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setData({ currentPage: 2 })
describe('sever success', () => {
it('sends query to Apollo when created', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
pageSize: 10,
order: 'DESC',
userId: 1,
})
})
it('returns the string in normal order if reversed property is not true', () => {
expect(wrapper.vm.currentPage).toBe(2)
it('has two values for the transaction', () => {
expect(wrapper.find('tbody').findAll('tr').length).toBe(2)
})
describe('watch currentPage', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setData({ currentPage: 2 })
})
it('returns the string in normal order if reversed property is not true', () => {
expect(wrapper.vm.currentPage).toBe(2)
})
})
})
})

View File

@ -42,7 +42,7 @@
</div>
</template>
<script>
import { creationTransactionList } from '../graphql/creationTransactionList'
import { adminListContributions } from '../graphql/adminListContributions'
export default {
name: 'CreationTransactionList',
props: {
@ -92,33 +92,26 @@ export default {
],
}
},
methods: {
getTransactions() {
this.$apollo
.query({
query: creationTransactionList,
variables: {
currentPage: this.currentPage,
pageSize: this.perPage,
order: 'DESC',
userId: parseInt(this.userId),
},
})
.then((result) => {
this.rows = result.data.creationTransactionList.contributionCount
this.items = result.data.creationTransactionList.contributionList
})
.catch((error) => {
this.toastError(error.message)
})
},
},
created() {
this.getTransactions()
},
watch: {
currentPage() {
this.getTransactions()
apollo: {
AdminListContributions: {
query() {
return adminListContributions
},
variables() {
return {
currentPage: this.currentPage,
pageSize: this.perPage,
order: 'DESC',
userId: parseInt(this.userId),
}
},
update({ adminListContributions }) {
this.rows = adminListContributions.contributionCount
this.items = adminListContributions.contributionList
},
error({ message }) {
this.toastError(message)
},
},
},
}

View File

@ -1,17 +1,19 @@
import gql from 'graphql-tag'
export const adminListAllContributions = gql`
export const adminListContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$statusFilter: [ContributionStatus!]
$userId: Int
) {
adminListAllContributions(
adminListContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
statusFilter: $statusFilter
userId: $userId
) {
contributionCount
contributionList {

View File

@ -1,23 +0,0 @@
import gql from 'graphql-tag'
export const creationTransactionList = gql`
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC, $userId: Int!) {
creationTransactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
userId: $userId
) {
contributionCount
contributionList {
id
amount
createdAt
confirmedAt
contributionDate
memo
state
}
}
}
`

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { denyContribution } from '../graphql/denyContribution'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
@ -38,7 +38,7 @@ const mocks = {
const defaultData = () => {
return {
adminListAllContributions: {
adminListContributions: {
contributionCount: 2,
contributionList: [
{
@ -92,14 +92,14 @@ const defaultData = () => {
describe('CreationConfirm', () => {
let wrapper
const adminListAllContributionsMock = jest.fn()
const adminListContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn()
const adminDenyContributionMock = jest.fn()
const confirmContributionMock = jest.fn()
mockClient.setRequestHandler(
adminListAllContributions,
adminListAllContributionsMock
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
@ -337,7 +337,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
@ -352,7 +352,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
@ -368,7 +368,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
@ -384,7 +384,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
@ -400,7 +400,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,

View File

@ -85,7 +85,7 @@
<script>
import Overlay from '../components/Overlay'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution'
@ -397,7 +397,7 @@ export default {
apollo: {
ListAllContributions: {
query() {
return adminListAllContributions
return adminListContributions
},
variables() {
return {
@ -407,11 +407,11 @@ export default {
}
},
fetchPolicy: 'no-cache',
update({ adminListAllContributions }) {
this.rows = adminListAllContributions.contributionCount
this.items = adminListAllContributions.contributionList
update({ adminListContributions }) {
this.rows = adminListContributions.contributionCount
this.items = adminListContributions.contributionList
if (this.statusFilter === FILTER_TAB_MAP[0]) {
this.$store.commit('setOpenCreations', adminListAllContributions.contributionCount)
this.$store.commit('setOpenCreations', adminListContributions.contributionCount)
}
},
error({ message }) {

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils'
import Overview from './Overview'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { toastErrorSpy } from '../../test/testSetup'
@ -30,7 +30,7 @@ const mocks = {
const defaultData = () => {
return {
adminListAllContributions: {
adminListContributions: {
contributionCount: 2,
contributionList: [
{
@ -84,11 +84,11 @@ const defaultData = () => {
describe('Overview', () => {
let wrapper
const adminListAllContributionsMock = jest.fn()
const adminListContributionsMock = jest.fn()
mockClient.setRequestHandler(
adminListAllContributions,
adminListAllContributionsMock
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
@ -109,8 +109,8 @@ describe('Overview', () => {
})
})
it('calls the adminListAllContributions query', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
it('calls the adminListContributions query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,

View File

@ -31,7 +31,7 @@
</div>
</template>
<script>
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
export default {
name: 'overview',
@ -43,7 +43,7 @@ export default {
apollo: {
AllContributions: {
query() {
return adminListAllContributions
return adminListContributions
},
variables() {
// may be at some point we need a pagination here
@ -51,8 +51,8 @@ export default {
statusFilter: this.statusFilter,
}
},
update({ adminListAllContributions }) {
this.$store.commit('setOpenCreations', adminListAllContributions.contributionCount)
update({ adminListContributions }) {
this.$store.commit('setOpenCreations', adminListContributions.contributionCount)
},
error({ message }) {
this.toastError(message)

View File

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

View File

@ -2,7 +2,6 @@ import { RIGHTS } from './RIGHTS'
export const INALIENABLE_RIGHTS = [
RIGHTS.LOGIN,
RIGHTS.GET_COMMUNITY_INFO,
RIGHTS.COMMUNITIES,
RIGHTS.CREATE_USER,
RIGHTS.SEND_RESET_PASSWORD_EMAIL,

View File

@ -2,7 +2,6 @@ export enum RIGHTS {
LOGIN = 'LOGIN',
VERIFY_LOGIN = 'VERIFY_LOGIN',
BALANCE = 'BALANCE',
GET_COMMUNITY_INFO = 'GET_COMMUNITY_INFO',
COMMUNITIES = 'COMMUNITIES',
LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES',
EXIST_PID = 'EXIST_PID',
@ -44,10 +43,9 @@ export enum RIGHTS {
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
ADMIN_LIST_CONTRIBUTIONS = 'ADMIN_LIST_CONTRIBUTIONS',
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',

View File

@ -1,33 +1,47 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field, Int } from 'type-graphql'
import { Community as DbCommunity } from '@entity/Community'
@ObjectType()
export class Community {
constructor(json?: any) {
if (json) {
this.id = Number(json.id)
this.name = json.name
this.url = json.url
this.description = json.description
this.registerUrl = json.registerUrl
}
constructor(dbCom: DbCommunity) {
this.id = dbCom.id
this.foreign = dbCom.foreign
this.publicKey = dbCom.publicKey.toString()
this.url =
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') +
'api/' +
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)
name: string
publicKey: string
@Field(() => String)
url: string
@Field(() => String)
description: string
@Field(() => Date, { nullable: true })
lastAnnouncedAt: Date | null
@Field(() => String)
registerUrl: string
@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,24 +1,25 @@
/* 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 { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import CONFIG from '@/config'
jest.mock('@/config')
import { getCommunities } from '@/seeds/graphql/queries'
import { Community as DbCommunity } from '@entity/Community'
import { testEnvironment } from '@test/helpers'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
let testEnv: any
beforeAll(async () => {
const server = await createServer({})
con = server.con
query = createTestClient(server.apollo).query
testEnv = await testEnvironment()
query = testEnv.query
con = testEnv.con
await DbCommunity.clear()
})
afterAll(async () => {
@ -26,74 +27,90 @@ afterAll(async () => {
})
describe('CommunityResolver', () => {
const getCommunityInfoQuery = `
query {
getCommunityInfo {
name
description
url
registerUrl
}
}
`
const communities = `
query {
communities {
id
name
url
description
registerUrl
}
}
`
describe('getCommunityInfo', () => {
it('returns the default values', async () => {
await expect(query({ query: getCommunityInfoQuery })).resolves.toMatchObject({
data: {
getCommunityInfo: {
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register',
describe('getCommunities', () => {
let homeCom1: DbCommunity
let homeCom2: DbCommunity
let homeCom3: DbCommunity
let foreignCom1: DbCommunity
let foreignCom2: DbCommunity
let foreignCom3: DbCommunity
describe('with empty list', () => {
it('returns no community entry', async () => {
// const result: Community[] = await query({ query: getCommunities })
// expect(result.length).toEqual(0)
await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: {
getCommunities: [],
},
},
})
})
})
})
describe('communities', () => {
describe('PRODUCTION = false', () => {
beforeEach(() => {
CONFIG.PRODUCTION = false
describe('only home-communities entries', () => {
beforeEach(async () => {
jest.clearAllMocks()
homeCom1 = DbCommunity.create()
homeCom1.foreign = false
homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom1.apiVersion = '1_0'
homeCom1.endPoint = 'http://localhost'
homeCom1.createdAt = new Date()
await DbCommunity.insert(homeCom1)
homeCom2 = DbCommunity.create()
homeCom2.foreign = false
homeCom2.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom2.apiVersion = '1_1'
homeCom2.endPoint = 'http://localhost'
homeCom2.createdAt = new Date()
await DbCommunity.insert(homeCom2)
homeCom3 = DbCommunity.create()
homeCom3.foreign = false
homeCom3.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom3.apiVersion = '2_0'
homeCom3.endPoint = 'http://localhost'
homeCom3.createdAt = new Date()
await DbCommunity.insert(homeCom3)
})
it('returns three communities', async () => {
await expect(query({ query: communities })).resolves.toMatchObject({
it('returns three home-community entries', async () => {
await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: {
communities: [
getCommunities: [
{
id: 1,
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register-community',
foreign: homeCom1.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom1.createdAt.toISOString(),
updatedAt: null,
},
{
id: 2,
name: 'Gradido Staging',
description: 'Der Testserver der Gradido-Akademie.',
url: 'https://stage1.gradido.net/',
registerUrl: 'https://stage1.gradido.net/register-community',
foreign: homeCom2.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_1'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom2.createdAt.toISOString(),
updatedAt: null,
},
{
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
foreign: homeCom3.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/2_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom3.createdAt.toISOString(),
updatedAt: null,
},
],
},
@ -101,21 +118,104 @@ describe('CommunityResolver', () => {
})
})
describe('PRODUCTION = true', () => {
beforeEach(() => {
CONFIG.PRODUCTION = true
describe('plus foreign-communities entries', () => {
beforeEach(async () => {
jest.clearAllMocks()
foreignCom1 = DbCommunity.create()
foreignCom1.foreign = true
foreignCom1.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom1.apiVersion = '1_0'
foreignCom1.endPoint = 'http://remotehost'
foreignCom1.createdAt = new Date()
await DbCommunity.insert(foreignCom1)
foreignCom2 = DbCommunity.create()
foreignCom2.foreign = true
foreignCom2.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom2.apiVersion = '1_1'
foreignCom2.endPoint = 'http://remotehost'
foreignCom2.createdAt = new Date()
await DbCommunity.insert(foreignCom2)
foreignCom3 = DbCommunity.create()
foreignCom3.foreign = true
foreignCom3.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom3.apiVersion = '1_2'
foreignCom3.endPoint = 'http://remotehost'
foreignCom3.createdAt = new Date()
await DbCommunity.insert(foreignCom3)
})
it('returns one community', async () => {
await expect(query({ query: communities })).resolves.toMatchObject({
it('returns 3x home and 3x foreign-community entries', async () => {
await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: {
communities: [
getCommunities: [
{
id: 1,
foreign: homeCom1.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom1.createdAt.toISOString(),
updatedAt: null,
},
{
id: 2,
foreign: homeCom2.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_1'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom2.createdAt.toISOString(),
updatedAt: null,
},
{
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
foreign: homeCom3.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/2_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom3.createdAt.toISOString(),
updatedAt: null,
},
{
id: 4,
foreign: foreignCom1.foreign,
publicKey: expect.stringMatching('publicKey-ForeignCommunity'),
url: expect.stringMatching('http://remotehost/api/1_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: foreignCom1.createdAt.toISOString(),
updatedAt: null,
},
{
id: 5,
foreign: foreignCom2.foreign,
publicKey: expect.stringMatching('publicKey-ForeignCommunity'),
url: expect.stringMatching('http://remotehost/api/1_1'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: foreignCom2.createdAt.toISOString(),
updatedAt: null,
},
{
id: 6,
foreign: foreignCom3.foreign,
publicKey: expect.stringMatching('publicKey-ForeignCommunity'),
url: expect.stringMatching('http://remotehost/api/1_2'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: foreignCom3.createdAt.toISOString(),
updatedAt: null,
},
],
},

View File

@ -1,58 +1,18 @@
import { Resolver, Query, Authorized } from 'type-graphql'
import { Community } from '@model/Community'
import { Community as DbCommunity } from '@entity/Community'
import { RIGHTS } from '@/auth/RIGHTS'
import CONFIG from '@/config'
@Resolver()
export class CommunityResolver {
@Authorized([RIGHTS.GET_COMMUNITY_INFO])
@Query(() => Community)
getCommunityInfo(): Community {
return new Community({
name: CONFIG.COMMUNITY_NAME,
description: CONFIG.COMMUNITY_DESCRIPTION,
url: CONFIG.COMMUNITY_URL,
registerUrl: CONFIG.COMMUNITY_REGISTER_URL,
})
}
@Authorized([RIGHTS.COMMUNITIES])
@Query(() => [Community])
communities(): Community[] {
if (CONFIG.PRODUCTION)
return [
new Community({
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
}),
]
return [
new Community({
id: 1,
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register-community',
}),
new Community({
id: 2,
name: 'Gradido Staging',
description: 'Der Testserver der Gradido-Akademie.',
url: 'https://stage1.gradido.net/',
registerUrl: 'https://stage1.gradido.net/register-community',
}),
new Community({
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
}),
]
async getCommunities(): Promise<Community[]> {
const dbCommunities: DbCommunity[] = await DbCommunity.find({
order: { foreign: 'ASC', publicKey: 'ASC', apiVersion: 'ASC' },
})
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
}
}

View File

@ -27,7 +27,7 @@ import {
import {
listAllContributions,
listContributions,
adminListAllContributions,
adminListContributions,
} from '@/seeds/graphql/queries'
import {
sendContributionConfirmedEmail,
@ -2669,12 +2669,12 @@ describe('ContributionResolver', () => {
})
})
describe('adminListAllContribution', () => {
describe('adminListContributions', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: adminListAllContributions,
query: adminListContributions,
}),
).resolves.toEqual(
expect.objectContaining({
@ -2699,7 +2699,7 @@ describe('ContributionResolver', () => {
it('returns an error', async () => {
await expect(
query({
query: adminListAllContributions,
query: adminListContributions,
}),
).resolves.toEqual(
expect.objectContaining({
@ -2723,9 +2723,9 @@ describe('ContributionResolver', () => {
it('returns 17 creations in total', async () => {
const {
data: { adminListAllContributions: contributionListObject },
}: { data: { adminListAllContributions: ContributionListResult } } = await query({
query: adminListAllContributions,
data: { adminListContributions: contributionListObject },
}: { data: { adminListContributions: ContributionListResult } } = await query({
query: adminListContributions,
})
expect(contributionListObject.contributionList).toHaveLength(17)
expect(contributionListObject).toMatchObject({
@ -2890,9 +2890,9 @@ describe('ContributionResolver', () => {
it('returns two pending creations with page size set to 2', async () => {
const {
data: { adminListAllContributions: contributionListObject },
}: { data: { adminListAllContributions: ContributionListResult } } = await query({
query: adminListAllContributions,
data: { adminListContributions: contributionListObject },
}: { data: { adminListContributions: ContributionListResult } } = await query({
query: adminListContributions,
variables: {
currentPage: 1,
pageSize: 2,

View File

@ -136,15 +136,15 @@ export class ContributionResolver {
): Promise<ContributionListResult> {
const user = getUser(context)
const [dbContributions, count] = await findContributions(
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
true,
['messages'],
user.id,
withDeleted: true,
relations: ['messages'],
userId: user.id,
statusFilter,
)
})
return new ContributionListResult(
count,
dbContributions.map((contribution) => new Contribution(contribution, user)),
@ -159,15 +159,13 @@ export class ContributionResolver {
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions(
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
false,
['user'],
undefined,
relations: ['user'],
statusFilter,
)
})
return new ContributionListResult(
count,
@ -384,23 +382,25 @@ export class ContributionResolver {
return result
}
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
@Query(() => ContributionListResult) // [UnconfirmedContribution]
async adminListAllContributions(
@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,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions(
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
true,
['user', 'messages'],
undefined,
withDeleted: true,
userId,
relations: ['user', 'messages'],
statusFilter,
)
})
return new ContributionListResult(
count,
@ -562,33 +562,6 @@ export class ContributionResolver {
return true
}
@Authorized([RIGHTS.CREATION_TRANSACTION_LIST])
@Query(() => ContributionListResult)
async creationTransactionList(
@Args()
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
@Arg('userId', () => Int) userId: number,
): Promise<ContributionListResult> {
const offset = (currentPage - 1) * pageSize
const [contributionResult, count] = await getConnection()
.createQueryBuilder()
.select('c')
.from(DbContribution, 'c')
.leftJoinAndSelect('c.user', 'u')
.where(`user_id = ${userId}`)
.withDeleted()
.limit(pageSize)
.offset(offset)
.orderBy('c.created_at', order)
.getManyAndCount()
return new ContributionListResult(
count,
contributionResult.map((contribution) => new Contribution(contribution, contribution.user)),
)
// return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
}
@Authorized([RIGHTS.OPEN_CREATIONS])
@Query(() => [OpenCreation])
async openCreations(@Ctx() context: Context): Promise<OpenCreation[]> {

View File

@ -3,21 +3,30 @@ import { Order } from '@enum/Order'
import { Contribution as DbContribution } from '@entity/Contribution'
import { In } from '@dbTools/typeorm'
interface FindContributionsOptions {
order: Order
currentPage: number
pageSize: number
withDeleted?: boolean
relations?: string[]
userId?: number | null
statusFilter?: ContributionStatus[] | null
}
export const findContributions = async (
order: Order,
currentPage: number,
pageSize: number,
withDeleted: boolean,
relations: string[],
userId?: number,
statusFilter?: ContributionStatus[] | null,
): Promise<[DbContribution[], number]> =>
DbContribution.findAndCount({
options: FindContributionsOptions,
): Promise<[DbContribution[], number]> => {
const { order, currentPage, pageSize, withDeleted, relations, userId, statusFilter } = {
withDeleted: false,
relations: [],
...options,
}
return DbContribution.findAndCount({
where: {
...(statusFilter && statusFilter.length && { contributionStatus: In(statusFilter) }),
...(userId && { userId }),
},
withDeleted: withDeleted,
withDeleted,
order: {
createdAt: order,
id: order,
@ -26,3 +35,4 @@ export const findContributions = async (
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
}

View File

@ -133,6 +133,22 @@ export const communities = gql`
}
`
export const getCommunities = gql`
query {
getCommunities {
id
foreign
publicKey
url
lastAnnouncedAt
verifiedAt
lastErrorAt
createdAt
updatedAt
}
}
`
export const queryTransactionLink = gql`
query ($code: String!) {
queryTransactionLink(code: $code) {
@ -204,18 +220,20 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
`
// from admin interface
export const adminListAllContributions = gql`
export const adminListContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$statusFilter: [ContributionStatus!]
$userId: Int
) {
adminListAllContributions(
adminListContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
statusFilter: $statusFilter
userId: $userId
) {
contributionCount
contributionList {

View File

@ -25,13 +25,13 @@ export class Community extends BaseEntity {
endPoint: string
@Column({ name: 'last_announced_at', type: 'datetime', nullable: true })
lastAnnouncedAt: Date
lastAnnouncedAt: Date | null
@Column({ name: 'verified_at', type: 'datetime', nullable: true })
verifiedAt: Date
verifiedAt: Date | null
@Column({ name: 'last_error_at', type: 'datetime', nullable: true })
lastErrorAt: Date
lastErrorAt: Date | null
@CreateDateColumn({
name: 'created_at',

View File

@ -4,29 +4,6 @@ This document contains the concept and technical details for the *federation* of
But meanwhile the usage of a DHT like HyperSwarm promises more coverage of the gradido requirements out of the box. More details about HyperSwarm can be found here [@hyperswarm/dht](https://github.com/hyperswarm/dht).
## ActivityPub (deprecated)
The activity pub defines a server-to-server federation protocol to share information between decentralized instances and will be the main komponent for the gradido community federation.
At first we asume a *gradido community* as an *ActivityPub user*. A user is represented by "*actors*" via the users's accounts on servers. User's accounts on different servers corrsponds to different actors, which means community accounts on different servers corrsponds to different communities.
Every community (actor) has an:
* inbox: to get messages from the world
* outbox: to send messages to others
and are simple endpoints or just URLs, which are described in the *ActivityStream* of each *ActivityPub community*.
### Open Decision:
It has to be decided, if the Federation will work with an internal or with external ActivityPub-Server, as shown in the picture below:
![FederationActivityPub](./image/FederationActivityPub.png " ")
The Variant A with an internal server contains the benefit to be as independent as possible from third party service providers and will not cause additional hosting costs. But this solution will cause the additional efforts of impementing an ActivityPub-Server in the gradido application and the responsibility for this component.
The Varaint B with an external server contains the benefit to reduce the implementation efforts and the responsibility for an own ActivitPub-Server. But it will cause an additional dependency to a third party service provider and the growing hosting costs.
## HyperSwarm
The decision to switch from ActivityPub to HyperSwarm base on the arguments, that the *hyperswarm/dht* library will satify the most federation requirements out of the box. It is now to design the business requirements of the [gradido community communication](../BusinessRequirements/CommunityVerwaltung.md#UC-createCommunity) in a technical conception.
@ -41,12 +18,30 @@ To enable such a relationship between an existing community and a new community
2. Authentication
3. Autorized Communication
### Overview
### Overview of Federation-Handshake
At first the following diagramm gives an overview of the three stages and shows the handshake between an existing community-A and a new created community-B including the data exchange for buildup such a federated, authenticated and autorized relationship.
![FederationHyperSwarm.png](./image/FederationHyperSwarm.png)
### Technical Architecture
The previous described handshake will be done by several technical modules of the gradido system. The following picture gives an overview about the modules and how the communicate with each other.
![img](./image/TechnicalOverview_V1-19.svg)
As soon as a Gradido Community is up and running the DHT-Modul first writes the home-community-entries in the database and starts with the federation via HyperSwarm. Each community, which is configured with the configuration key GRADIDO_HUB to listen on the correct topic of the DHT will be part of the Gradido-Net-Federation. That means each DHT-Modul of each community will receive the publicKey of all connected communities. The DHT-Modul will open for each received publicKey a communication-socket with the associated community DHT-Modul. Over this open socket the connected communities exchange the data "api-version" and "url" for later direct communication between both communities. The exchanged api-version info and urls will be written in the own database.
The background of this exchanged data base on the supported api-versions a community will support with its own federation-modules. Each running federation-module in a community will support exact one graphql api-version of a cross-community-communication. To reach a dedicated federation-module with the correct api-version during a cross-community-communication the url for this federation-module must be known by both communities. As shown in the picture above the graphql-client with api-version V1_0 in the left community will interact with the federation-module with api-version V1_0 on the right community. During the lifecycle of the gradido-application it will be necessary to extent the features and interfaces for the cross-community-communication. To keep a backwards compatibilty and not to force each community to always upgrade their running software version on the last api-version at the same time, it will be necessary to support several api-versions in parallel. The different running api-version modules are responsible to convert and treat the exchanged data in a correct way to ensure konsistent data in the local database of the community.
The up and running Backend-Module contains a validation logic to verify the community entries from the own DHT-Module. For each announced but unverified community-entry the GraphQL-Client is used to invoke a getPublicKey-Request. Depending on the containing api-version the matching GraphQL-Client is used and the getPublicKey-Request will be send to the given URL.
As soon as the federation-module of the associated community received the getPublicKey-request the own publicKey is read from database and send back in the response.
The GraphQL-Client will read the publicKey of the other community from the returned response data and compare it with the data of the community-entry, which caused the getPublicKey-Request. If they match the community-entry will be updated by inserting the current timestamp in the verifiedAt-field of this community-entry.
This federation and verification logic will work the whole time and can be monitored by observing the communities-table changes. The Admin-UI will contain a Page to have a look on the current state of the communities table content.
### Prerequisits
Before starting in describing the details of the federation handshake, some prerequisits have to be defined.
@ -235,7 +230,6 @@ For the first federation release the *DHT-Node* will be part of the *apollo serv
| communityApiVersion.apiversion | keep existing value |
| communityApiVersion.validFrom | exchangedData.API.validFrom |
| communityApiVersion.verifiedAt | keep existing value |
*
3. After all received data is stored successfully, the *DHT-Node* starts the *stage2 - Authentication* of the federation handshake
### Stage2 - Authentication
@ -284,8 +278,6 @@ As soon the *openConnection* request is invoked:
3. check if the decrypted `parameter.signedAndEncryptedURL` is equals the selected url from the previous selected CommunityFederationEntry
1. if not then break the further processing of this request by only writing an error-log event. There will be no answer to the invoker community, because this community will only go on with a `openConnectionRedirect`-request from this community.
2. if yes then verify the signature of `parameter.signedAndEncryptedURL` with the `cf.pubKey` read in step 2 before
3.
4.
### Stage3 - Autorized Business Communication

View File

@ -0,0 +1,282 @@
<mxfile host="65bd71144e">
<diagram id="RqE3izjX3TYt3HTUOB95" name="Seite-1">
<mxGraphModel dx="3343" dy="773" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Community&amp;nbsp; &quot;Gradido-Akademie&quot;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;verticalAlign=top;fontSize=16;fontStyle=1;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-158.26" y="80" width="870" height="800" as="geometry"/>
</mxCell>
<mxCell id="3" value="Gradido - technical Infrastructure-Overview&lt;br&gt;&lt;font style=&quot;font-size: 12px&quot;&gt;State of 02.2023&lt;/font&gt;" style="text;html=1;strokeColor=#82b366;fillColor=#d5e8d4;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;labelBorderColor=none;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-1160" y="20" width="1880" height="40" as="geometry"/>
</mxCell>
<mxCell id="4" value="&lt;b&gt;Backend-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="-118.25999999999999" y="269" width="540" height="101" as="geometry"/>
</mxCell>
<mxCell id="7" value="CommunityServer DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=15;fontStyle=1;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="211.74" y="760" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="8" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="7" target="4" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="191.74" y="590" as="sourcePoint"/>
<mxPoint x="241.74" y="540" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="15" value="&lt;b&gt;Layer 1:&lt;/b&gt;" style="text;html=1;strokeColor=#6c8ebf;fillColor=#dae8fc;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;opacity=0;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-148.26" y="735" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="16" value="" style="endArrow=none;dashed=1;html=1;fontSize=15;fontColor=#000000;entryX=1.002;entryY=0.801;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" target="2" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-158.26" y="720" as="sourcePoint"/>
<mxPoint x="603.74" y="720" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="17" value="&lt;b&gt;Layer 2:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-148.26" y="240" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="21" value="&quot;&lt;b&gt;GDT-Server&lt;/b&gt;&quot; &lt;br&gt;base on cakephp + mySQL" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#fff2cc;strokeColor=#d6b656;gradientColor=#ffd966;" parent="1" vertex="1">
<mxGeometry x="491.74" y="269" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="22" value="GDT-Server DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=15;fontStyle=1;gradientColor=#ffd966;" parent="1" vertex="1">
<mxGeometry x="521.74" y="760" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="23" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="22" target="21" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="381.74" y="590" as="sourcePoint"/>
<mxPoint x="586.6200000000001" y="370" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="24" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;fontColor=#000000;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" target="21" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="421.74" y="299" as="sourcePoint"/>
<mxPoint x="401.74" y="479" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="25" value="&lt;font style=&quot;font-size: 12px&quot;&gt;json-&lt;br&gt;ajax-&lt;br&gt;request&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=15;fontColor=#000000;labelBackgroundColor=none;" parent="24" vertex="1" connectable="0">
<mxGeometry x="-0.3429" relative="1" as="geometry">
<mxPoint x="10" y="-28" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="30" value="" style="endArrow=none;dashed=1;html=1;fontSize=15;fontColor=#000000;entryX=1.002;entryY=0.401;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0;exitY=0.4;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-159.9999999999999" y="230" as="sourcePoint"/>
<mxPoint x="711.7399999999999" y="230.79999999999995" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="31" value="&lt;b&gt;Layer 3:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-148.26" y="90" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="32" value="&quot;&lt;b&gt;Elopage&lt;/b&gt;&quot; &lt;br&gt;external Service-Portal" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="471.74" y="120" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="33" value="&quot;&lt;b&gt;User-UI&lt;/b&gt;&quot;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-48.25999999999999" y="120" width="200" height="60" as="geometry"/>
</mxCell>
<mxCell id="34" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;fontColor=#000000;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.315;exitY=-0.05;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="4" target="33" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="351.74" y="420" as="sourcePoint"/>
<mxPoint x="401.74" y="370" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="35" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;fontColor=#000000;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.395;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="21" target="32" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="576.24" y="310" as="sourcePoint"/>
<mxPoint x="576.24" y="180" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="36" value="&lt;span style=&quot;font-size: 12px&quot;&gt;graphql&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=15;fontColor=#000000;labelBackgroundColor=none;" parent="1" vertex="1" connectable="0">
<mxGeometry x="191.74" y="216" as="geometry">
<mxPoint x="-169" y="-4" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="37" value="&lt;font style=&quot;font-size: 12px&quot;&gt;json-&lt;br&gt;request&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=15;fontColor=#000000;labelBackgroundColor=none;" parent="1" vertex="1" connectable="0">
<mxGeometry x="551.74" y="216" as="geometry">
<mxPoint x="-2" y="-15" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="39" style="edgeStyle=none;html=1;entryX=0.767;entryY=-0.017;entryDx=0;entryDy=0;startArrow=classic;startFill=1;entryPerimeter=0;" parent="1" source="38" target="4" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="40" value="graphql" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" parent="39" vertex="1" connectable="0">
<mxGeometry x="-0.4305" y="1" relative="1" as="geometry">
<mxPoint x="23" y="-1" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="38" value="&quot;&lt;b&gt;Admin-UI&lt;/b&gt;&quot;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="191.74" y="120" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="42" style="edgeStyle=none;html=1;entryX=0.145;entryY=0;entryDx=0;entryDy=4.35;entryPerimeter=0;startArrow=classic;startFill=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="41" target="7" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="41" value="&lt;b&gt;DHT-Modul&lt;/b&gt;&lt;br&gt;HyperSwarm" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-118.25999999999999" y="640" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="102" style="edgeStyle=none;html=1;fontSize=16;startArrow=classic;startFill=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="43" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="260" y="760" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="43" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V2_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;dashed=1;" parent="1" vertex="1">
<mxGeometry x="-118.25999999999999" y="380" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="101" style="edgeStyle=none;html=1;fontSize=16;startArrow=classic;startFill=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="44" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="260" y="760" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="44" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_x" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;dashed=1;" parent="1" vertex="1">
<mxGeometry x="-88.25999999999999" y="430" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="100" style="edgeStyle=none;html=1;entryX=0.335;entryY=0.013;entryDx=0;entryDy=0;entryPerimeter=0;fontSize=16;startArrow=classic;startFill=1;exitX=0.78;exitY=0.667;exitDx=0;exitDy=0;exitPerimeter=0;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="45" target="7" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="45" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_1" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;dashed=1;" parent="1" vertex="1">
<mxGeometry x="-58.25999999999999" y="480" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="47" style="edgeStyle=none;html=1;entryX=0.333;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;startArrow=classic;startFill=1;exitX=0.996;exitY=0.633;exitDx=0;exitDy=0;exitPerimeter=0;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="46" target="7" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="46" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-28.25999999999999" y="530" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="48" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-108.25999999999999" y="289" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="49" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-98.25999999999999" y="299" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="50" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-88.25999999999999" y="309" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="51" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-78.25999999999999" y="319" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="52" value="Community &quot;GallischesDorf-TBB&quot;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;fontStyle=1;fontSize=16;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-1148.26" y="80" width="628.26" height="800" as="geometry"/>
</mxCell>
<mxCell id="54" value="&lt;b&gt;Backend-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="-1108.26" y="269" width="568.26" height="101" as="geometry"/>
</mxCell>
<mxCell id="55" value="CommunityServer DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=15;fontStyle=1;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-1048.26" y="770" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="56" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="55" target="54" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-798.26" y="590" as="sourcePoint"/>
<mxPoint x="-748.26" y="540" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="57" value="&lt;b&gt;Layer 1:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-1138.26" y="735" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="58" value="" style="endArrow=none;dashed=1;html=1;fontSize=15;fontColor=#000000;entryX=1.002;entryY=0.801;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" target="52" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-1148.26" y="720" as="sourcePoint"/>
<mxPoint x="-386.26" y="720" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="59" value="&lt;b&gt;Layer 2:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-1138.26" y="240" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="65" value="" style="endArrow=none;dashed=1;html=1;fontSize=15;fontColor=#000000;exitX=0;exitY=0.4;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-1149.9999999999998" y="230" as="sourcePoint"/>
<mxPoint x="-520" y="231" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="66" value="&lt;b&gt;Layer 3:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-1138.26" y="90" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="68" value="&quot;&lt;b&gt;User-UI&lt;/b&gt;&quot;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-1030" y="120" width="200" height="60" as="geometry"/>
</mxCell>
<mxCell id="69" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;fontColor=#000000;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.315;exitY=-0.05;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="54" target="68" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-638.26" y="420" as="sourcePoint"/>
<mxPoint x="-588.26" y="370" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="71" value="&lt;span style=&quot;font-size: 12px&quot;&gt;graphql&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=15;fontColor=#000000;labelBackgroundColor=none;" parent="1" vertex="1" connectable="0">
<mxGeometry x="-798.26" y="216" as="geometry">
<mxPoint x="-169" y="-4" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="73" style="edgeStyle=none;html=1;entryX=0.767;entryY=-0.017;entryDx=0;entryDy=0;startArrow=classic;startFill=1;entryPerimeter=0;" parent="1" source="75" target="54" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="74" value="graphql" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" parent="73" vertex="1" connectable="0">
<mxGeometry x="-0.4305" y="1" relative="1" as="geometry">
<mxPoint x="23" y="-1" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="75" value="&quot;&lt;b&gt;Admin-UI&lt;/b&gt;&quot;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-775" y="120" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="76" style="edgeStyle=none;html=1;entryX=0.855;entryY=0;entryDx=0;entryDy=4.35;entryPerimeter=0;startArrow=classic;startFill=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;" parent="1" source="77" target="55" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="90" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="77" target="41" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="91" value="&amp;nbsp;DHT-Socket Communication&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="90" vertex="1" connectable="0">
<mxGeometry x="-0.1307" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="77" value="&lt;b&gt;DHT-Modul&lt;/b&gt;&lt;br&gt;HyperSwarm" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-745" y="640" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="87" style="edgeStyle=none;html=1;entryX=0.655;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;startArrow=classic;startFill=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;" parent="1" source="80" target="55" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="80" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_1" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;dashed=1;" parent="1" vertex="1">
<mxGeometry x="-760" y="500" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="81" style="edgeStyle=none;html=1;entryX=0.655;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;startArrow=classic;startFill=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;" parent="1" source="82" target="55" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="82" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-730" y="550" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="98" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;startArrow=classic;startFill=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="85" target="45" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="99" value="graphQL-Handshake" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="98" vertex="1" connectable="0">
<mxGeometry x="-0.5775" relative="1" as="geometry">
<mxPoint x="28" y="-14" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="85" value="GraphQL-Client V1_1" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-745" y="289" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="96" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;startArrow=classic;startFill=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="86" target="46" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="97" value="graphQL-Handshake" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="96" vertex="1" connectable="0">
<mxGeometry x="-0.5322" y="-1" relative="1" as="geometry">
<mxPoint x="-11" y="16" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="86" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-730" y="314" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="92" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;startArrow=classic;startFill=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="51" target="82" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="94" value="graphQL-Handshake" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="92" vertex="1" connectable="0">
<mxGeometry x="0.1267" y="2" relative="1" as="geometry">
<mxPoint x="-93" y="56" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="93" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;startArrow=classic;startFill=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="50" target="80" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="95" value="graphQL-Handshake" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="93" vertex="1" connectable="0">
<mxGeometry x="-0.0706" relative="1" as="geometry">
<mxPoint x="63" y="-60" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -0,0 +1,53 @@
/* 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'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
beforeAll(async () => {
const server = await createServer()
con = server.con
query = createTestClient(server.apollo).query
DbCommunity.clear()
})
afterAll(async () => {
await con.close()
})
describe('PublicKeyResolver', () => {
const getPublicKeyQuery = `
query {
getPublicKey
{
publicKey
}
}
`
describe('getPublicKey', () => {
beforeEach(async () => {
const homeCom = new DbCommunity()
homeCom.foreign = false
homeCom.apiVersion = '1_0'
homeCom.endPoint = 'endpoint-url'
homeCom.publicKey = Buffer.from('homeCommunity-publicKey')
await DbCommunity.insert(homeCom)
})
it('returns homeCommunity-publicKey', async () => {
await expect(query({ query: getPublicKeyQuery })).resolves.toMatchObject({
data: {
getPublicKey: {
publicKey: expect.stringMatching('homeCommunity-publicKey'),
},
},
})
})
})
})

View File

@ -9,12 +9,12 @@ import { GetPublicKeyResult } from '../model/GetPublicKeyResult'
export class PublicKeyResolver {
@Query(() => GetPublicKeyResult)
async getPublicKey(): Promise<GetPublicKeyResult> {
logger.info(`getPublicKey()...`)
logger.debug(`getPublicKey() via apiVersion=1_0 ...`)
const homeCom = await DbCommunity.findOneOrFail({
foreign: false,
apiVersion: '1_0',
})
logger.info(`getPublicKey()... with publicKey=${homeCom.publicKey}`)
logger.info(`getPublicKey()-1_0... return publicKey=${homeCom.publicKey}`)
return new GetPublicKeyResult(homeCom.publicKey.toString())
}
}