Merge branch 'master' into update_user_settings_gms

This commit is contained in:
einhornimmond 2024-03-04 18:06:48 +01:00 committed by GitHub
commit aadfd23777
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1176 additions and 206 deletions

View File

@ -9,7 +9,7 @@ module.exports = {
],
coverageThreshold: {
global: {
lines: 96,
lines: 95,
},
},
moduleFileExtensions: [
@ -31,6 +31,9 @@ module.exports = {
setupFiles: ['<rootDir>/test/testSetup.js', 'jest-canvas-mock'],
testMatch: ['**/?(*.)+(spec|test).js?(x)'],
// snapshotSerializers: ['jest-serializer-vue'],
transformIgnorePatterns: ['<rootDir>/node_modules/(?!vee-validate/dist/rules)'],
transformIgnorePatterns: [
'<rootDir>/node_modules/(?!vee-validate/dist/rules)',
'/node_modules/(?!@babel)',
],
testEnvironment: 'jest-environment-jsdom-sixteen', // why this is still needed? should not be needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen
}

View File

@ -16,6 +16,7 @@
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "cross-env TZ=UTC jest",
"test:debug": "node --inspect-brk node_modules/.bin/vue-cli-service test:unit --no-cache --watch --runInBand",
"locales": "scripts/sort.sh"
},
"dependencies": {

View File

@ -1,63 +0,0 @@
<template>
<div class="federation-visualize-item">
<b-row>
<b-col cols="1"><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
<b-col>
<div>{{ item.url }}</div>
<small>{{ `${item.publicKey.substring(0, 26)}` }}</small>
</b-col>
<b-col cols="2">{{ lastAnnouncedAt }}</b-col>
<b-col cols="2">{{ createdAt }}</b-col>
</b-row>
</div>
</template>
<script>
import { formatDistanceToNow } from 'date-fns'
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
const locales = { en, de, es, fr, nl }
export default {
name: 'FederationVisualizeItem',
props: {
item: { type: Object },
},
data() {
return {
formatDistanceToNow,
locale: this.$i18n.locale,
}
},
computed: {
verified() {
return new Date(this.item.verifiedAt) >= new Date(this.item.lastAnnouncedAt)
},
icon() {
return this.verified ? 'check' : 'x-circle'
},
variant() {
return this.verified ? 'success' : 'danger'
},
lastAnnouncedAt() {
if (this.item.lastAnnouncedAt) {
return formatDistanceToNow(new Date(this.item.lastAnnouncedAt), {
includeSecond: true,
addSuffix: true,
locale: locales[this.locale],
})
}
return ''
},
createdAt() {
if (this.item.createdAt) {
return formatDistanceToNow(new Date(this.item.createdAt), {
includeSecond: true,
addSuffix: true,
locale: locales[this.locale],
})
}
return ''
},
},
}
</script>

View File

@ -1,36 +1,83 @@
import { mount } from '@vue/test-utils'
import FederationVisualizeItem from './FederationVisualizeItem.vue'
import Vuex from 'vuex'
import CommunityVisualizeItem from './CommunityVisualizeItem.vue'
const localVue = global.localVue
localVue.use(Vuex)
const today = new Date()
const createdDate = new Date()
createdDate.setDate(createdDate.getDate() - 3)
// Mock für den Vuex-Store
const store = new Vuex.Store({
state: {
moderator: {
roles: ['ADMIN'],
},
},
})
let propsData = {
item: {
id: 7590,
id: 1,
foreign: false,
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
url: 'http://localhost/api/2_0',
lastAnnouncedAt: createdDate,
verifiedAt: today,
lastErrorAt: null,
url: 'http://localhost/api/',
publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2',
communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21',
authenticatedAt: null,
name: 'Gradido Test',
description: 'Gradido Community zum testen',
gmsApiKey: '<api key>',
creationDate: createdDate,
createdAt: createdDate,
updatedAt: null,
updatedAt: createdDate,
federatedCommunities: [
{
id: 2046,
apiVersion: '2_0',
endPoint: 'http://localhost/api/',
lastAnnouncedAt: createdDate,
verifiedAt: today,
lastErrorAt: null,
createdAt: createdDate,
updatedAt: null,
},
{
id: 2045,
apiVersion: '1_1',
endPoint: 'http://localhost/api/',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: '2024-01-16T10:08:21.550Z',
updatedAt: null,
},
{
id: 2044,
apiVersion: '1_0',
endPoint: 'http://localhost/api/',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: '2024-01-16T10:08:21.544Z',
updatedAt: null,
},
],
},
}
const mocks = {
$t: (key) => key,
$i18n: {
locale: 'en',
},
}
describe('FederationVisualizeItem', () => {
describe('CommunityVisualizeItem', () => {
let wrapper
const Wrapper = () => {
return mount(FederationVisualizeItem, { localVue, mocks, propsData })
return mount(CommunityVisualizeItem, { localVue, mocks, propsData, store })
}
describe('mount', () => {
@ -39,19 +86,35 @@ describe('FederationVisualizeItem', () => {
})
it('renders the component', () => {
expect(wrapper.find('div.federation-visualize-item').exists()).toBe(true)
expect(wrapper.exists()).toBe(true)
expect(wrapper.find('div.community-visualize-item').exists()).toBe(true)
expect(wrapper.find('.details').exists()).toBe(false)
})
it('toggles details on click', async () => {
// Click the row to toggle details
await wrapper.find('.row').trigger('click')
// Assert that details are now open
expect(wrapper.find('.details').exists()).toBe(true)
// Click the row again to toggle details back
await wrapper.find('.row').trigger('click')
// Assert that details are now closed
expect(wrapper.find('.details').exists()).toBe(false)
})
describe('rendering item properties', () => {
it('has the url', () => {
expect(wrapper.find('.row > div:nth-child(2) > div').text()).toBe(
'http://localhost/api/2_0',
expect(wrapper.find('.row > div:nth-child(2) > div > a').text()).toBe(
'http://localhost/api/',
)
})
it('has the public key', () => {
expect(wrapper.find('.row > div:nth-child(2) > small').text()).toContain(
'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7'.substring(0, 26),
'4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2'.substring(0, 26),
)
})
@ -65,33 +128,6 @@ describe('FederationVisualizeItem', () => {
})
})
describe('not verified item', () => {
beforeEach(() => {
propsData = {
item: {
id: 7590,
foreign: false,
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
url: 'http://localhost/api/2_0',
lastAnnouncedAt: createdDate,
verifiedAt: null,
lastErrorAt: null,
createdAt: createdDate,
updatedAt: null,
},
}
wrapper = Wrapper()
})
it('has the x-circle icon', () => {
expect(wrapper.find('svg.bi-x-circle').exists()).toBe(true)
})
it('has the text variant "danger"', () => {
expect(wrapper.find('.text-danger').exists()).toBe(true)
})
})
// describe('with different locales (de, en, fr, es, nl)', () => {
describe('lastAnnouncedAt', () => {
it('computes the time string for different locales (de, en, fr, es, nl)', () => {
@ -155,6 +191,30 @@ describe('FederationVisualizeItem', () => {
expect(wrapper.vm.createdAt).toBe('3 dagen geleden')
})
describe('not verified item', () => {
beforeEach(() => {
propsData = {
item: {
id: 7590,
foreign: false,
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
url: 'http://localhost/api/',
createdAt: createdDate,
updatedAt: null,
},
}
wrapper = Wrapper()
})
it('has the x-circle icon', () => {
expect(wrapper.find('svg.bi-x-circle').exists()).toBe(true)
})
it('has the text variant "danger"', () => {
expect(wrapper.find('.text-danger').exists()).toBe(true)
})
})
describe('createdAt == null', () => {
beforeEach(() => {
propsData = {
@ -163,9 +223,9 @@ describe('FederationVisualizeItem', () => {
foreign: false,
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
url: 'http://localhost/api/2_0',
lastAnnouncedAt: createdDate,
verifiedAt: null,
lastErrorAt: null,
communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21',
authenticatedAt: null,
creationDate: null,
createdAt: null,
updatedAt: null,
},

View File

@ -0,0 +1,160 @@
<template>
<div class="community-visualize-item">
<b-row @click="toggleDetails">
<b-col cols="1"><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
<b-col>
<div>
<a :href="item.url" target="_blank">{{ item.url }}</a>
</div>
<small>{{ `${item.publicKey.substring(0, 26)}` }}</small>
</b-col>
<b-col v-b-tooltip="item.description">{{ item.name }}</b-col>
<b-col cols="2">{{ lastAnnouncedAt }}</b-col>
<b-col cols="2">{{ createdAt }}</b-col>
</b-row>
<b-row v-if="details" class="details">
<b-col colspan="5">
<b-list-group>
<b-list-group-item v-if="item.uuid">
{{ $t('federation.communityUuid') }}&nbsp;{{ item.uuid }}
</b-list-group-item>
<b-list-group-item v-if="item.authenticatedAt">
{{ $t('federation.authenticatedAt') }}&nbsp;{{ item.authenticatedAt }}
</b-list-group-item>
<b-list-group-item>
{{ $t('federation.publicKey') }}&nbsp;{{ item.publicKey }}
</b-list-group-item>
<b-list-group-item v-if="!item.foreign">
{{ $t('federation.gmsApiKey') }}&nbsp;
<editable-label
:value="gmsApiKey"
:allowEdit="$store.state.moderator.roles.includes('ADMIN')"
@save="handleSaveGsmApiKey"
/>
</b-list-group-item>
<b-list-group-item>
<b-list-group>
<b-row>
<b-col class="ml-1">{{ $t('federation.verified') }}</b-col>
<b-col>{{ $t('federation.apiVersion') }}</b-col>
<b-col>{{ $t('federation.createdAt') }}</b-col>
<b-col>{{ $t('federation.lastAnnouncedAt') }}</b-col>
<b-col>{{ $t('federation.verifiedAt') }}</b-col>
<b-col>{{ $t('federation.lastErrorAt') }}</b-col>
</b-row>
<b-list-group-item
v-for="federation in item.federatedCommunities"
:key="federation.id"
:variant="!item.foreign ? 'primary' : 'warning'"
>
<federation-visualize-item :item="federation" />
</b-list-group-item>
</b-list-group>
</b-list-group-item>
</b-list-group>
</b-col>
</b-row>
</div>
</template>
<script>
import { formatDistanceToNow } from 'date-fns'
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
import EditableLabel from '@/components/input/EditableLabel'
import FederationVisualizeItem from './FederationVisualizeItem.vue'
import { updateHomeCommunity } from '../../graphql/updateHomeCommunity'
const locales = { en, de, es, fr, nl }
export default {
name: 'CommunityVisualizeItem',
components: {
EditableLabel,
FederationVisualizeItem,
},
props: {
item: { type: Object },
},
data() {
return {
formatDistanceToNow,
locale: this.$i18n.locale,
details: false,
gmsApiKey: '',
}
},
created() {
this.gmsApiKey = this.item.gmsApiKey
},
computed: {
verified() {
if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) {
return false
}
return (
this.item.federatedCommunities.filter(
(federatedCommunity) =>
new Date(federatedCommunity.verifiedAt) >= new Date(federatedCommunity.lastAnnouncedAt),
).length > 0
)
},
icon() {
return this.verified ? 'check' : 'x-circle'
},
variant() {
return this.verified ? 'success' : 'danger'
},
lastAnnouncedAt() {
if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) return ''
const minDate = new Date(0)
const lastAnnouncedAt = this.item.federatedCommunities.reduce(
(lastAnnouncedAt, federateCommunity) => {
if (!federateCommunity.lastAnnouncedAt) return lastAnnouncedAt
const date = new Date(federateCommunity.lastAnnouncedAt)
return date > lastAnnouncedAt ? date : lastAnnouncedAt
},
minDate,
)
if (lastAnnouncedAt !== minDate) {
return formatDistanceToNow(lastAnnouncedAt, {
includeSecond: true,
addSuffix: true,
locale: locales[this.locale],
})
}
return ''
},
createdAt() {
if (this.item.createdAt) {
return formatDistanceToNow(new Date(this.item.createdAt), {
includeSecond: true,
addSuffix: true,
locale: locales[this.locale],
})
}
return ''
},
},
methods: {
toggleDetails() {
this.details = !this.details
},
handleSaveGsmApiKey(gmsApiKey) {
this.gmsApiKey = gmsApiKey
this.$apollo
.mutate({
mutation: updateHomeCommunity,
variables: {
uuid: this.item.uuid,
gmsApiKey: gmsApiKey,
},
})
.then(() => {
this.toastSuccess(this.$t('federation.toast_gmsApiKeyUpdated'))
})
.catch((error) => {
this.toastError(error.message)
})
},
},
}
</script>

View File

@ -0,0 +1,47 @@
<template>
<div class="federation-visualize-item">
<b-row>
<b-col><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
<b-col class="ml-1">{{ item.apiVersion }}</b-col>
<b-col v-b-tooltip="item.createdAt">{{ distanceDate(item.createdAt) }}</b-col>
<b-col v-b-tooltip="item.lastAnnouncedAt">{{ distanceDate(item.lastAnnouncedAt) }}</b-col>
<b-col v-b-tooltip="item.verifiedAt">{{ distanceDate(item.verifiedAt) }}</b-col>
<b-col v-b-tooltip="item.lastErrorAt">{{ distanceDate(item.lastErrorAt) }}</b-col>
</b-row>
</div>
</template>
<script>
import { formatDistanceToNow } from 'date-fns'
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
const locales = { en, de, es, fr, nl }
export default {
name: 'FederationVisualizeItem',
props: {
item: { type: Object },
},
computed: {
verified() {
return new Date(this.item.verifiedAt) >= new Date(this.item.lastAnnouncedAt)
},
icon() {
return this.verified ? 'check' : 'x-circle'
},
variant() {
return this.verified ? 'success' : 'danger'
},
},
methods: {
distanceDate(dateString) {
return dateString
? formatDistanceToNow(new Date(dateString), {
includeSecond: true,
addSuffix: true,
locale: locales[this.$i18n.locale],
})
: ''
},
},
}
</script>

View File

@ -0,0 +1,83 @@
// Test written by ChatGPT 3.5
import { mount } from '@vue/test-utils'
import EditableLabel from './EditableLabel.vue'
const localVue = global.localVue
const value = 'test label value'
describe('EditableLabel', () => {
let wrapper
const createWrapper = (propsData) => {
return mount(EditableLabel, {
localVue,
propsData,
})
}
it('renders the label when not editing', () => {
wrapper = createWrapper({ value, allowEdit: true })
expect(wrapper.find('label').text()).toBe(value)
})
it('renders the input when editing', async () => {
wrapper = createWrapper({ value, allowEdit: true })
await wrapper.find('button').trigger('click')
expect(wrapper.find('input').exists()).toBe(true)
})
it('emits save event when clicking save button', async () => {
wrapper = createWrapper({ value, allowEdit: true })
await wrapper.find('button').trigger('click')
await wrapper.setData({ inputValue: 'New Value' })
await wrapper.find('button').trigger('click')
expect(wrapper.emitted().save).toBeTruthy()
expect(wrapper.emitted().save[0][0]).toBe('New Value')
})
it('disables save button when value is not changed', async () => {
wrapper = createWrapper({ value, allowEdit: true })
await wrapper.find('button').trigger('click')
expect(wrapper.find('button').attributes('disabled')).toBe('disabled')
})
it('enables save button when value is changed', async () => {
wrapper = createWrapper({ value, allowEdit: true })
await wrapper.find('button').trigger('click')
await wrapper.setData({ inputValue: 'New Value' })
expect(wrapper.find('button').attributes('disabled')).toBeFalsy()
})
it('updates originalValue when saving changes', async () => {
wrapper = createWrapper({ value, allowEdit: true })
await wrapper.find('button').trigger('click')
await wrapper.setData({ inputValue: 'New Value' })
await wrapper.find('button').trigger('click')
expect(wrapper.vm.originalValue).toBe('New Value')
})
it('changes variant to success when editing', async () => {
wrapper = createWrapper({ value, allowEdit: true })
await wrapper.find('button').trigger('click')
expect(wrapper.vm.variant).toBe('success')
})
it('changes variant to prime when not editing', async () => {
wrapper = createWrapper({ value, allowEdit: true })
expect(wrapper.vm.variant).toBe('prime')
})
})

View File

@ -0,0 +1,64 @@
<template>
<div>
<b-form-group>
<label v-if="!editing">{{ value }}</label>
<b-form-input v-else v-model="inputValue" :placeholder="placeholder" />
</b-form-group>
<b-button
v-if="allowEdit"
@click="toggleEdit"
:disabled="!isValueChanged && editing"
:variant="variant"
>
<b-icon v-if="!editing" icon="pencil-fill" tooltip="$t('edit')"></b-icon>
<b-icon v-else icon="check" tooltip="$t('save')"></b-icon>
</b-button>
</div>
</template>
<script>
export default {
// Code written from chatGPT 3.5
name: 'EditableLabel',
props: {
value: {
type: String,
required: false,
default: '',
},
allowEdit: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
required: false,
default: '',
},
},
data() {
return {
editing: false,
inputValue: this.value,
originalValue: this.value,
}
},
computed: {
variant() {
return this.editing ? 'success' : 'prime'
},
isValueChanged() {
return this.inputValue !== this.originalValue
},
},
methods: {
toggleEdit() {
if (this.editing) {
this.$emit('save', this.inputValue)
this.originalValue = this.inputValue
}
this.editing = !this.editing
},
},
}
</script>

View File

@ -0,0 +1,29 @@
import gql from 'graphql-tag'
export const allCommunities = gql`
query {
allCommunities {
foreign
url
publicKey
uuid
authenticatedAt
name
description
gmsApiKey
creationDate
createdAt
updatedAt
federatedCommunities {
id
apiVersion
endPoint
lastAnnouncedAt
verifiedAt
lastErrorAt
createdAt
updatedAt
}
}
}
`

View File

@ -1,17 +0,0 @@
import gql from 'graphql-tag'
export const getCommunities = gql`
query {
getCommunities {
id
foreign
publicKey
url
lastAnnouncedAt
verifiedAt
lastErrorAt
createdAt
updatedAt
}
}
`

View File

@ -0,0 +1,9 @@
import gql from 'graphql-tag'
export const updateHomeCommunity = gql`
mutation ($uuid: String!, $gmsApiKey: String!) {
updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey) {
id
}
}
`

View File

@ -73,11 +73,20 @@
"error": "Fehler",
"expired": "abgelaufen",
"federation": {
"apiVersion": "API Version",
"authenticatedAt": "Verifiziert am:",
"communityUuid": "Community UUID:",
"createdAt": "Erstellt am",
"gmsApiKey": "GMS API Key:",
"toast_gmsApiKeyUpdated": "Der GMS Api Key wurde erfolgreich aktualisiert!",
"gradidoInstances": "Gradido Instanzen",
"lastAnnouncedAt": "letzte Bekanntgabe",
"lastErrorAt": "Letzer Fehler am",
"name": "Name",
"publicKey": "PublicKey:",
"url": "Url",
"verified": "Verifiziert"
"verified": "Verifiziert",
"verifiedAt": "Verifiziert am"
},
"firstname": "Vorname",
"footer": {

View File

@ -73,11 +73,20 @@
"error": "Error",
"expired": "expired",
"federation": {
"apiVersion": "API Version",
"authenticatedAt": "verified at:",
"communityUuid": "Community UUID:",
"createdAt": "Created At ",
"gmsApiKey": "GMS API Key:",
"toast_gmsApiKeyUpdated": "The GMS Api Key has been successfully updated!",
"gradidoInstances": "Gradido Instances",
"lastAnnouncedAt": "Last Announced",
"lastErrorAt": "last error at",
"name": "Name",
"publicKey": "PublicKey:",
"url": "Url",
"verified": "Verified"
"verified": "Verified",
"verifiedAt": "Verified at"
},
"firstname": "Firstname",
"footer": {

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import FederationVisualize from './FederationVisualize'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { getCommunities } from '@/graphql/getCommunities'
import { allCommunities } from '@/graphql/allCommunities'
import { toastErrorSpy } from '../../test/testSetup'
const mockClient = createMockClient()
@ -25,42 +25,54 @@ const mocks = {
const defaultData = () => {
return {
getCommunities: [
allCommunities: [
{
id: 1776,
foreign: true,
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
url: 'http://localhost/api/2_0',
lastAnnouncedAt: '2023-04-07T12:27:24.037Z',
verifiedAt: null,
lastErrorAt: null,
createdAt: '2023-04-07T11:45:06.254Z',
updatedAt: null,
__typename: 'Community',
},
{
id: 1775,
foreign: true,
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
url: 'http://localhost/api/1_1',
lastAnnouncedAt: '2023-04-07T12:27:24.023Z',
verifiedAt: null,
lastErrorAt: null,
createdAt: '2023-04-07T11:45:06.234Z',
updatedAt: null,
__typename: 'Community',
},
{
id: 1774,
foreign: true,
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
url: 'http://localhost/api/1_0',
lastAnnouncedAt: '2023-04-07T12:27:24.009Z',
verifiedAt: null,
lastErrorAt: null,
createdAt: '2023-04-07T11:45:06.218Z',
updatedAt: null,
__typename: 'Community',
id: 1,
foreign: false,
url: 'http://localhost/api/',
publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2',
communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21',
authenticatedAt: null,
name: 'Gradido Test',
description: 'Gradido Community zum testen',
gmsApiKey: '<api key>',
creationDate: '2024-01-09T15:56:40.592Z',
createdAt: '2024-01-09T15:56:40.595Z',
updatedAt: '2024-01-16T11:17:15.000Z',
federatedCommunities: [
{
id: 2046,
apiVersion: '2_0',
endPoint: 'http://localhost/api/',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: '2024-01-16T10:08:21.544Z',
updatedAt: null,
},
{
id: 2045,
apiVersion: '1_1',
endPoint: 'http://localhost/api/',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: '2024-01-16T10:08:21.550Z',
updatedAt: null,
__typename: 'FederatedCommunity',
},
{
id: 2044,
apiVersion: '1_0',
endPoint: 'http://localhost/api/',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: '2024-01-16T10:08:21.544Z',
updatedAt: null,
__typename: 'FederatedCommunity',
},
],
},
],
}
@ -68,11 +80,11 @@ const defaultData = () => {
describe('FederationVisualize', () => {
let wrapper
const getCommunitiesMock = jest.fn()
const allCommunitiesMock = jest.fn()
mockClient.setRequestHandler(
getCommunities,
getCommunitiesMock
allCommunities,
allCommunitiesMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
@ -95,7 +107,7 @@ describe('FederationVisualize', () => {
describe('sever success', () => {
it('sends query to Apollo when created', () => {
expect(getCommunitiesMock).toBeCalled()
expect(allCommunitiesMock).toBeCalled()
})
it('has a DIV element with the class "federation-visualize"', () => {
@ -106,8 +118,8 @@ describe('FederationVisualize', () => {
expect(wrapper.find('[data-test="federation-communities-refresh-btn"]').exists()).toBe(true)
})
it('renders 3 community list items', () => {
expect(wrapper.findAll('.list-group-item').length).toBe(3)
it('renders 1 community list item', () => {
expect(wrapper.findAll('.list-group-item').length).toBe(1)
})
describe('cklicking the refresh button', () => {
@ -117,7 +129,7 @@ describe('FederationVisualize', () => {
})
it('calls the API', async () => {
expect(getCommunitiesMock).toBeCalled()
expect(allCommunitiesMock).toBeCalled()
})
})
})

View File

@ -7,7 +7,7 @@
icon="arrow-clockwise"
font-scale="2"
:animation="animation"
@click="$apollo.queries.GetCommunities.refresh()"
@click="$apollo.queries.allCommunities.refresh()"
data-test="federation-communities-refresh-btn"
></b-icon>
</b-button>
@ -16,28 +16,29 @@
<b-row>
<b-col cols="1" class="ml-1">{{ $t('federation.verified') }}</b-col>
<b-col class="ml-3">{{ $t('federation.url') }}</b-col>
<b-col class="ml-3">{{ $t('federation.name') }}</b-col>
<b-col cols="2">{{ $t('federation.lastAnnouncedAt') }}</b-col>
<b-col cols="2">{{ $t('federation.createdAt') }}</b-col>
</b-row>
<b-list-group-item
v-for="item in communities"
:key="item.id"
:key="item.publicKey"
:variant="!item.foreign ? 'primary' : 'warning'"
>
<federation-visualize-item :item="item" />
<community-visualize-item :item="item" />
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>
import { getCommunities } from '@/graphql/getCommunities'
import { allCommunities } from '@/graphql/allCommunities'
import FederationVisualizeItem from '../components/Fedaration/FederationVisualizeItem.vue'
import CommunityVisualizeItem from '../components/Federation/CommunityVisualizeItem.vue'
export default {
name: 'FederationVisualize',
components: {
FederationVisualizeItem,
CommunityVisualizeItem,
},
data() {
return {
@ -48,17 +49,17 @@ export default {
},
computed: {
animation() {
return this.$apollo.queries.GetCommunities.loading ? 'spin' : ''
return this.$apollo.queries.allCommunities.loading ? 'spin' : ''
},
},
apollo: {
GetCommunities: {
allCommunities: {
fetchPolicy: 'network-only',
query() {
return getCommunities
return allCommunities
},
update({ getCommunities }) {
this.communities = getCommunities
update({ allCommunities }) {
this.communities = allCommunities
},
error({ message }) {
this.toastError(message)

View File

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

View File

@ -0,0 +1,76 @@
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ObjectType, Field } from 'type-graphql'
import { FederatedCommunity } from './FederatedCommunity'
@ObjectType()
export class AdminCommunityView {
constructor(dbCom: DbCommunity) {
if (dbCom.federatedCommunities && dbCom.federatedCommunities.length > 0) {
const federatedCommunity = dbCom.federatedCommunities[0]
this.foreign = federatedCommunity.foreign
const url = new URL(federatedCommunity.endPoint)
// use only the host part
this.url = url.protocol + '//' + url.host
this.publicKey = federatedCommunity.publicKey.toString('hex')
this.federatedCommunities = dbCom.federatedCommunities.map(
(federatedCom: DbFederatedCommunity) => new FederatedCommunity(federatedCom),
)
}
if (dbCom.foreign !== undefined) {
this.foreign = dbCom.foreign
}
this.name = dbCom.name
this.description = dbCom.description
this.gmsApiKey = dbCom.gmsApiKey
if (dbCom.url) {
this.url = dbCom.url
}
if (dbCom.publicKey && dbCom.publicKey.length === 32) {
this.publicKey = dbCom.publicKey.toString('hex')
}
this.creationDate = dbCom.creationDate
this.createdAt = dbCom.createdAt
this.updatedAt = dbCom.updatedAt
this.uuid = dbCom.communityUuid
this.authenticatedAt = dbCom.authenticatedAt
this.gmsApiKey = dbCom.gmsApiKey
}
@Field(() => Boolean)
foreign: boolean
@Field(() => String)
url: string
@Field(() => String)
publicKey: string
@Field(() => String, { nullable: true })
uuid: string | null
@Field(() => Date, { nullable: true })
authenticatedAt: Date | null
@Field(() => String, { nullable: true })
name: string | null
@Field(() => String, { nullable: true })
description: string | null
@Field(() => String, { nullable: true })
gmsApiKey: string | null
@Field(() => Date, { nullable: true })
creationDate: Date | null
@Field(() => Date, { nullable: true })
createdAt: Date | null
@Field(() => Date, { nullable: true })
updatedAt: Date | null
@Field(() => [FederatedCommunity], { nullable: true })
federatedCommunities: FederatedCommunity[] | null
}

View File

@ -7,8 +7,8 @@ export class FederatedCommunity {
this.id = dbCom.id
this.foreign = dbCom.foreign
this.publicKey = dbCom.publicKey.toString('hex')
this.url =
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion
this.apiVersion = dbCom.apiVersion
this.endPoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
this.verifiedAt = dbCom.verifiedAt
this.lastErrorAt = dbCom.lastErrorAt
@ -26,7 +26,10 @@ export class FederatedCommunity {
publicKey: string
@Field(() => String)
url: string
apiVersion: string
@Field(() => String)
endPoint: string
@Field(() => Date, { nullable: true })
lastAnnouncedAt: Date | null

View File

@ -18,6 +18,7 @@ import { logger, i18n as localization } from '@test/testSetup'
import { userFactory } from '@/seeds/factory/user'
import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations'
import {
allCommunities,
getCommunities,
communitiesQuery,
getHomeCommunityQuery,
@ -170,7 +171,8 @@ describe('CommunityResolver', () => {
id: 3,
foreign: homeCom3.foreign,
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
url: expect.stringMatching('http://localhost/api/2_0'),
endPoint: expect.stringMatching('http://localhost/api/'),
apiVersion: '2_0',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
@ -181,7 +183,8 @@ describe('CommunityResolver', () => {
id: 2,
foreign: homeCom2.foreign,
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
url: expect.stringMatching('http://localhost/api/1_1'),
endPoint: expect.stringMatching('http://localhost/api/'),
apiVersion: '1_1',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
@ -192,7 +195,8 @@ describe('CommunityResolver', () => {
id: 1,
foreign: homeCom1.foreign,
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
url: expect.stringMatching('http://localhost/api/1_0'),
endPoint: expect.stringMatching('http://localhost/api/'),
apiVersion: '1_0',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
@ -228,7 +232,7 @@ describe('CommunityResolver', () => {
foreignCom3 = DbFederatedCommunity.create()
foreignCom3.foreign = true
foreignCom3.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex')
foreignCom3.apiVersion = '1_2'
foreignCom3.apiVersion = '2_0'
foreignCom3.endPoint = 'http://remotehost/api'
foreignCom3.createdAt = new Date()
await DbFederatedCommunity.insert(foreignCom3)
@ -242,7 +246,8 @@ describe('CommunityResolver', () => {
id: 3,
foreign: homeCom3.foreign,
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
url: expect.stringMatching('http://localhost/api/2_0'),
endPoint: expect.stringMatching('http://localhost/api/'),
apiVersion: '2_0',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
@ -253,7 +258,8 @@ describe('CommunityResolver', () => {
id: 2,
foreign: homeCom2.foreign,
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
url: expect.stringMatching('http://localhost/api/1_1'),
endPoint: expect.stringMatching('http://localhost/api/'),
apiVersion: '1_1',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
@ -264,7 +270,8 @@ describe('CommunityResolver', () => {
id: 1,
foreign: homeCom1.foreign,
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
url: expect.stringMatching('http://localhost/api/1_0'),
endPoint: expect.stringMatching('http://localhost/api/'),
apiVersion: '1_0',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
@ -275,7 +282,8 @@ describe('CommunityResolver', () => {
id: 6,
foreign: foreignCom3.foreign,
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public),
url: expect.stringMatching('http://remotehost/api/1_2'),
endPoint: expect.stringMatching('http://remotehost/api/'),
apiVersion: '2_0',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
@ -286,7 +294,8 @@ describe('CommunityResolver', () => {
id: 5,
foreign: foreignCom2.foreign,
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public),
url: expect.stringMatching('http://remotehost/api/1_1'),
endPoint: expect.stringMatching('http://remotehost/api/'),
apiVersion: '1_1',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
@ -297,7 +306,8 @@ describe('CommunityResolver', () => {
id: 4,
foreign: foreignCom1.foreign,
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public),
url: expect.stringMatching('http://remotehost/api/1_0'),
endPoint: expect.stringMatching('http://remotehost/api/'),
apiVersion: '1_0',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
@ -309,6 +319,229 @@ describe('CommunityResolver', () => {
})
})
})
describe('with 6 federated community entries', () => {
let comHomeCom1: DbCommunity
let comForeignCom1: DbCommunity
let comForeignCom2: DbCommunity
let foreignCom4: DbFederatedCommunity
beforeEach(async () => {
jest.clearAllMocks()
comHomeCom1 = DbCommunity.create()
comHomeCom1.foreign = false
comHomeCom1.url = 'http://localhost'
comHomeCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[0].public, 'hex')
comHomeCom1.privateKey = Buffer.from(ed25519KeyPairStaticHex[0].private, 'hex')
comHomeCom1.communityUuid = 'HomeCom-UUID'
comHomeCom1.authenticatedAt = new Date()
comHomeCom1.name = 'HomeCommunity-name'
comHomeCom1.description = 'HomeCommunity-description'
comHomeCom1.creationDate = new Date()
await DbCommunity.insert(comHomeCom1)
comForeignCom1 = DbCommunity.create()
comForeignCom1.foreign = true
comForeignCom1.url = 'http://stage-2.gradido.net'
comForeignCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[3].public, 'hex')
comForeignCom1.privateKey = Buffer.from(ed25519KeyPairStaticHex[3].private, 'hex')
// foreignCom1.communityUuid = 'Stage2-Com-UUID'
// foreignCom1.authenticatedAt = new Date()
comForeignCom1.name = 'Stage-2_Community-name'
comForeignCom1.description = 'Stage-2_Community-description'
comForeignCom1.creationDate = new Date()
await DbCommunity.insert(comForeignCom1)
comForeignCom2 = DbCommunity.create()
comForeignCom2.foreign = true
comForeignCom2.url = 'http://stage-3.gradido.net'
comForeignCom2.publicKey = Buffer.from(ed25519KeyPairStaticHex[4].public, 'hex')
comForeignCom2.privateKey = Buffer.from(ed25519KeyPairStaticHex[4].private, 'hex')
comForeignCom2.communityUuid = 'Stage3-Com-UUID'
comForeignCom2.authenticatedAt = new Date()
comForeignCom2.name = 'Stage-3_Community-name'
comForeignCom2.description = 'Stage-3_Community-description'
comForeignCom2.creationDate = new Date()
await DbCommunity.insert(comForeignCom2)
foreignCom4 = DbFederatedCommunity.create()
foreignCom4.foreign = true
foreignCom4.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex')
foreignCom4.apiVersion = '1_0'
foreignCom4.endPoint = 'http://remotehost/api'
foreignCom4.createdAt = new Date()
await DbFederatedCommunity.insert(foreignCom4)
})
it('return communities structured for admin ', async () => {
await expect(query({ query: allCommunities })).resolves.toMatchObject({
data: {
allCommunities: [
{
foreign: false,
url: 'http://localhost',
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
authenticatedAt: null,
createdAt: null,
creationDate: null,
description: null,
gmsApiKey: null,
name: null,
updatedAt: null,
uuid: null,
federatedCommunities: [
{
id: 3,
apiVersion: '2_0',
endPoint: 'http://localhost/api/',
createdAt: homeCom3.createdAt.toISOString(),
lastAnnouncedAt: null,
lastErrorAt: null,
updatedAt: null,
verifiedAt: null,
},
],
},
{
foreign: false,
url: 'http://localhost',
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
authenticatedAt: null,
createdAt: null,
creationDate: null,
description: null,
gmsApiKey: null,
name: null,
updatedAt: null,
uuid: null,
federatedCommunities: [
{
id: 2,
apiVersion: '1_1',
endPoint: 'http://localhost/api/',
createdAt: homeCom2.createdAt.toISOString(),
lastAnnouncedAt: null,
lastErrorAt: null,
updatedAt: null,
verifiedAt: null,
},
],
},
{
foreign: false,
url: 'http://localhost',
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
authenticatedAt: comHomeCom1.authenticatedAt?.toISOString(),
createdAt: comHomeCom1.createdAt.toISOString(),
creationDate: comHomeCom1.creationDate?.toISOString(),
description: comHomeCom1.description,
gmsApiKey: null,
name: comHomeCom1.name,
updatedAt: null,
uuid: comHomeCom1.communityUuid,
federatedCommunities: [
{
id: 1,
apiVersion: '1_0',
endPoint: 'http://localhost/api/',
createdAt: homeCom1.createdAt.toISOString(),
lastAnnouncedAt: null,
lastErrorAt: null,
updatedAt: null,
verifiedAt: null,
},
],
},
{
foreign: true,
url: 'http://remotehost',
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public),
authenticatedAt: null,
createdAt: null,
creationDate: null,
description: null,
gmsApiKey: null,
name: null,
updatedAt: null,
uuid: null,
federatedCommunities: [
{
id: 7,
apiVersion: '1_0',
endPoint: 'http://remotehost/api/',
createdAt: foreignCom4.createdAt.toISOString(),
lastAnnouncedAt: null,
lastErrorAt: null,
updatedAt: null,
verifiedAt: null,
},
{
id: 6,
apiVersion: '2_0',
endPoint: 'http://remotehost/api/',
createdAt: foreignCom3.createdAt.toISOString(),
lastAnnouncedAt: null,
lastErrorAt: null,
updatedAt: null,
verifiedAt: null,
},
],
},
{
foreign: true,
url: 'http://stage-3.gradido.net',
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public),
authenticatedAt: comForeignCom2.authenticatedAt?.toISOString(),
createdAt: comForeignCom2.createdAt.toISOString(),
creationDate: comForeignCom2.creationDate?.toISOString(),
description: comForeignCom2.description,
gmsApiKey: null,
name: comForeignCom2.name,
updatedAt: null,
uuid: comForeignCom2.communityUuid,
federatedCommunities: [
{
id: 5,
apiVersion: '1_1',
endPoint: 'http://remotehost/api/',
createdAt: foreignCom2.createdAt.toISOString(),
lastAnnouncedAt: null,
lastErrorAt: null,
updatedAt: null,
verifiedAt: null,
},
],
},
{
foreign: true,
url: 'http://stage-2.gradido.net',
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public),
authenticatedAt: null,
createdAt: comForeignCom1.createdAt.toISOString(),
creationDate: comForeignCom1.creationDate?.toISOString(),
description: comForeignCom1.description,
gmsApiKey: null,
name: comForeignCom1.name,
updatedAt: null,
uuid: null,
federatedCommunities: [
{
id: 4,
apiVersion: '1_0',
endPoint: 'http://remotehost/api/',
createdAt: foreignCom1.createdAt.toISOString(),
lastAnnouncedAt: null,
lastErrorAt: null,
updatedAt: null,
verifiedAt: null,
},
],
},
],
},
})
})
})
})
describe('communities', () => {
@ -470,8 +703,14 @@ describe('CommunityResolver', () => {
foreignCom1 = DbCommunity.create()
foreignCom1.foreign = true
foreignCom1.url = 'http://stage-2.gradido.net/api'
foreignCom1.publicKey = Buffer.from('publicKey-stage-2_Community')
foreignCom1.privateKey = Buffer.from('privateKey-stage-2_Community')
foreignCom1.publicKey = Buffer.from(
'8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d',
'hex',
)
foreignCom1.privateKey = Buffer.from(
'f6c2a9d78e20a3c910f35b8ffcf824aa7b37f0d3d81bfc4c0e65e17a194b3a4a',
'hex',
)
// foreignCom1.communityUuid = 'Stage2-Com-UUID'
// foreignCom1.authenticatedAt = new Date()
foreignCom1.name = 'Stage-2_Community-name'
@ -482,8 +721,14 @@ describe('CommunityResolver', () => {
foreignCom2 = DbCommunity.create()
foreignCom2.foreign = true
foreignCom2.url = 'http://stage-3.gradido.net/api'
foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community')
foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community')
foreignCom2.publicKey = Buffer.from(
'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f',
'hex',
)
foreignCom2.privateKey = Buffer.from(
'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f',
'hex',
)
foreignCom2.communityUuid = uuidv4()
foreignCom2.authenticatedAt = new Date()
foreignCom2.name = 'Stage-3_Community-name'

View File

@ -3,14 +3,21 @@ import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { Resolver, Query, Authorized, Mutation, Args, Arg } from 'type-graphql'
import { Paginated } from '@arg/Paginated'
import { EditCommunityInput } from '@input/EditCommunityInput'
import { AdminCommunityView } from '@model/AdminCommunityView'
import { Community } from '@model/Community'
import { FederatedCommunity } from '@model/FederatedCommunity'
import { RIGHTS } from '@/auth/RIGHTS'
import { LogError } from '@/server/LogError'
import { getCommunityByIdentifier, getCommunityByUuid, getHomeCommunity } from './util/communities'
import {
getAllCommunities,
getCommunityByIdentifier,
getCommunityByUuid,
getHomeCommunity,
} from './util/communities'
@Resolver()
export class CommunityResolver {
@ -29,6 +36,12 @@ export class CommunityResolver {
)
}
@Authorized([RIGHTS.COMMUNITIES])
@Query(() => [AdminCommunityView])
async allCommunities(@Args() paginated: Paginated): Promise<AdminCommunityView[]> {
return (await getAllCommunities(paginated)).map((dbCom) => new AdminCommunityView(dbCom))
}
@Authorized([RIGHTS.COMMUNITIES])
@Query(() => [Community])
async communities(): Promise<Community[]> {

View File

@ -1,5 +1,11 @@
import { FindOneOptions } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { Paginated } from '@arg/Paginated'
import { LogError } from '@/server/LogError'
import { Connection } from '@/typeorm/connection'
function findWithCommunityIdentifier(communityIdentifier: string): FindOneOptions<DbCommunity> {
return {
@ -86,3 +92,55 @@ export async function getCommunityByIdentifier(
): Promise<DbCommunity | null> {
return await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier))
}
/**
* Simulate RIGHT Join between Communities and Federated Communities
* select *
* Community as c
* RIGHT JOIN FederatedCommunity as f
* ON(c.public_key = f.public_key)
* Typeorm don't has right joins
* @returns
*/
export async function getAllCommunities({
pageSize = 25,
currentPage = 1,
}: Paginated): Promise<DbCommunity[]> {
const connection = await Connection.getInstance()
if (!connection) {
throw new LogError('Cannot connect to db')
}
// foreign: 'ASC',
// createdAt: 'DESC',
// lastAnnouncedAt: 'DESC',
const result = await connection
.getRepository(DbFederatedCommunity)
.createQueryBuilder('federatedCommunity')
.leftJoinAndSelect('federatedCommunity.community', 'community')
.orderBy('federatedCommunity.foreign', 'ASC')
.addOrderBy('federatedCommunity.createdAt', 'DESC')
.addOrderBy('federatedCommunity.lastAnnouncedAt', 'DESC')
.skip((currentPage - 1) * pageSize * 3)
.take(pageSize * 3)
.getManyAndCount()
const communityMap = new Map<string, DbCommunity>()
result[0].forEach((value: DbFederatedCommunity) => {
const publicKeyHex = value.publicKey.toString('hex')
if (!communityMap.has(value.publicKey.toString('hex'))) {
let community: DbCommunity = DbCommunity.create()
if (value.community) {
community = value.community
}
if (!community.federatedCommunities) {
community.federatedCommunities = []
}
communityMap.set(publicKeyHex, community)
}
const community = communityMap.get(publicKeyHex)
if (!community?.federatedCommunities) {
throw new LogError('missing community after set it into map', publicKeyHex)
}
community.federatedCommunities.push(value)
})
return Array.from(communityMap.values())
}

View File

@ -172,7 +172,8 @@ export const getCommunities = gql`
id
foreign
publicKey
url
endPoint
apiVersion
lastAnnouncedAt
verifiedAt
lastErrorAt
@ -182,6 +183,34 @@ export const getCommunities = gql`
}
`
export const allCommunities = gql`
query {
allCommunities {
foreign
url
publicKey
uuid
authenticatedAt
name
description
gmsApiKey
creationDate
createdAt
updatedAt
federatedCommunities {
id
apiVersion
endPoint
lastAnnouncedAt
verifiedAt
lastErrorAt
createdAt
updatedAt
}
}
}
`
export const queryTransactionLink = gql`
query ($code: String!) {
queryTransactionLink(code: $code) {

View File

@ -0,0 +1,78 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
JoinColumn,
} from 'typeorm'
import { FederatedCommunity } from '../FederatedCommunity'
import { User } from '../User'
@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: 32, nullable: false })
publicKey: Buffer
@Column({ name: 'private_key', type: 'binary', length: 64, nullable: true })
privateKey: Buffer | null
@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
@Column({ name: 'gms_api_key', type: 'varchar', length: 512, nullable: true, default: null })
gmsApiKey: string | 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
@OneToMany(() => User, (user) => user.community)
@JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' })
users: User[]
@OneToMany(() => FederatedCommunity, (federatedCommunity) => federatedCommunity.community)
@JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' })
federatedCommunities?: FederatedCommunity[]
}

View File

@ -0,0 +1,58 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm'
import { Community } from '../Community'
@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: 32, 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
@ManyToOne(() => Community, (community) => community.federatedCommunities)
@JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' })
community?: Community
}

View File

@ -1 +1 @@
export { Community } from './0082-introduce_gms_registration/Community'
export { Community } from './0083-join_community_federated_communities/Community'

View File

@ -1 +1 @@
export { FederatedCommunity } from './0068-community_tables_public_key_length/FederatedCommunity'
export { FederatedCommunity } from './0083-join_community_federated_communities/FederatedCommunity'

View File

@ -0,0 +1,3 @@
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {}

View File

@ -4,7 +4,7 @@ import dotenv from 'dotenv'
dotenv.config()
const constants = {
DB_VERSION: '0082-introduce_gms_registration',
DB_VERSION: '0083-join_community_federated_communities',
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',

View File

@ -10,7 +10,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0082-introduce_gms_registration',
DB_VERSION: '0083-join_community_federated_communities',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info