mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
merge master
This commit is contained in:
commit
452f68b032
1
.github/workflows/lint_pr.yml
vendored
1
.github/workflows/lint_pr.yml
vendored
@ -1,7 +1,6 @@
|
||||
name: "gradido lint pull request CI"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -436,7 +436,7 @@ jobs:
|
||||
report_name: Coverage Frontend
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 93
|
||||
min_coverage: 95
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
@ -478,7 +478,7 @@ jobs:
|
||||
report_name: Coverage Admin Interface
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 95
|
||||
min_coverage: 96
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
98
.github/workflows/test_dht-node.yml
vendored
Normal file
98
.github/workflows/test_dht-node.yml
vendored
Normal file
@ -0,0 +1,98 @@
|
||||
name: gradido test_dht-node CI
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
##############################################################################
|
||||
# JOB: DOCKER BUILD TEST #####################################################
|
||||
##############################################################################
|
||||
build:
|
||||
name: Docker Build Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build `test` image
|
||||
run: |
|
||||
docker build --target test -t "gradido/dht-node:test" -f dht-node/Dockerfile .
|
||||
docker save "gradido/dht-node:test" > /tmp/dht-node.tar
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-dht-node-test
|
||||
path: /tmp/dht-node.tar
|
||||
|
||||
##############################################################################
|
||||
# JOB: LINT ##################################################################
|
||||
##############################################################################
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-dht-node-test
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/dht-node.tar
|
||||
|
||||
- name: Lint
|
||||
run: docker run --rm gradido/dht-node:test yarn run lint
|
||||
|
||||
##############################################################################
|
||||
# JOB: UNIT TEST #############################################################
|
||||
##############################################################################
|
||||
unit_test:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-dht-node-test
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/dht-node.tar
|
||||
|
||||
- name: docker-compose mariadb
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
|
||||
- name: Sleep for 30 seconds
|
||||
run: sleep 30s
|
||||
shell: bash
|
||||
|
||||
- name: docker-compose database
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
|
||||
|
||||
- name: Sleep for 30 seconds
|
||||
run: sleep 30s
|
||||
shell: bash
|
||||
|
||||
#- name: Unit tests
|
||||
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
|
||||
- name: Unit tests
|
||||
run: |
|
||||
docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net -v ~/coverage:/app/coverage --rm gradido/dht-node:test yarn run test
|
||||
cp -r ~/coverage ./coverage
|
||||
|
||||
- name: Coverage check
|
||||
uses: webcraftmedia/coverage-check-action@master
|
||||
with:
|
||||
report_name: Coverage dht-node
|
||||
type: lcov
|
||||
#result_path: ./dht-node/coverage/lcov.info
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 79
|
||||
token: ${{ github.token }}
|
||||
@ -39,7 +39,7 @@ module.exports = {
|
||||
{
|
||||
src: './src',
|
||||
extensions: ['.js', '.vue'],
|
||||
ignores: [],
|
||||
ignores: ['/overlay/'],
|
||||
enableFix: false,
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="component-overlay">
|
||||
<b-jumbotron class="bg-light p-4">
|
||||
<template #header>{{ $t('overlay.confirm.title') }}</template>
|
||||
<template #header><slot name="title" /></template>
|
||||
|
||||
<template #lead>
|
||||
<b-row class="mt-4">
|
||||
@ -31,26 +31,18 @@
|
||||
</template>
|
||||
|
||||
<hr class="my-4" />
|
||||
<p>{{ $t('overlay.confirm.text') }}</p>
|
||||
<p>
|
||||
{{ $t('overlay.confirm.question') }}
|
||||
</p>
|
||||
<slot name="text" />
|
||||
<slot name="question" />
|
||||
|
||||
<b-container>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-button size="md" variant="danger" class="m-3" @click="$emit('overlay-cancel')">
|
||||
{{ $t('overlay.confirm.cancel') }}
|
||||
<b-button size="md" variant="info" class="m-3" @click="$emit('overlay-cancel')">
|
||||
{{ $t('overlay.cancel') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button
|
||||
size="md"
|
||||
variant="success"
|
||||
class="m-3 text-right"
|
||||
@click="$emit('confirm-creation', item)"
|
||||
>
|
||||
{{ $t('overlay.confirm.yes') }}
|
||||
</b-button>
|
||||
<slot name="submit-btn" />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
|
||||
@ -5,6 +5,7 @@ const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({})
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({})
|
||||
const toggleDetailsMock = jest.fn()
|
||||
|
||||
const propsData = {
|
||||
items: [
|
||||
@ -138,5 +139,50 @@ describe('OpenCreationsTable', () => {
|
||||
expect(wrapper.vm.items[0].creation).toEqual([444, 555, 666])
|
||||
})
|
||||
})
|
||||
|
||||
describe('call updateState', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.updateState(4)
|
||||
})
|
||||
|
||||
it('emits update-state', () => {
|
||||
expect(wrapper.vm.$root.$emit('update-state', 4)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('call updateCreationData', () => {
|
||||
const date = new Date()
|
||||
beforeEach(() => {
|
||||
wrapper.vm.updateCreationData({
|
||||
amount: Number(80.0),
|
||||
date: date,
|
||||
memo: 'Test memo',
|
||||
row: {
|
||||
item: {},
|
||||
detailsShowing: false,
|
||||
toggleDetails: toggleDetailsMock,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('emits update-state', () => {
|
||||
expect(
|
||||
wrapper.vm.$emit('update-contributions', {
|
||||
amount: Number(80.0),
|
||||
date: date,
|
||||
memo: 'Test memo',
|
||||
row: {
|
||||
item: {},
|
||||
detailsShowing: false,
|
||||
toggleDetails: toggleDetailsMock,
|
||||
},
|
||||
}),
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('calls toggleDetails', () => {
|
||||
expect(toggleDetailsMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
<b-button
|
||||
variant="danger"
|
||||
size="md"
|
||||
@click="$emit('remove-creation', row.item)"
|
||||
@click="$emit('show-overlay', row.item, 'delete')"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="x" variant="light"></b-icon>
|
||||
<b-icon icon="trash" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(editCreation)="row">
|
||||
@ -37,12 +37,24 @@
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(deny)="row">
|
||||
<div v-if="$store.state.moderator.id !== row.item.userId">
|
||||
<b-button
|
||||
variant="warning"
|
||||
size="md"
|
||||
@click="$emit('show-overlay', row.item, 'deny')"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="x" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(confirm)="row">
|
||||
<div v-if="$store.state.moderator.id !== row.item.userId">
|
||||
<b-button
|
||||
variant="success"
|
||||
size="md"
|
||||
@click="$emit('show-overlay', row.item)"
|
||||
@click="$emit('show-overlay', row.item, 'confirm')"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="check" scale="2" variant=""></b-icon>
|
||||
|
||||
7
admin/src/graphql/denyContribution.js
Normal file
7
admin/src/graphql/denyContribution.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const denyContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
denyContribution(id: $id)
|
||||
}
|
||||
`
|
||||
@ -41,7 +41,6 @@
|
||||
"creation_form": {
|
||||
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
|
||||
"creation_for": "Aktives Grundeinkommen für",
|
||||
"deleteNow": "Möchtest du diesen Beitrag zur Gemeinschaft wirklich löschen?",
|
||||
"enter_text": "Text eintragen",
|
||||
"form": "Schöpfungsformular",
|
||||
"min_characters": "Mindestens 10 Zeichen eingeben",
|
||||
@ -52,6 +51,7 @@
|
||||
"toasted": "Offene Schöpfung ({value} GDD) für {email} wurde gespeichert und liegt zur Bestätigung bereit",
|
||||
"toasted_created": "Schöpfung wurde erfolgreich gespeichert",
|
||||
"toasted_delete": "Offene Schöpfung wurde gelöscht",
|
||||
"toasted_denied": "Offene Schöpfung wurde abgelehnt",
|
||||
"toasted_update": "`Offene Schöpfung {value} GDD) für {email} wurde geändert und liegt zur Bestätigung bereit",
|
||||
"update_creation": "Schöpfung aktualisieren"
|
||||
},
|
||||
@ -61,6 +61,7 @@
|
||||
"deleted": "gelöscht",
|
||||
"deleted_user": "Alle gelöschten Nutzer",
|
||||
"delete_user": "Nutzer löschen",
|
||||
"deny": "Ablehnen",
|
||||
"edit": "Bearbeiten",
|
||||
"enabled": "aktiviert",
|
||||
"error": "Fehler",
|
||||
@ -116,12 +117,24 @@
|
||||
"open": "offen",
|
||||
"open_creations": "Offene Schöpfungen",
|
||||
"overlay": {
|
||||
"cancel": "Abbrechen",
|
||||
"confirm": {
|
||||
"cancel": "Abbrechen",
|
||||
"question": "Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und endgültig speichern?",
|
||||
"question": "Willst du diesen Gemeinwohl-Beitrag wirklich bestätigen und gutschreiben?",
|
||||
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar. Bitte überprüfe genau, dass alles stimmt.",
|
||||
"title": "Gemeinwohl-Beitrag bestätigen!",
|
||||
"yes": "Ja, Beitrag bestätigen und speichern!"
|
||||
},
|
||||
"delete": {
|
||||
"question": "Willst du diesen Gemeinwohl-Beitrag wirklich endgültig löschen?",
|
||||
"text": "Nach dem Löschen ist der Datensatz nicht mehr vorhanden.",
|
||||
"title": "Gemeinwohl-Beitrag löschen!",
|
||||
"yes": "Ja, Beitrag löschen!"
|
||||
},
|
||||
"deny": {
|
||||
"question": "Willst du diesen Gemeinwohl-Beitrag wirklich ablehnen?",
|
||||
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.",
|
||||
"title": "Schöpfung bestätigen!",
|
||||
"yes": "Ja, Schöpfung bestätigen und speichern!"
|
||||
"title": "Gemeinwohl-Beitrag ablehnen!",
|
||||
"yes": "Ja, Beitrag ablehnen und speichern!"
|
||||
}
|
||||
},
|
||||
"redeemed": "eingelöst",
|
||||
|
||||
@ -41,7 +41,6 @@
|
||||
"creation_form": {
|
||||
"creation_failed": "Could not create pending creation for {email}",
|
||||
"creation_for": "Active Basic Income for",
|
||||
"deleteNow": "Do you really want to delete this contribution to the community?",
|
||||
"enter_text": "Enter text",
|
||||
"form": "Creation form",
|
||||
"min_characters": "Enter at least 10 characters",
|
||||
@ -52,6 +51,7 @@
|
||||
"toasted": "Open creation ({value} GDD) for {email} has been saved and is ready for confirmation.",
|
||||
"toasted_created": "Creation has been successfully saved",
|
||||
"toasted_delete": "Open creation has been deleted",
|
||||
"toasted_denied": "Open creation has been denied",
|
||||
"toasted_update": "Open creation {value} GDD) for {email} has been changed and is ready for confirmation.",
|
||||
"update_creation": "Creation update"
|
||||
},
|
||||
@ -61,6 +61,7 @@
|
||||
"deleted": "deleted",
|
||||
"deleted_user": "All deleted user",
|
||||
"delete_user": "Delete user",
|
||||
"deny": "Reject",
|
||||
"edit": "Edit",
|
||||
"enabled": "enabled",
|
||||
"error": "Error",
|
||||
@ -116,12 +117,24 @@
|
||||
"open": "open",
|
||||
"open_creations": "Open creations",
|
||||
"overlay": {
|
||||
"cancel": "Cancel",
|
||||
"confirm": {
|
||||
"cancel": "Cancel",
|
||||
"question": "Do you really want to carry out and finally save this pre-stored creation?",
|
||||
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
|
||||
"text": "After saving, the record can no longer be changed. Please check carefully that everything is correct.",
|
||||
"title": "Confirm creation!",
|
||||
"yes": "Yes, confirm and save creation!"
|
||||
},
|
||||
"delete": {
|
||||
"question": "Do you really want to carry out and finally save this pre-stored creation?",
|
||||
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
|
||||
"title": "Delete creation!",
|
||||
"yes": "Yes, delete and save creation!"
|
||||
},
|
||||
"deny": {
|
||||
"question": "Do you really want to carry out and finally save this pre-stored creation?",
|
||||
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
|
||||
"title": "Reject creation!",
|
||||
"yes": "Yes, reject and save creation!"
|
||||
}
|
||||
},
|
||||
"redeemed": "redeemed",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationConfirm from './CreationConfirm.vue'
|
||||
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||
import { denyContribution } from '../graphql/denyContribution'
|
||||
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||
import { confirmContribution } from '../graphql/confirmContribution'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
@ -75,6 +76,7 @@ describe('CreationConfirm', () => {
|
||||
|
||||
const listUnconfirmedContributionsMock = jest.fn()
|
||||
const adminDeleteContributionMock = jest.fn()
|
||||
const adminDenyContributionMock = jest.fn()
|
||||
const confirmContributionMock = jest.fn()
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
@ -89,6 +91,13 @@ describe('CreationConfirm', () => {
|
||||
adminDeleteContributionMock.mockResolvedValue({ data: { adminDeleteContribution: true } }),
|
||||
)
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
denyContribution,
|
||||
adminDenyContributionMock
|
||||
.mockRejectedValueOnce({ message: 'Ouch!' })
|
||||
.mockResolvedValue({ data: { denyContribution: true } }),
|
||||
)
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
confirmContribution,
|
||||
confirmContributionMock.mockResolvedValue({ data: { confirmContribution: true } }),
|
||||
@ -130,110 +139,174 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove creation with success', () => {
|
||||
let spy
|
||||
|
||||
describe('admin confirms deletion', () => {
|
||||
describe('actions in overlay', () => {
|
||||
describe('delete creation', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('opens a modal', () => {
|
||||
expect(spy).toBeCalled()
|
||||
})
|
||||
|
||||
it('calls the adminDeleteContribution mutation', () => {
|
||||
expect(adminDeleteContributionMock).toBeCalledWith({ id: 1 })
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
|
||||
})
|
||||
})
|
||||
|
||||
describe('admin cancels deletion', () => {
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve(false))
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('does not call the adminDeleteContribution mutation', () => {
|
||||
expect(adminDeleteContributionMock).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove creation with error', () => {
|
||||
let spy
|
||||
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve('some value'))
|
||||
adminDeleteContributionMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('tr').at(2).findAll('button').at(2).trigger('click')
|
||||
})
|
||||
|
||||
describe('overlay', () => {
|
||||
it('opens the overlay', () => {
|
||||
expect(wrapper.find('#overlay').isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('cancel confirmation', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(0).trigger('click')
|
||||
describe('with success', () => {
|
||||
describe('cancel deletion', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('closes the overlay', async () => {
|
||||
expect(wrapper.find('#overlay').exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('still has 2 items in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('closes the overlay', async () => {
|
||||
expect(wrapper.find('#overlay').exists()).toBeFalsy()
|
||||
describe('confirm deletion', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('calls the adminDeleteContribution mutation', () => {
|
||||
expect(adminDeleteContributionMock).toBeCalledWith({ id: 1 })
|
||||
})
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
|
||||
})
|
||||
|
||||
it('has 1 item left in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('still has 2 items in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(2)
|
||||
describe('with error', () => {
|
||||
beforeEach(async () => {
|
||||
adminDeleteContributionMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('tr').at(2).findAll('button').at(3).trigger('click')
|
||||
})
|
||||
|
||||
it('opens the overlay', () => {
|
||||
expect(wrapper.find('#overlay').isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('with succes', () => {
|
||||
describe('cancel confirmation', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('closes the overlay', async () => {
|
||||
expect(wrapper.find('#overlay').exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('still has 2 items in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm confirmation', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('calls the confirmContribution mutation', () => {
|
||||
expect(confirmContributionMock).toBeCalledWith({ id: 2 })
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_created')
|
||||
})
|
||||
|
||||
it('has 1 item left in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation', () => {
|
||||
describe('with error', () => {
|
||||
beforeEach(async () => {
|
||||
confirmContributionMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('calls the confirmContribution mutation', () => {
|
||||
expect(confirmContributionMock).toBeCalledWith({ id: 2 })
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deny creation', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(2).trigger('click')
|
||||
})
|
||||
|
||||
it('opens the overlay', () => {
|
||||
expect(wrapper.find('#overlay').isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('with succes', () => {
|
||||
describe('cancel deny', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('closes the overlay', async () => {
|
||||
expect(wrapper.find('#overlay').exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('still has 2 items in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
describe('confirm deny', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_created')
|
||||
})
|
||||
it('calls the denyContribution mutation', () => {
|
||||
expect(adminDenyContributionMock).toBeCalledWith({ id: 1 })
|
||||
})
|
||||
|
||||
it('has 1 item left in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(1)
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_denied')
|
||||
})
|
||||
|
||||
it('has 1 item left in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation with error', () => {
|
||||
describe('with error', () => {
|
||||
beforeEach(async () => {
|
||||
confirmContributionMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
adminDenyContributionMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys -->
|
||||
<template>
|
||||
<div class="creation-confirm">
|
||||
<div>
|
||||
@ -7,7 +8,6 @@
|
||||
class="mt-4"
|
||||
:items="pendingCreations"
|
||||
:fields="fields"
|
||||
@remove-creation="removeCreation"
|
||||
@show-overlay="showOverlay"
|
||||
@update-state="updateState"
|
||||
@update-contributions="$apollo.queries.PendingContributions.refetch()"
|
||||
@ -29,7 +29,27 @@
|
||||
</div>
|
||||
|
||||
<div v-if="overlay" id="overlay" @dblclick="overlay = false">
|
||||
<overlay :item="item" @overlay-cancel="overlay = false" @confirm-creation="confirmCreation" />
|
||||
<overlay :item="item" @overlay-cancel="overlay = false">
|
||||
<template #title>
|
||||
{{ $t(overlayTitle) }}
|
||||
</template>
|
||||
<template #text>
|
||||
<p>{{ $t(overlayText) }}</p>
|
||||
</template>
|
||||
<template #question>
|
||||
<p>{{ $t(overlayQuestion) }}</p>
|
||||
</template>
|
||||
<template #submit-btn>
|
||||
<b-button
|
||||
size="md"
|
||||
v-bind:variant="overlayIcon"
|
||||
class="m-3 text-right"
|
||||
@click="overlayEvent"
|
||||
>
|
||||
{{ $t(overlayBtnText) }}
|
||||
</b-button>
|
||||
</template>
|
||||
</overlay>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -39,6 +59,7 @@ import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
|
||||
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||
import { confirmContribution } from '../graphql/confirmContribution'
|
||||
import { denyContribution } from '../graphql/denyContribution'
|
||||
|
||||
export default {
|
||||
name: 'CreationConfirm',
|
||||
@ -51,27 +72,45 @@ export default {
|
||||
pendingCreations: [],
|
||||
overlay: false,
|
||||
item: {},
|
||||
variant: 'confirm',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
removeCreation(item) {
|
||||
this.$bvModal.msgBoxConfirm(this.$t('creation_form.deleteNow')).then(async (value) => {
|
||||
if (value)
|
||||
await this.$apollo
|
||||
.mutate({
|
||||
mutation: adminDeleteContribution,
|
||||
variables: {
|
||||
id: item.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.updatePendingCreations(item.id)
|
||||
this.toastSuccess(this.$t('creation_form.toasted_delete'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
})
|
||||
deleteCreation() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: adminDeleteContribution,
|
||||
variables: {
|
||||
id: this.item.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.overlay = false
|
||||
this.updatePendingCreations(this.item.id)
|
||||
this.toastSuccess(this.$t('creation_form.toasted_delete'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.overlay = false
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
denyCreation() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: denyContribution,
|
||||
variables: {
|
||||
id: this.item.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.overlay = false
|
||||
this.updatePendingCreations(this.item.id)
|
||||
this.toastSuccess(this.$t('creation_form.toasted_denied'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.overlay = false
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
confirmCreation() {
|
||||
this.$apollo
|
||||
@ -95,9 +134,10 @@ export default {
|
||||
this.pendingCreations = this.pendingCreations.filter((obj) => obj.id !== id)
|
||||
this.$store.commit('openCreationsMinus', 1)
|
||||
},
|
||||
showOverlay(item) {
|
||||
showOverlay(item, variant) {
|
||||
this.overlay = true
|
||||
this.item = item
|
||||
this.variant = variant
|
||||
},
|
||||
updateState(id) {
|
||||
this.pendingCreations.find((obj) => obj.id === id).messagesCount++
|
||||
@ -128,9 +168,37 @@ export default {
|
||||
},
|
||||
{ key: 'moderator', label: this.$t('moderator') },
|
||||
{ key: 'editCreation', label: this.$t('edit') },
|
||||
{ key: 'deny', label: this.$t('deny') },
|
||||
{ key: 'confirm', label: this.$t('save') },
|
||||
]
|
||||
},
|
||||
overlayTitle() {
|
||||
return `overlay.${this.variant}.title`
|
||||
},
|
||||
overlayText() {
|
||||
return `overlay.${this.variant}.text`
|
||||
},
|
||||
overlayQuestion() {
|
||||
return `overlay.${this.variant}.question`
|
||||
},
|
||||
overlayBtnText() {
|
||||
return `overlay.${this.variant}.yes`
|
||||
},
|
||||
overlayEvent() {
|
||||
return this[`${this.variant}Creation`]
|
||||
},
|
||||
overlayIcon() {
|
||||
switch (this.variant) {
|
||||
case 'confirm':
|
||||
return 'success'
|
||||
case 'deny':
|
||||
return 'warning'
|
||||
case 'delete':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
PendingContributions: {
|
||||
|
||||
@ -4141,9 +4141,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001271:
|
||||
version "1.0.30001442"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz"
|
||||
integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==
|
||||
version "1.0.30001445"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001445.tgz"
|
||||
integrity sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
||||
@ -54,4 +54,5 @@ export enum RIGHTS {
|
||||
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
||||
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
||||
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
|
||||
DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
sendAccountActivationEmail,
|
||||
sendAccountMultiRegistrationEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionRejectedEmail,
|
||||
sendContributionDeniedEmail,
|
||||
sendResetPasswordEmail,
|
||||
sendTransactionLinkRedeemedEmail,
|
||||
sendTransactionReceivedEmail,
|
||||
@ -360,9 +360,9 @@ describe('sendEmailVariants', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendContributionRejectedEmail', () => {
|
||||
describe('sendContributionDeniedEmail', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendContributionRejectedEmail({
|
||||
result = await sendContributionDeniedEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
@ -379,7 +379,7 @@ describe('sendEmailVariants', () => {
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'contributionRejected',
|
||||
template: 'contributionDenied',
|
||||
locals: {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
|
||||
@ -103,7 +103,7 @@ export const sendContributionConfirmedEmail = (data: {
|
||||
})
|
||||
}
|
||||
|
||||
export const sendContributionRejectedEmail = (data: {
|
||||
export const sendContributionDeniedEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
@ -114,7 +114,7 @@ export const sendContributionRejectedEmail = (data: {
|
||||
}): Promise<Record<string, unknown> | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'contributionRejected',
|
||||
template: 'contributionDenied',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
|
||||
16
backend/src/emails/templates/contributionDenied/html.pug
Normal file
16
backend/src/emails/templates/contributionDenied/html.pug
Normal file
@ -0,0 +1,16 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.contributionDenied.subject')
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.contributionDenied.subject')
|
||||
#container.col
|
||||
include ../hello.pug
|
||||
p= t('emails.contributionDenied.commonGoodContributionDenied', { senderFirstName, senderLastName, contributionMemo })
|
||||
p= t('emails.contributionDenied.toSeeContributionsAndMessages')
|
||||
p
|
||||
= t('emails.general.linkToYourAccount')
|
||||
= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
include ../greatingFormularImprint.pug
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.contributionDenied.subject')
|
||||
@ -1,16 +0,0 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.contributionRejected.subject')
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.contributionRejected.subject')
|
||||
#container.col
|
||||
include ../hello.pug
|
||||
p= t('emails.contributionRejected.commonGoodContributionRejected', { senderFirstName, senderLastName, contributionMemo })
|
||||
p= t('emails.contributionRejected.toSeeContributionsAndMessages')
|
||||
p
|
||||
= t('emails.general.linkToYourAccount')
|
||||
= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
include ../greatingFormularImprint.pug
|
||||
@ -1 +0,0 @@
|
||||
= t('emails.contributionRejected.subject')
|
||||
@ -7,6 +7,7 @@ class EventProtocolEmitter {
|
||||
/* }extends EventEmitter { */
|
||||
private events: Event[]
|
||||
|
||||
/*
|
||||
public addEvent(event: Event) {
|
||||
this.events.push(event)
|
||||
}
|
||||
@ -14,6 +15,7 @@ class EventProtocolEmitter {
|
||||
public getEvents(): Event[] {
|
||||
return this.events
|
||||
}
|
||||
*/
|
||||
|
||||
public isDisabled() {
|
||||
logger.info(`EventProtocol - isDisabled=${CONFIG.EVENT_PROTOCOL_DISABLED}`)
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum TransactionType {
|
||||
CREATION = 'creation',
|
||||
SEND = 'send',
|
||||
RECIEVE = 'receive',
|
||||
}
|
||||
|
||||
registerEnumType(TransactionType, {
|
||||
name: 'TransactionType', // this one is mandatory
|
||||
description: 'Name of the Type of the transaction', // this one is optional
|
||||
})
|
||||
@ -18,6 +18,8 @@ export class Contribution {
|
||||
this.contributionDate = contribution.contributionDate
|
||||
this.state = contribution.contributionStatus
|
||||
this.messagesCount = contribution.messages ? contribution.messages.length : 0
|
||||
this.deniedAt = contribution.deniedAt
|
||||
this.deniedBy = contribution.deniedBy
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
@ -47,6 +49,12 @@ export class Contribution {
|
||||
@Field(() => Number, { nullable: true })
|
||||
confirmedBy: number | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deniedAt: Date | null
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
deniedBy: number | null
|
||||
|
||||
@Field(() => Date)
|
||||
contributionDate: Date
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import {
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionRejectedEmail,
|
||||
sendContributionDeniedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
|
||||
@ -217,7 +217,7 @@ export class ContributionResolver {
|
||||
const user = getUser(context)
|
||||
|
||||
const contributionToUpdate = await DbContribution.findOne({
|
||||
where: { id: contributionId, confirmedAt: IsNull() },
|
||||
where: { id: contributionId, confirmedAt: IsNull(), deniedAt: IsNull() },
|
||||
})
|
||||
if (!contributionToUpdate) {
|
||||
logger.error('No contribution found to given id')
|
||||
@ -409,7 +409,7 @@ export class ContributionResolver {
|
||||
const moderator = getUser(context)
|
||||
|
||||
const contributionToUpdate = await DbContribution.findOne({
|
||||
where: { id, confirmedAt: IsNull() },
|
||||
where: { id, confirmedAt: IsNull(), deniedAt: IsNull() },
|
||||
})
|
||||
if (!contributionToUpdate) {
|
||||
logger.error('No contribution found to given id.')
|
||||
@ -475,6 +475,7 @@ export class ContributionResolver {
|
||||
.from(DbContribution, 'c')
|
||||
.leftJoinAndSelect('c.messages', 'm')
|
||||
.where({ confirmedAt: IsNull() })
|
||||
.andWhere({ deniedAt: IsNull() })
|
||||
.getMany()
|
||||
|
||||
if (contributions.length === 0) {
|
||||
@ -540,7 +541,7 @@ export class ContributionResolver {
|
||||
await eventProtocol.writeEvent(
|
||||
event.setEventAdminContributionDelete(eventAdminContributionDelete),
|
||||
)
|
||||
sendContributionRejectedEmail({
|
||||
sendContributionDeniedEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.emailContact.email,
|
||||
@ -572,6 +573,10 @@ export class ContributionResolver {
|
||||
logger.error(`Contribution already confirmd: ${id}`)
|
||||
throw new Error('Contribution already confirmd.')
|
||||
}
|
||||
if (contribution.contributionStatus === 'DENIED') {
|
||||
logger.error(`Contribution already denied: ${id}`)
|
||||
throw new Error('Contribution already denied.')
|
||||
}
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === contribution.userId) {
|
||||
logger.error('Moderator can not confirm own contribution')
|
||||
@ -684,6 +689,7 @@ export class ContributionResolver {
|
||||
.from(DbContribution, 'c')
|
||||
.leftJoinAndSelect('c.user', 'u')
|
||||
.where(`user_id = ${userId}`)
|
||||
.withDeleted()
|
||||
.limit(pageSize)
|
||||
.offset(offset)
|
||||
.orderBy('c.created_at', order)
|
||||
@ -714,4 +720,58 @@ export class ContributionResolver {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DENY_CONTRIBUTION])
|
||||
@Mutation(() => Boolean)
|
||||
async denyContribution(
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const contributionToUpdate = await DbContribution.findOne({
|
||||
id,
|
||||
confirmedAt: IsNull(),
|
||||
deniedBy: IsNull(),
|
||||
})
|
||||
if (!contributionToUpdate) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error(`Contribution not found for given id.`)
|
||||
}
|
||||
if (
|
||||
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
|
||||
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
|
||||
) {
|
||||
logger.error(
|
||||
`Contribution state (${contributionToUpdate.contributionStatus}) is not allowed.`,
|
||||
)
|
||||
throw new Error(`State of the contribution is not allowed.`)
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
const user = await DbUser.findOne(
|
||||
{ id: contributionToUpdate.userId },
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
if (!user) {
|
||||
logger.error(
|
||||
`Could not find User for the Contribution (userId: ${contributionToUpdate.userId}).`,
|
||||
)
|
||||
throw new Error('Could not find User for the Contribution.')
|
||||
}
|
||||
|
||||
contributionToUpdate.contributionStatus = ContributionStatus.DENIED
|
||||
contributionToUpdate.deniedBy = moderator.id
|
||||
contributionToUpdate.deniedAt = new Date()
|
||||
const res = await contributionToUpdate.save()
|
||||
|
||||
sendContributionDeniedEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.emailContact.email,
|
||||
language: user.language,
|
||||
senderFirstName: moderator.firstName,
|
||||
senderLastName: moderator.lastName,
|
||||
contributionMemo: contributionToUpdate.memo,
|
||||
})
|
||||
|
||||
return !!res
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ describe('EmailOptinCodes', () => {
|
||||
mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: null,
|
||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
||||
errors: [new GraphQLError('email already sent less than 10 minutes ago')],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -830,7 +830,7 @@ describe('UserResolver', () => {
|
||||
new GraphQLError(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)} ago`,
|
||||
),
|
||||
],
|
||||
}),
|
||||
@ -870,13 +870,13 @@ describe('UserResolver', () => {
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
||||
errors: [new GraphQLError('email already sent less than 10 minutes ago')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(`email already sent less than 10 minutes minutes ago`)
|
||||
expect(logger.error).toBeCalledWith(`email already sent less than 10 minutes ago`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -392,16 +392,11 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) {
|
||||
logger.error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
throw new Error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
const errorMessage = `email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} ago`
|
||||
logger.error(errorMessage)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
user.emailContact.updatedAt = new Date()
|
||||
|
||||
@ -62,7 +62,7 @@ describe('util/creation', () => {
|
||||
amount: 160.0,
|
||||
memo: 'Admin contribution for the last month',
|
||||
creationDate: contributionDateFormatter(
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
||||
),
|
||||
},
|
||||
})
|
||||
@ -73,7 +73,7 @@ describe('util/creation', () => {
|
||||
amount: 450.0,
|
||||
memo: 'Admin contribution for two months ago',
|
||||
creationDate: contributionDateFormatter(
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, now.getDate()),
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1),
|
||||
),
|
||||
},
|
||||
})
|
||||
@ -95,7 +95,7 @@ describe('util/creation', () => {
|
||||
amount: 500.0,
|
||||
memo: 'Contribution for the last month',
|
||||
creationDate: contributionDateFormatter(
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
||||
),
|
||||
},
|
||||
})
|
||||
@ -114,9 +114,7 @@ describe('util/creation', () => {
|
||||
}),
|
||||
expect.objectContaining({
|
||||
userId: user.id,
|
||||
contributionDate: setZeroHours(
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
|
||||
),
|
||||
contributionDate: setZeroHours(new Date(now.getFullYear(), now.getMonth() - 1, 1)),
|
||||
amount: expect.decimalEqual(160),
|
||||
memo: 'Admin contribution for the last month',
|
||||
moderatorId: admin.id,
|
||||
@ -125,9 +123,7 @@ describe('util/creation', () => {
|
||||
}),
|
||||
expect.objectContaining({
|
||||
userId: user.id,
|
||||
contributionDate: setZeroHours(
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, now.getDate()),
|
||||
),
|
||||
contributionDate: setZeroHours(new Date(now.getFullYear(), now.getMonth() - 2, 1)),
|
||||
amount: expect.decimalEqual(450),
|
||||
memo: 'Admin contribution for two months ago',
|
||||
moderatorId: admin.id,
|
||||
@ -145,9 +141,7 @@ describe('util/creation', () => {
|
||||
}),
|
||||
expect.objectContaining({
|
||||
userId: user.id,
|
||||
contributionDate: setZeroHours(
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
|
||||
),
|
||||
contributionDate: setZeroHours(new Date(now.getFullYear(), now.getMonth() - 1, 1)),
|
||||
amount: expect.decimalEqual(500),
|
||||
memo: 'Contribution for the last month',
|
||||
moderatorId: null,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import createServer from './server/createServer'
|
||||
import { startDHT } from '@/federation/index'
|
||||
|
||||
// config
|
||||
import CONFIG from './config'
|
||||
@ -17,20 +16,6 @@ async function main() {
|
||||
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
|
||||
}
|
||||
})
|
||||
|
||||
// start DHT hyperswarm when DHT_TOPIC is set in .env
|
||||
if (CONFIG.FEDERATION_DHT_TOPIC) {
|
||||
if (CONFIG.FEDERATION_COMMUNITY_URL === null) {
|
||||
throw Error(`Config-Error: missing configuration of property FEDERATION_COMMUNITY_URL`)
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
||||
CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...'
|
||||
}`,
|
||||
)
|
||||
await startDHT(CONFIG.FEDERATION_DHT_TOPIC) // con,
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
|
||||
@ -23,8 +23,8 @@
|
||||
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
|
||||
"subject": "Gradido: Your contribution to the common good was confirmed"
|
||||
},
|
||||
"contributionRejected": {
|
||||
"commonGoodContributionRejected": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
|
||||
"contributionDenied": {
|
||||
"commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
|
||||
"subject": "Gradido: Your common good contribution was rejected",
|
||||
"toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
|
||||
},
|
||||
|
||||
22
dht-node/.env.dist
Normal file
22
dht-node/.env.dist
Normal file
@ -0,0 +1,22 @@
|
||||
CONFIG_VERSION=v1.2023-01-01
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.dht-node.log
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
# LOG_LEVEL=info
|
||||
|
||||
# Federation
|
||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this topic
|
||||
FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
17
dht-node/.env.template
Normal file
17
dht-node/.env.template
Normal file
@ -0,0 +1,17 @@
|
||||
CONFIG_VERSION=$BACKEND_CONFIG_VERSION
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=$DB_USER
|
||||
DB_PASSWORD=$DB_PASSWORD
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
|
||||
|
||||
# Federation
|
||||
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
||||
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
||||
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL
|
||||
3
dht-node/.eslintignore
Normal file
3
dht-node/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
**/*.min.js
|
||||
build
|
||||
26
dht-node/.eslintrc.js
Normal file
26
dht-node/.eslintrc.js
Normal file
@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
// jest: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['prettier', '@typescript-eslint' /*, 'jest' */],
|
||||
extends: [
|
||||
'standard',
|
||||
'eslint:recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
'no-console': ['error'],
|
||||
'no-debugger': 'error',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
8
dht-node/.gitignore
vendored
Normal file
8
dht-node/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/node_modules/
|
||||
/.env
|
||||
/.env.bak
|
||||
/build/
|
||||
package-json.lock
|
||||
coverage
|
||||
# emacs
|
||||
*~
|
||||
9
dht-node/.prettierrc.js
Normal file
9
dht-node/.prettierrc.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
printWidth: 100,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
endOfLine: "auto",
|
||||
};
|
||||
119
dht-node/Dockerfile
Normal file
119
dht-node/Dockerfile
Normal file
@ -0,0 +1,119 @@
|
||||
##################################################################################
|
||||
# BASE ###########################################################################
|
||||
##################################################################################
|
||||
FROM node:19.5.0-alpine3.17 as base
|
||||
#FROM ubuntu:latest as base
|
||||
|
||||
# ENVs (available in production aswell, can be overwritten by commandline or env file)
|
||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||
ENV DOCKER_WORKDIR="/app"
|
||||
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
|
||||
ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
|
||||
## We cannot do $(npm run version).${BUILD_NUMBER} here so we default to 0.0.0.0
|
||||
ENV BUILD_VERSION="0.0.0.0"
|
||||
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
|
||||
ENV BUILD_COMMIT="0000000"
|
||||
## SET NODE_ENV
|
||||
ENV NODE_ENV="production"
|
||||
## App relevant Envs
|
||||
#ENV PORT="5000"
|
||||
|
||||
# Labels
|
||||
LABEL org.label-schema.build-date="${BUILD_DATE}"
|
||||
LABEL org.label-schema.name="gradido:dht-node"
|
||||
LABEL org.label-schema.description="Gradido dht-node"
|
||||
LABEL org.label-schema.usage="https://github.com/gradido/gradido/blob/master/README.md"
|
||||
LABEL org.label-schema.url="https://gradido.net"
|
||||
LABEL org.label-schema.vcs-url="https://github.com/gradido/gradido/tree/master/dht-node"
|
||||
LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
|
||||
LABEL org.label-schema.vendor="Gradido Community"
|
||||
LABEL org.label-schema.version="${BUILD_VERSION}"
|
||||
LABEL org.label-schema.schema-version="1.0"
|
||||
LABEL maintainer="support@gradido.net"
|
||||
|
||||
# Install Additional Software
|
||||
## install: sodium requirements
|
||||
RUN apk add --no-cache --virtual build-deps python3 alpine-sdk autoconf libtool automake && \
|
||||
mkdir -p /prebuilds && cd /prebuilds && npm init -y && npm install sodium-native@3.1.1 && \
|
||||
apk del build-deps
|
||||
ENV SODIUM_NATIVE_PREBUILD=/prebuilds/node_modules/sodium-native/
|
||||
|
||||
# Settings
|
||||
## Expose Container Port
|
||||
#EXPOSE ${PORT}
|
||||
|
||||
## Workdir
|
||||
RUN mkdir -p ${DOCKER_WORKDIR}
|
||||
WORKDIR ${DOCKER_WORKDIR}
|
||||
|
||||
RUN mkdir -p /database
|
||||
|
||||
##################################################################################
|
||||
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
|
||||
##################################################################################
|
||||
FROM base as development
|
||||
|
||||
# We don't need to copy or build anything since we gonna bind to the
|
||||
# local filesystem which will need a rebuild anyway
|
||||
|
||||
# Run command
|
||||
# (for development we need to execute yarn install since the
|
||||
# node_modules are on another volume and need updating)
|
||||
CMD /bin/sh -c "cd /database && yarn install && yarn build && cd /app && yarn install && yarn run dev"
|
||||
|
||||
##################################################################################
|
||||
# BUILD (Does contain all files and is therefore bloated) ########################
|
||||
##################################################################################
|
||||
FROM base as build
|
||||
|
||||
# Copy everything from dht-node
|
||||
COPY ./dht-node/ ./
|
||||
# Copy everything from database
|
||||
COPY ./database/ ../database/
|
||||
|
||||
# yarn install dht-node
|
||||
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
|
||||
# yarn install database
|
||||
RUN cd ../database && yarn install --production=false --frozen-lockfile --non-interactive
|
||||
|
||||
# yarn build
|
||||
RUN yarn run build
|
||||
|
||||
# yarn build database
|
||||
RUN cd ../database && yarn run build
|
||||
|
||||
##################################################################################
|
||||
# TEST ###########################################################################
|
||||
##################################################################################
|
||||
FROM build as test
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn run start"
|
||||
|
||||
##################################################################################
|
||||
# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
|
||||
##################################################################################
|
||||
FROM base as production
|
||||
|
||||
# Copy "binary"-files from build image
|
||||
COPY --from=build ${DOCKER_WORKDIR}/build ./build
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build
|
||||
# We also copy the node_modules express and serve-static for the run script
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules
|
||||
|
||||
# Copy static files
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
|
||||
# Copy package.json for script definitions (lock file should not be needed)
|
||||
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
|
||||
# Copy tsconfig.json to provide alias path definitions
|
||||
COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
|
||||
# Copy log4js-config.json to provide log configuration
|
||||
COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json
|
||||
|
||||
# Copy run scripts run/
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/run ./run
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn run start"
|
||||
22
dht-node/jest.config.js
Normal file
22
dht-node/jest.config.js
Normal file
@ -0,0 +1,22 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
preset: 'ts-jest',
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
setupFilesAfterEnv: [],
|
||||
modulePathIgnorePatterns: ['<rootDir>/build/'],
|
||||
moduleNameMapper: {
|
||||
'@/(.*)': '<rootDir>/src/$1',
|
||||
'@test/(.*)': '<rootDir>/test/$1',
|
||||
'@entity/(.*)':
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../database/entity/$1'
|
||||
: '<rootDir>/../database/build/entity/$1',
|
||||
'@dbTools/(.*)':
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../database/src/$1'
|
||||
: '<rootDir>/../database/build/src/$1',
|
||||
},
|
||||
}
|
||||
69
dht-node/log4js-config.json
Normal file
69
dht-node/log4js-config.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"appenders":
|
||||
{
|
||||
"dht":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/dht-node/apiversion-%v.log",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"errorFile":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/dht-node/errors.log",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"errors":
|
||||
{
|
||||
"type": "logLevelFilter",
|
||||
"level": "error",
|
||||
"appender": "errorFile"
|
||||
},
|
||||
"out":
|
||||
{
|
||||
"type": "stdout",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
}
|
||||
}
|
||||
},
|
||||
"categories":
|
||||
{
|
||||
"default":
|
||||
{
|
||||
"appenders":
|
||||
[
|
||||
"out",
|
||||
"errors"
|
||||
],
|
||||
"level": "debug",
|
||||
"enableCallStack": true
|
||||
},
|
||||
"dht":
|
||||
{
|
||||
"appenders":
|
||||
[
|
||||
"dht",
|
||||
"out",
|
||||
"errors"
|
||||
],
|
||||
"level": "debug",
|
||||
"enableCallStack": true
|
||||
}
|
||||
}
|
||||
}
|
||||
45
dht-node/package.json
Normal file
45
dht-node/package.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "gradido-dht-node",
|
||||
"version": "1.0.0",
|
||||
"description": "Gradido dht-node module",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/",
|
||||
"author": "Claus-Peter Huebner",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"clean": "tsc --build --clean",
|
||||
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
|
||||
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
||||
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hyperswarm/dht": "^6.4.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "10.0.0",
|
||||
"log4js": "^6.7.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-n": "^15.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"jest": "^27.2.4",
|
||||
"prettier": "^2.3.1",
|
||||
"ts-jest": "^27.0.5"
|
||||
}
|
||||
}
|
||||
62
dht-node/src/config/index.ts
Normal file
62
dht-node/src/config/index.ts
Normal file
@ -0,0 +1,62 @@
|
||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0059-add_hide_amount_to_users',
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v1.2023-01-01',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
const server = {
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||
}
|
||||
|
||||
const database = {
|
||||
DB_HOST: process.env.DB_HOST || 'localhost',
|
||||
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
|
||||
DB_USER: process.env.DB_USER || 'root',
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || '',
|
||||
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
|
||||
TYPEORM_LOGGING_RELATIVE_PATH:
|
||||
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log',
|
||||
}
|
||||
|
||||
const eventProtocol = {
|
||||
// global switch to enable writing of EventProtocol-Entries
|
||||
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
|
||||
}
|
||||
|
||||
const federation = {
|
||||
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB',
|
||||
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
|
||||
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
|
||||
}
|
||||
|
||||
// Check config version
|
||||
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
|
||||
if (
|
||||
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
|
||||
constants.CONFIG_VERSION.CURRENT,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
|
||||
)
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
...constants,
|
||||
...server,
|
||||
...database,
|
||||
...eventProtocol,
|
||||
...federation,
|
||||
}
|
||||
|
||||
export default CONFIG
|
||||
@ -95,7 +95,7 @@ let con: any
|
||||
let testEnv: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger)
|
||||
testEnv = await testEnvironment()
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
@ -181,7 +181,7 @@ describe('federation', () => {
|
||||
it('logs an error of unexpected data format and structure', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Error on receiving data from socket:',
|
||||
new SyntaxError('Unexpected token o in JSON at position 1'),
|
||||
new SyntaxError('Unexpected token \'o\', "no-json string" is not valid JSON'),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -200,7 +200,7 @@ describe('federation', () => {
|
||||
it('logs an error of unexpected data format and structure', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Error on receiving data from socket:',
|
||||
new SyntaxError('Unexpected token i in JSON at position 0'),
|
||||
new SyntaxError('Unexpected token \'i\', "invalid ty"... is not valid JSON'),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -224,7 +224,7 @@ describe('federation', () => {
|
||||
it('logs an error of unexpected data format and structure', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Error on receiving data from socket:',
|
||||
new SyntaxError('Unexpected token a in JSON at position 0'),
|
||||
new SyntaxError('Unexpected token \'a\', "api,url,in"... is not valid JSON'),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import DHT from '@hyperswarm/dht'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { logger } from '@/server/logger'
|
||||
import CONFIG from '@/config'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
|
||||
@ -58,57 +58,56 @@ export const startDHT = async (topic: string): Promise<void> => {
|
||||
logger.info(`data: ${data.toString('ascii')}`)
|
||||
const recApiVersions: CommunityApi[] = JSON.parse(data.toString('ascii'))
|
||||
|
||||
// TODO better to introduce the validation by https://github.com/typestack/class-validato
|
||||
if (recApiVersions && Array.isArray(recApiVersions) && recApiVersions.length < 5) {
|
||||
for (const recApiVersion of recApiVersions) {
|
||||
if (
|
||||
!recApiVersion.api ||
|
||||
typeof recApiVersion.api !== 'string' ||
|
||||
!recApiVersion.url ||
|
||||
typeof recApiVersion.url !== 'string'
|
||||
) {
|
||||
logger.warn(
|
||||
`received invalid apiVersion-Definition: ${JSON.stringify(recApiVersion)}`,
|
||||
)
|
||||
// in a forEach-loop use return instead of continue
|
||||
return
|
||||
}
|
||||
// TODO better to introduce the validation on entity-Level by https://github.com/typestack/class-validator
|
||||
if (recApiVersion.api.length > 10 || recApiVersion.url.length > 255) {
|
||||
logger.warn(
|
||||
`received apiVersion with content longer than max length: ${JSON.stringify(
|
||||
recApiVersion,
|
||||
)}`,
|
||||
)
|
||||
// in a forEach-loop use return instead of continue
|
||||
return
|
||||
}
|
||||
|
||||
const variables = {
|
||||
apiVersion: recApiVersion.api,
|
||||
endPoint: recApiVersion.url,
|
||||
publicKey: socket.remotePublicKey.toString('hex'),
|
||||
lastAnnouncedAt: new Date(),
|
||||
}
|
||||
logger.debug(`upsert with variables=${JSON.stringify(variables)}`)
|
||||
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
|
||||
await DbCommunity.createQueryBuilder()
|
||||
.insert()
|
||||
.into(DbCommunity)
|
||||
.values(variables)
|
||||
.orUpdate({
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
.execute()
|
||||
logger.info(`federation community upserted successfully...`)
|
||||
}
|
||||
} else {
|
||||
// TODO better to introduce the validation by https://github.com/typestack/class-validator
|
||||
if (!recApiVersions || !Array.isArray(recApiVersions) || recApiVersions.length >= 5) {
|
||||
logger.warn(
|
||||
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
|
||||
recApiVersions,
|
||||
)}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
for (const recApiVersion of recApiVersions) {
|
||||
if (
|
||||
!recApiVersion.api ||
|
||||
typeof recApiVersion.api !== 'string' ||
|
||||
!recApiVersion.url ||
|
||||
typeof recApiVersion.url !== 'string'
|
||||
) {
|
||||
logger.warn(
|
||||
`received invalid apiVersion-Definition: ${JSON.stringify(recApiVersion)}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
// TODO better to introduce the validation on entity-Level by https://github.com/typestack/class-validator
|
||||
if (recApiVersion.api.length > 10 || recApiVersion.url.length > 255) {
|
||||
logger.warn(
|
||||
`received apiVersion with content longer than max length: ${JSON.stringify(
|
||||
recApiVersion,
|
||||
)}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const variables = {
|
||||
apiVersion: recApiVersion.api,
|
||||
endPoint: recApiVersion.url,
|
||||
publicKey: socket.remotePublicKey.toString('hex'),
|
||||
lastAnnouncedAt: new Date(),
|
||||
}
|
||||
logger.debug(`upsert with variables=${JSON.stringify(variables)}`)
|
||||
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
|
||||
await DbCommunity.createQueryBuilder()
|
||||
.insert()
|
||||
.into(DbCommunity)
|
||||
.values(variables)
|
||||
.orUpdate({
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
.execute()
|
||||
logger.info(`federation community upserted successfully...`)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error on receiving data from socket:', e)
|
||||
@ -155,7 +154,9 @@ export const startDHT = async (topic: string): Promise<void> => {
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Found new peers: ${collectedPubKeys}`)
|
||||
if (collectedPubKeys.length) {
|
||||
logger.info(`Found new peers: ${collectedPubKeys}`)
|
||||
}
|
||||
|
||||
collectedPubKeys.forEach((remotePubKey) => {
|
||||
const socket = node.connect(Buffer.from(remotePubKey, 'hex'))
|
||||
38
dht-node/src/index.ts
Normal file
38
dht-node/src/index.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { startDHT } from '@/dht_node/index'
|
||||
|
||||
// config
|
||||
import CONFIG from './config'
|
||||
import { logger } from './server/logger'
|
||||
import connection from './typeorm/connection'
|
||||
import { checkDBVersion } from './typeorm/DBVersion'
|
||||
|
||||
async function main() {
|
||||
// open mysql connection
|
||||
const con = await connection()
|
||||
if (!con || !con.isConnected) {
|
||||
logger.fatal(`Couldn't open connection to database!`)
|
||||
throw new Error(`Fatal: Couldn't open connection to database`)
|
||||
}
|
||||
|
||||
// check for correct database version
|
||||
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
|
||||
if (!dbVersion) {
|
||||
logger.fatal('Fatal: Database Version incorrect')
|
||||
throw new Error('Fatal: Database Version incorrect')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
||||
CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...'
|
||||
}`,
|
||||
)
|
||||
await startDHT(CONFIG.FEDERATION_DHT_TOPIC)
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
20
dht-node/src/server/logger.ts
Normal file
20
dht-node/src/server/logger.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import log4js from 'log4js'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
|
||||
|
||||
options.categories.dht.level = CONFIG.LOG_LEVEL
|
||||
let filename: string = options.appenders.dht.filename
|
||||
options.appenders.dht.filename = filename.replace(
|
||||
'apiversion-%v',
|
||||
'dht-' + CONFIG.FEDERATION_DHT_TOPIC,
|
||||
)
|
||||
filename = options.appenders.errorFile.filename
|
||||
|
||||
log4js.configure(options)
|
||||
|
||||
const logger = log4js.getLogger('dht')
|
||||
|
||||
export { logger }
|
||||
27
dht-node/src/typeorm/DBVersion.ts
Normal file
27
dht-node/src/typeorm/DBVersion.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Migration } from '@entity/Migration'
|
||||
import { logger } from '@/server/logger'
|
||||
|
||||
const getDBVersion = async (): Promise<string | null> => {
|
||||
try {
|
||||
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
|
||||
return dbVersion ? dbVersion.fileName : null
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
||||
const dbVersion = await getDBVersion()
|
||||
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
|
||||
logger.error(
|
||||
`Wrong database version detected - the dht-node requires '${DB_VERSION}' but found '${
|
||||
dbVersion || 'None'
|
||||
}`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export { checkDBVersion, getDBVersion }
|
||||
34
dht-node/src/typeorm/connection.ts
Normal file
34
dht-node/src/typeorm/connection.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// TODO This is super weird - since the entities are defined in another project they have their own globals.
|
||||
// We cannot use our connection here, but must use the external typeorm installation
|
||||
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
|
||||
import CONFIG from '@/config'
|
||||
import { entities } from '@entity/index'
|
||||
|
||||
const connection = async (): Promise<Connection | null> => {
|
||||
try {
|
||||
return createConnection({
|
||||
name: 'default',
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
entities,
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default connection
|
||||
7
dht-node/test/helpers.test.ts
Normal file
7
dht-node/test/helpers.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { contributionDateFormatter } from '@test/helpers'
|
||||
|
||||
describe('contributionDateFormatter', () => {
|
||||
it('formats the date correctly', () => {
|
||||
expect(contributionDateFormatter(new Date('Thu Feb 29 2024 13:12:11'))).toEqual('2/29/2024')
|
||||
})
|
||||
})
|
||||
68
dht-node/test/helpers.ts
Normal file
68
dht-node/test/helpers.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import CONFIG from '@/config'
|
||||
import connection from '@/typeorm/connection'
|
||||
import { checkDBVersion } from '@/typeorm/DBVersion'
|
||||
import { initialize } from '@dbTools/helpers'
|
||||
import { entities } from '@entity/index'
|
||||
import { logger } from './testSetup'
|
||||
|
||||
export const headerPushMock = jest.fn((t) => {
|
||||
context.token = t.value
|
||||
})
|
||||
|
||||
const context = {
|
||||
token: '',
|
||||
setHeaders: {
|
||||
push: headerPushMock,
|
||||
forEach: jest.fn(),
|
||||
},
|
||||
clientTimezoneOffset: 0,
|
||||
}
|
||||
|
||||
export const cleanDB = async () => {
|
||||
// this only works as long we do not have foreign key constraints
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
await resetEntity(entities[i])
|
||||
}
|
||||
}
|
||||
|
||||
export const testEnvironment = async () => {
|
||||
// open mysql connection
|
||||
const con = await connection()
|
||||
if (!con || !con.isConnected) {
|
||||
logger.fatal(`Couldn't open connection to database!`)
|
||||
throw new Error(`Fatal: Couldn't open connection to database`)
|
||||
}
|
||||
|
||||
// check for correct database version
|
||||
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
|
||||
if (!dbVersion) {
|
||||
logger.fatal('Fatal: Database Version incorrect')
|
||||
throw new Error('Fatal: Database Version incorrect')
|
||||
}
|
||||
await initialize()
|
||||
return { con }
|
||||
}
|
||||
|
||||
export const resetEntity = async (entity: any) => {
|
||||
const items = await entity.find({ withDeleted: true })
|
||||
if (items.length > 0) {
|
||||
const ids = items.map((i: any) => i.id)
|
||||
await entity.delete(ids)
|
||||
}
|
||||
}
|
||||
|
||||
export const resetToken = () => {
|
||||
context.token = ''
|
||||
}
|
||||
|
||||
// format date string as it comes from the frontend for the contribution date
|
||||
export const contributionDateFormatter = (date: Date): string => {
|
||||
return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||
}
|
||||
|
||||
export const setClientTimezoneOffset = (offset: number): void => {
|
||||
context.clientTimezoneOffset = offset
|
||||
}
|
||||
22
dht-node/test/testSetup.ts
Normal file
22
dht-node/test/testSetup.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { logger } from '@/server/logger'
|
||||
|
||||
jest.setTimeout(1000000)
|
||||
|
||||
jest.mock('@/server/logger', () => {
|
||||
const originalModule = jest.requireActual('@/server/logger')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
logger: {
|
||||
addContext: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export { logger }
|
||||
86
dht-node/tsconfig.json
Normal file
86
dht-node/tsconfig.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./build", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
|
||||
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
"@/*": ["src/*"],
|
||||
"@test/*": ["test/*"],
|
||||
/* external */
|
||||
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
|
||||
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
|
||||
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
|
||||
},
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
"typeRoots": ["src/dht_node/@types", "node_modules/@types"], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../database/tsconfig.json",
|
||||
// add 'prepend' if you want to include the referenced project in your output file
|
||||
// "prepend": true
|
||||
}
|
||||
]
|
||||
}
|
||||
4320
dht-node/yarn.lock
Normal file
4320
dht-node/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -61,6 +61,29 @@ services:
|
||||
- ./backend:/app
|
||||
- ./database:/database
|
||||
|
||||
########################################################
|
||||
# DHT-NODE #############################################
|
||||
########################################################
|
||||
dht-node:
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/dht-node:local-development
|
||||
build:
|
||||
target: development
|
||||
networks:
|
||||
- external-net
|
||||
- internal-net
|
||||
environment:
|
||||
- NODE_ENV="development"
|
||||
volumes:
|
||||
# This makes sure the docker container has its own node modules.
|
||||
# Therefore it is possible to have a different node version on the host machine
|
||||
- dht_node_modules:/app/node_modules
|
||||
- dht_database_node_modules:/database/node_modules
|
||||
- dht_database_build:/database/build
|
||||
# bind the local folder to the docker to allow live reload
|
||||
- ./dht-node:/app
|
||||
- ./database:/database
|
||||
|
||||
########################################################
|
||||
# DATABASE ##############################################
|
||||
########################################################
|
||||
@ -129,5 +152,8 @@ volumes:
|
||||
backend_node_modules:
|
||||
backend_database_node_modules:
|
||||
backend_database_build:
|
||||
dht_node_modules:
|
||||
dht_database_node_modules:
|
||||
dht_database_build:
|
||||
database_node_modules:
|
||||
database_build:
|
||||
database_build:
|
||||
@ -111,6 +111,42 @@ services:
|
||||
# <host_machine_directory>:<container_directory> – mirror bidirectional path in local context with path in Docker container
|
||||
- ./logs/backend:/logs/backend
|
||||
|
||||
########################################################
|
||||
# DHT-NODE #############################################
|
||||
########################################################
|
||||
dht-node:
|
||||
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
|
||||
image: gradido/dht-node:local-production
|
||||
build:
|
||||
# since we have to include the entities from ./database we cannot define the context as ./backend
|
||||
# this might blow build image size to the moon ?!
|
||||
context: ./
|
||||
dockerfile: ./dht-node/Dockerfile
|
||||
target: production
|
||||
networks:
|
||||
- internal-net
|
||||
- external-net
|
||||
#ports:
|
||||
# - 5000:5000
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: always
|
||||
environment:
|
||||
# Envs used in Dockerfile
|
||||
# - DOCKER_WORKDIR="/app"
|
||||
# - PORT=5000
|
||||
- BUILD_DATE
|
||||
- BUILD_VERSION
|
||||
- BUILD_COMMIT
|
||||
- NODE_ENV="production"
|
||||
- DB_HOST=mariadb
|
||||
# Application only envs
|
||||
#env_file:
|
||||
# - ./frontend/.env
|
||||
volumes:
|
||||
# <host_machine_directory>:<container_directory> – mirror bidirectional path in local context with path in Docker container
|
||||
- ./logs/dht-node:/logs/dht-node
|
||||
|
||||
########################################################
|
||||
# DATABASE #############################################
|
||||
########################################################
|
||||
|
||||
3
federation/.eslintignore
Normal file
3
federation/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
**/*.min.js
|
||||
build
|
||||
28
federation/.eslintrc.js
Normal file
28
federation/.eslintrc.js
Normal file
@ -0,0 +1,28 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
// jest: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['prettier', '@typescript-eslint' /*, 'jest' */],
|
||||
extends: [
|
||||
'standard',
|
||||
'eslint:recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
'no-console': ['error'],
|
||||
'no-debugger': 'error',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
8
federation/.gitignore
vendored
Normal file
8
federation/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/node_modules/
|
||||
/.env
|
||||
/.env.bak
|
||||
/build/
|
||||
package-json.lock
|
||||
coverage
|
||||
# emacs
|
||||
*~
|
||||
146
federation/log4js-config.json
Normal file
146
federation/log4js-config.json
Normal file
@ -0,0 +1,146 @@
|
||||
{
|
||||
"appenders":
|
||||
{
|
||||
"access":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/federation/access-%p.log",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"apollo":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/federation/apollo-%p.log",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"backend":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/federation/backend-%p.log",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"federation":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/federation/apiversion-%v-%p.log",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"errorFile":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/federation/errors-%p.log",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"errors":
|
||||
{
|
||||
"type": "logLevelFilter",
|
||||
"level": "error",
|
||||
"appender": "errorFile"
|
||||
},
|
||||
"out":
|
||||
{
|
||||
"type": "stdout",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
}
|
||||
},
|
||||
"apolloOut":
|
||||
{
|
||||
"type": "stdout",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
}
|
||||
}
|
||||
},
|
||||
"categories":
|
||||
{
|
||||
"default":
|
||||
{
|
||||
"appenders":
|
||||
[
|
||||
"out",
|
||||
"errors"
|
||||
],
|
||||
"level": "debug",
|
||||
"enableCallStack": true
|
||||
},
|
||||
"apollo":
|
||||
{
|
||||
"appenders":
|
||||
[
|
||||
"apollo",
|
||||
"apolloOut",
|
||||
"errors"
|
||||
],
|
||||
"level": "debug",
|
||||
"enableCallStack": true
|
||||
},
|
||||
"backend":
|
||||
{
|
||||
"appenders":
|
||||
[
|
||||
"backend",
|
||||
"out",
|
||||
"errors"
|
||||
],
|
||||
"level": "debug",
|
||||
"enableCallStack": true
|
||||
},
|
||||
"federation":
|
||||
{
|
||||
"appenders":
|
||||
[
|
||||
"federation",
|
||||
"out",
|
||||
"errors"
|
||||
],
|
||||
"level": "debug",
|
||||
"enableCallStack": true
|
||||
},
|
||||
"http":
|
||||
{
|
||||
"appenders":
|
||||
[
|
||||
"access"
|
||||
],
|
||||
"level": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
49
federation/package.json
Normal file
49
federation/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "gradido-federation",
|
||||
"version": "1.0.0",
|
||||
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/federation",
|
||||
"author": "Claus-Peter Huebner",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"clean": "tsc --build --clean",
|
||||
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
|
||||
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-server-express": "^2.25.2",
|
||||
"class-validator": "^0.13.2",
|
||||
"cors": "2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"dotenv": "10.0.0",
|
||||
"express": "4.17.1",
|
||||
"graphql": "15.5.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"log4js": "^6.7.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.1",
|
||||
"type-graphql": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||
"@typescript-eslint/parser": "^4.28.0",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"@types/node": "^16.10.3",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"prettier": "^2.3.1",
|
||||
"typescript": "^4.3.4",
|
||||
"nodemon": "^2.0.7"
|
||||
}
|
||||
}
|
||||
94
federation/src/config/index.ts
Normal file
94
federation/src/config/index.ts
Normal file
@ -0,0 +1,94 @@
|
||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
/*
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
Decimal.set({
|
||||
precision: 25,
|
||||
rounding: Decimal.ROUND_HALF_UP,
|
||||
})
|
||||
*/
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0059-add_hide_amount_to_users',
|
||||
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v1.2023-01-09',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
const server = {
|
||||
PORT: process.env.PORT || 5000,
|
||||
// JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||
// JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
|
||||
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||
// GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||
}
|
||||
const database = {
|
||||
DB_HOST: process.env.DB_HOST || 'localhost',
|
||||
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
|
||||
DB_USER: process.env.DB_USER || 'root',
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || '',
|
||||
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
|
||||
TYPEORM_LOGGING_RELATIVE_PATH:
|
||||
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
|
||||
}
|
||||
/*
|
||||
const community = {
|
||||
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
|
||||
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
|
||||
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
|
||||
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL:
|
||||
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
|
||||
COMMUNITY_DESCRIPTION:
|
||||
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
}
|
||||
*/
|
||||
// const eventProtocol = {
|
||||
// global switch to enable writing of EventProtocol-Entries
|
||||
// EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
|
||||
// }
|
||||
|
||||
// This is needed by graphql-directive-auth
|
||||
// process.env.APP_SECRET = server.JWT_SECRET
|
||||
|
||||
// Check config version
|
||||
constants.CONFIG_VERSION.CURRENT =
|
||||
process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
|
||||
if (
|
||||
![
|
||||
constants.CONFIG_VERSION.EXPECTED,
|
||||
constants.CONFIG_VERSION.DEFAULT,
|
||||
].includes(constants.CONFIG_VERSION.CURRENT)
|
||||
) {
|
||||
throw new Error(
|
||||
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`
|
||||
)
|
||||
}
|
||||
|
||||
const federation = {
|
||||
// FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
|
||||
// FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
|
||||
FEDERATION_PORT: process.env.FEDERATION_PORT || 5000,
|
||||
FEDERATION_API: process.env.FEDERATION_API || '1_0',
|
||||
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
...constants,
|
||||
...server,
|
||||
...database,
|
||||
// ...community,
|
||||
// ...eventProtocol,
|
||||
...federation,
|
||||
}
|
||||
|
||||
export default CONFIG
|
||||
12
federation/src/graphql/api/1_0/resolver/Test2Resolver.ts
Normal file
12
federation/src/graphql/api/1_0/resolver/Test2Resolver.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Query, Resolver } from 'type-graphql'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
import { GetTestApiResult } from '../../GetTestApiResult'
|
||||
|
||||
@Resolver()
|
||||
export class Test2Resolver {
|
||||
@Query(() => GetTestApiResult)
|
||||
async test2(): Promise<GetTestApiResult> {
|
||||
logger.info(`test api 2 1_0`)
|
||||
return new GetTestApiResult('1_0')
|
||||
}
|
||||
}
|
||||
12
federation/src/graphql/api/1_0/resolver/TestResolver.ts
Normal file
12
federation/src/graphql/api/1_0/resolver/TestResolver.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
import { GetTestApiResult } from '../../GetTestApiResult'
|
||||
|
||||
@Resolver()
|
||||
export class TestResolver {
|
||||
@Query(() => GetTestApiResult)
|
||||
async test(): Promise<GetTestApiResult> {
|
||||
logger.info(`test api 1_0`)
|
||||
return new GetTestApiResult('1_0')
|
||||
}
|
||||
}
|
||||
12
federation/src/graphql/api/1_1/resolver/TestResolver.ts
Normal file
12
federation/src/graphql/api/1_1/resolver/TestResolver.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
import { GetTestApiResult } from '../../GetTestApiResult'
|
||||
|
||||
@Resolver()
|
||||
export class TestResolver {
|
||||
@Query(() => GetTestApiResult)
|
||||
async test(): Promise<GetTestApiResult> {
|
||||
logger.info(`test api 1_1`)
|
||||
return new GetTestApiResult('1_1')
|
||||
}
|
||||
}
|
||||
12
federation/src/graphql/api/2_0/resolver/TestResolver.ts
Normal file
12
federation/src/graphql/api/2_0/resolver/TestResolver.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
import { GetTestApiResult } from '../../GetTestApiResult'
|
||||
|
||||
@Resolver()
|
||||
export class TestResolver {
|
||||
@Query(() => GetTestApiResult)
|
||||
async test(): Promise<GetTestApiResult> {
|
||||
logger.info(`test api 2_0`)
|
||||
return new GetTestApiResult('2_0')
|
||||
}
|
||||
}
|
||||
11
federation/src/graphql/api/GetTestApiResult.ts
Normal file
11
federation/src/graphql/api/GetTestApiResult.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Field, ObjectType } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class GetTestApiResult {
|
||||
constructor(apiVersion: string) {
|
||||
this.api = apiVersion
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
api: string
|
||||
}
|
||||
12
federation/src/graphql/api/schema.ts
Normal file
12
federation/src/graphql/api/schema.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import path from 'path'
|
||||
// config
|
||||
import CONFIG from '../../config'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
|
||||
export const getApiResolvers = (): string => {
|
||||
logger.info(`getApiResolvers...${CONFIG.FEDERATION_API}`)
|
||||
return path.join(
|
||||
__dirname,
|
||||
`./${CONFIG.FEDERATION_API}/resolver/*Resolver.{ts,js}`
|
||||
)
|
||||
}
|
||||
23
federation/src/graphql/scalar/Decimal.ts
Normal file
23
federation/src/graphql/scalar/Decimal.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { GraphQLScalarType, Kind } from 'graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export default new GraphQLScalarType({
|
||||
name: 'Decimal',
|
||||
description: 'The `Decimal` scalar type to represent currency values',
|
||||
|
||||
serialize(value: Decimal) {
|
||||
return value.toString()
|
||||
},
|
||||
|
||||
parseValue(value) {
|
||||
return new Decimal(value)
|
||||
},
|
||||
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new TypeError(`${String(ast)} is not a valid decimal value.`)
|
||||
}
|
||||
|
||||
return new Decimal(ast.value)
|
||||
},
|
||||
})
|
||||
17
federation/src/graphql/schema.ts
Normal file
17
federation/src/graphql/schema.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { GraphQLSchema } from 'graphql'
|
||||
import { buildSchema } from 'type-graphql'
|
||||
|
||||
// import isAuthorized from './directive/isAuthorized'
|
||||
import DecimalScalar from './scalar/Decimal'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { getApiResolvers } from './api/schema'
|
||||
|
||||
const schema = async (): Promise<GraphQLSchema> => {
|
||||
return await buildSchema({
|
||||
resolvers: [getApiResolvers()],
|
||||
// authChecker: isAuthorized,
|
||||
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
|
||||
})
|
||||
}
|
||||
|
||||
export default schema
|
||||
33
federation/src/index.ts
Normal file
33
federation/src/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import createServer from './server/createServer'
|
||||
|
||||
// config
|
||||
import CONFIG from './config'
|
||||
|
||||
async function main() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`FEDERATION_PORT=${CONFIG.FEDERATION_PORT}`)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`FEDERATION_API=${CONFIG.FEDERATION_API}`)
|
||||
const { app } = await createServer()
|
||||
|
||||
app.listen(CONFIG.FEDERATION_PORT, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`Server is running at http://localhost:${CONFIG.FEDERATION_PORT}`
|
||||
)
|
||||
if (CONFIG.GRAPHIQL) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`GraphIQL available at http://localhost:${CONFIG.FEDERATION_PORT}`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
8
federation/src/server/cors.ts
Normal file
8
federation/src/server/cors.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import cors from 'cors'
|
||||
|
||||
const corsOptions = {
|
||||
origin: '*',
|
||||
exposedHeaders: ['token'],
|
||||
}
|
||||
|
||||
export default cors(corsOptions)
|
||||
91
federation/src/server/createServer.ts
Normal file
91
federation/src/server/createServer.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import express, { Express } from 'express'
|
||||
|
||||
// database
|
||||
import connection from '@/typeorm/connection'
|
||||
import { checkDBVersion } from '@/typeorm/DBVersion'
|
||||
|
||||
// server
|
||||
import cors from './cors'
|
||||
// import serverContext from './context'
|
||||
import plugins from './plugins'
|
||||
|
||||
// config
|
||||
import CONFIG from '@/config'
|
||||
|
||||
// graphql
|
||||
import schema from '@/graphql/schema'
|
||||
|
||||
// webhooks
|
||||
// import { elopageWebhook } from '@/webhook/elopage'
|
||||
import { Connection } from '@dbTools/typeorm'
|
||||
|
||||
import { apolloLogger } from './logger'
|
||||
import { Logger } from 'log4js'
|
||||
|
||||
// i18n
|
||||
// import { i18n } from './localization'
|
||||
|
||||
// TODO implement
|
||||
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
||||
|
||||
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
|
||||
|
||||
const createServer = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// context: any = serverContext,
|
||||
logger: Logger = apolloLogger
|
||||
// localization: i18n.I18n = i18n,
|
||||
): Promise<ServerDef> => {
|
||||
logger.addContext('user', 'unknown')
|
||||
logger.debug('createServer...')
|
||||
|
||||
// open mysql connection
|
||||
const con = await connection()
|
||||
if (!con || !con.isConnected) {
|
||||
logger.fatal(`Couldn't open connection to database!`)
|
||||
throw new Error(`Fatal: Couldn't open connection to database`)
|
||||
}
|
||||
|
||||
// check for correct database version
|
||||
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
|
||||
if (!dbVersion) {
|
||||
logger.fatal('Fatal: Database Version incorrect')
|
||||
throw new Error('Fatal: Database Version incorrect')
|
||||
}
|
||||
|
||||
// Express Server
|
||||
const app = express()
|
||||
|
||||
// cors
|
||||
app.use(cors)
|
||||
|
||||
// bodyparser json
|
||||
app.use(express.json())
|
||||
// bodyparser urlencoded for elopage
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
// i18n
|
||||
// app.use(localization.init)
|
||||
|
||||
// Elopage Webhook
|
||||
// app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
||||
|
||||
// Apollo Server
|
||||
const apollo = new ApolloServer({
|
||||
schema: await schema(),
|
||||
// playground: CONFIG.GRAPHIQL,
|
||||
// introspection: CONFIG.GRAPHIQL,
|
||||
// context,
|
||||
plugins,
|
||||
logger,
|
||||
})
|
||||
apollo.applyMiddleware({ app, path: '/' })
|
||||
logger.debug('createServer...successful')
|
||||
|
||||
return { apollo, app, con }
|
||||
}
|
||||
|
||||
export default createServer
|
||||
43
federation/src/server/logger.ts
Normal file
43
federation/src/server/logger.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import log4js from 'log4js'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
|
||||
|
||||
options.categories.backend.level = CONFIG.LOG_LEVEL
|
||||
options.categories.apollo.level = CONFIG.LOG_LEVEL
|
||||
let filename: string = options.appenders.federation.filename
|
||||
options.appenders.federation.filename = filename
|
||||
.replace('%v', CONFIG.FEDERATION_API)
|
||||
.replace('%p', CONFIG.FEDERATION_PORT.toString())
|
||||
filename = options.appenders.access.filename
|
||||
options.appenders.access.filename = filename.replace(
|
||||
'%p',
|
||||
CONFIG.FEDERATION_PORT.toString()
|
||||
)
|
||||
filename = options.appenders.apollo.filename
|
||||
options.appenders.apollo.filename = filename.replace(
|
||||
'%p',
|
||||
CONFIG.FEDERATION_PORT.toString()
|
||||
)
|
||||
filename = options.appenders.backend.filename
|
||||
options.appenders.backend.filename = filename.replace(
|
||||
'%p',
|
||||
CONFIG.FEDERATION_PORT.toString()
|
||||
)
|
||||
filename = options.appenders.errorFile.filename
|
||||
options.appenders.errorFile.filename = filename.replace(
|
||||
'%p',
|
||||
CONFIG.FEDERATION_PORT.toString()
|
||||
)
|
||||
|
||||
log4js.configure(options)
|
||||
|
||||
const apolloLogger = log4js.getLogger('apollo')
|
||||
// const backendLogger = log4js.getLogger('backend')
|
||||
const federationLogger = log4js.getLogger('federation')
|
||||
|
||||
// backendLogger.addContext('user', 'unknown')
|
||||
|
||||
export { apolloLogger, federationLogger }
|
||||
68
federation/src/server/plugins.ts
Normal file
68
federation/src/server/plugins.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
|
||||
const setHeadersPlugin = {
|
||||
requestDidStart() {
|
||||
return {
|
||||
willSendResponse(requestContext: any) {
|
||||
const { setHeaders = [] } = requestContext.context
|
||||
setHeaders.forEach(({ key, value }: { [key: string]: string }) => {
|
||||
if (requestContext.response.http.headers.get(key)) {
|
||||
requestContext.response.http.headers.set(key, value)
|
||||
} else {
|
||||
requestContext.response.http.headers.append(key, value)
|
||||
}
|
||||
})
|
||||
return requestContext
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const filterVariables = (variables: any) => {
|
||||
const vars = clonedeep(variables)
|
||||
if (vars.password) vars.password = '***'
|
||||
if (vars.passwordNew) vars.passwordNew = '***'
|
||||
return vars
|
||||
}
|
||||
|
||||
const logPlugin = {
|
||||
requestDidStart(requestContext: any) {
|
||||
const { logger } = requestContext
|
||||
const { query, mutation, variables, operationName } = requestContext.request
|
||||
if (operationName !== 'IntrospectionQuery') {
|
||||
logger.info(`Request:
|
||||
${mutation || query}variables: ${JSON.stringify(
|
||||
filterVariables(variables),
|
||||
null,
|
||||
2
|
||||
)}`)
|
||||
}
|
||||
return {
|
||||
willSendResponse(requestContext: any) {
|
||||
if (operationName !== 'IntrospectionQuery') {
|
||||
if (requestContext.context.user)
|
||||
logger.info(`User ID: ${requestContext.context.user.id}`)
|
||||
if (requestContext.response.data) {
|
||||
logger.info('Response Success!')
|
||||
logger.trace(`Response-Data:
|
||||
${JSON.stringify(requestContext.response.data, null, 2)}`)
|
||||
}
|
||||
if (requestContext.response.errors)
|
||||
logger.error(`Response-Errors:
|
||||
${JSON.stringify(requestContext.response.errors, null, 2)}`)
|
||||
}
|
||||
return requestContext
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const plugins =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? [setHeadersPlugin]
|
||||
: [setHeadersPlugin, logPlugin]
|
||||
|
||||
export default plugins
|
||||
27
federation/src/typeorm/DBVersion.ts
Normal file
27
federation/src/typeorm/DBVersion.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Migration } from '@entity/Migration'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
|
||||
const getDBVersion = async (): Promise<string | null> => {
|
||||
try {
|
||||
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
|
||||
return dbVersion ? dbVersion.fileName : null
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
||||
const dbVersion = await getDBVersion()
|
||||
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
|
||||
logger.error(
|
||||
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
|
||||
dbVersion || 'None'
|
||||
}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export { checkDBVersion, getDBVersion }
|
||||
34
federation/src/typeorm/connection.ts
Normal file
34
federation/src/typeorm/connection.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// TODO This is super weird - since the entities are defined in another project they have their own globals.
|
||||
// We cannot use our connection here, but must use the external typeorm installation
|
||||
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
|
||||
import CONFIG from '@/config'
|
||||
import { entities } from '@entity/index'
|
||||
|
||||
const connection = async (): Promise<Connection | null> => {
|
||||
try {
|
||||
return createConnection({
|
||||
name: 'default',
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
entities,
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default connection
|
||||
90
federation/tsconfig.json
Normal file
90
federation/tsconfig.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./build", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
|
||||
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
"@/*": ["src/*"],
|
||||
// "@arg/*": ["src/graphql/arg/*"],
|
||||
// "@enum/*": ["src/graphql/enum/*"],
|
||||
// "@model/*": ["src/graphql/model/*"],
|
||||
"@repository/*": ["src/typeorm/repository/*"],
|
||||
// "@test/*": ["test/*"],
|
||||
/* external */
|
||||
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
|
||||
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
|
||||
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
|
||||
},
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
"typeRoots": ["src/dht_node/@types", "node_modules/@types"], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../database/tsconfig.json",
|
||||
// add 'prepend' if you want to include the referenced project in your output file
|
||||
// "prepend": true
|
||||
}
|
||||
]
|
||||
}
|
||||
3247
federation/yarn.lock
Normal file
3247
federation/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -66,6 +66,7 @@
|
||||
"vuex-persistedstate": "^4.0.0-beta.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/client": "^3.7.4",
|
||||
"@intlify/eslint-plugin-vue-i18n": "^1.4.0",
|
||||
"@vue/cli-plugin-babel": "^3.7.0",
|
||||
"@vue/cli-plugin-eslint": "^3.7.0",
|
||||
@ -76,6 +77,7 @@
|
||||
"babel-plugin-transform-require-context": "^0.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"mock-apollo-client": "^1.2.1",
|
||||
"postcss": "^8.4.8",
|
||||
"postcss-html": "^1.3.0",
|
||||
"postcss-scss": "^4.0.3",
|
||||
|
||||
@ -33,6 +33,10 @@
|
||||
<div class="small">
|
||||
{{ $t('creation') }} {{ $t('(') }}{{ amount / 20 }} {{ $t('h') }}{{ $t(')') }}
|
||||
</div>
|
||||
<div v-if="state === 'DENIED' && allContribution" class="font-weight-bold">
|
||||
<b-icon icon="x-circle" variant="danger"></b-icon>
|
||||
{{ $t('contribution.alert.denied') }}
|
||||
</div>
|
||||
<div v-if="state === 'DELETED'" class="small">
|
||||
{{ $t('contribution.deleted') }}
|
||||
</div>
|
||||
@ -146,6 +150,14 @@ export default {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
deniedBy: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
deniedAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
state: {
|
||||
type: String,
|
||||
required: false,
|
||||
@ -175,12 +187,14 @@ export default {
|
||||
computed: {
|
||||
icon() {
|
||||
if (this.deletedAt) return 'trash'
|
||||
if (this.deniedAt) return 'x-circle'
|
||||
if (this.confirmedAt) return 'check'
|
||||
if (this.state === 'IN_PROGRESS') return 'question-circle'
|
||||
return 'bell-fill'
|
||||
},
|
||||
variant() {
|
||||
if (this.deletedAt) return 'danger'
|
||||
if (this.deniedAt) return 'warning'
|
||||
if (this.confirmedAt) return 'success'
|
||||
if (this.state === 'IN_PROGRESS') return 'f5'
|
||||
return 'primary'
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
<div class="transaction-form">
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<b-card class="appBoxShadow gradido-border-radius" body-class="p-3">
|
||||
<b-card class="appBoxShadow gradido-border-radius" body-class="p-4">
|
||||
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
|
||||
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)" @reset="onReset">
|
||||
<b-form-radio-group v-model="radioSelected" class="container">
|
||||
<b-form-radio-group v-model="radioSelected">
|
||||
<b-row class="mb-4">
|
||||
<b-col cols="12" lg="6">
|
||||
<b-row class="bg-248 gradido-border-radius pt-lg-2 mr-lg-2">
|
||||
@ -39,14 +39,13 @@
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<div class="mt-4 mb-4" v-if="radioSelected === sendTypes.link">
|
||||
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
|
||||
<div>
|
||||
{{ $t('gdd_per_link.choose-amount') }}
|
||||
</div>
|
||||
</div>
|
||||
</b-form-radio-group>
|
||||
<div class="mt-4 mb-4" v-if="radioSelected === sendTypes.link">
|
||||
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
|
||||
<div>
|
||||
{{ $t('gdd_per_link.choose-amount') }}
|
||||
</div>
|
||||
</div>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-row>
|
||||
@ -106,7 +105,10 @@
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6" class="text-lg-right">
|
||||
<b-button block type="submit" variant="gradido">
|
||||
{{ $t('form.check_now') }}
|
||||
<span v-if="radioSelected === sendTypes.send">{{ $t('form.check_now') }}</span>
|
||||
<span v-if="radioSelected === sendTypes.link">
|
||||
{{ $t('form.generate_now') }}
|
||||
</span>
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
@ -55,7 +55,7 @@ describe('ContributionInfo', () => {
|
||||
expect(listItems.at(2).text()).toBe('contribution.alert.confirm')
|
||||
|
||||
expect(listItems.at(3).find('svg').attributes('aria-label')).toEqual('x circle')
|
||||
expect(listItems.at(3).text()).toBe('contribution.alert.rejected')
|
||||
expect(listItems.at(3).text()).toBe('contribution.alert.denied')
|
||||
})
|
||||
})
|
||||
|
||||
@ -78,8 +78,14 @@ describe('ContributionInfo', () => {
|
||||
expect(listItems.at(0).find('svg').attributes('aria-label')).toEqual('bell fill')
|
||||
expect(listItems.at(0).text()).toBe('contribution.alert.pending')
|
||||
|
||||
expect(listItems.at(1).find('svg').attributes('aria-label')).toEqual('check')
|
||||
expect(listItems.at(1).text()).toBe('contribution.alert.confirm')
|
||||
expect(listItems.at(1).find('svg').attributes('aria-label')).toEqual('question square')
|
||||
expect(listItems.at(1).text()).toBe('contribution.alert.in_progress')
|
||||
|
||||
expect(listItems.at(2).find('svg').attributes('aria-label')).toEqual('check')
|
||||
expect(listItems.at(2).text()).toBe('contribution.alert.confirm')
|
||||
|
||||
expect(listItems.at(3).find('svg').attributes('aria-label')).toEqual('x circle')
|
||||
expect(listItems.at(3).text()).toBe('contribution.alert.denied')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -19,8 +19,8 @@
|
||||
{{ $t('contribution.alert.confirm') }}
|
||||
</li>
|
||||
<li>
|
||||
<b-icon icon="x-circle" variant="danger"></b-icon>
|
||||
{{ $t('contribution.alert.rejected') }}
|
||||
<b-icon icon="x-circle" variant="warning"></b-icon>
|
||||
{{ $t('contribution.alert.denied') }}
|
||||
</li>
|
||||
<li>
|
||||
<b-icon icon="trash" variant="danger"></b-icon>
|
||||
@ -38,10 +38,18 @@
|
||||
<b-icon icon="bell-fill" variant="primary"></b-icon>
|
||||
{{ $t('contribution.alert.pending') }}
|
||||
</li>
|
||||
<li>
|
||||
<b-icon icon="question-square" variant="warning"></b-icon>
|
||||
{{ $t('contribution.alert.in_progress') }}
|
||||
</li>
|
||||
<li>
|
||||
<b-icon icon="check" variant="success"></b-icon>
|
||||
{{ $t('contribution.alert.confirm') }}
|
||||
</li>
|
||||
<li>
|
||||
<b-icon icon="x-circle" variant="warning"></b-icon>
|
||||
{{ $t('contribution.alert.denied') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="hash === '#edit'" show fade variant="secondary" class="text-dark">
|
||||
|
||||
@ -183,6 +183,8 @@ export const listContributions = gql`
|
||||
deletedAt
|
||||
state
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -202,6 +204,10 @@ export const listAllContributions = gql`
|
||||
contributionDate
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
state
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,16 +42,16 @@
|
||||
"communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.",
|
||||
"confirm": "bestätigt",
|
||||
"deleted": "gelöscht",
|
||||
"denied": "abgelehnt",
|
||||
"in_progress": "Es gibt eine Rückfrage der Moderatoren.",
|
||||
"myContributionNoteList": "Eingereichte Beiträge, die noch nicht bestätigt wurden, kannst du jederzeit bearbeiten oder löschen.",
|
||||
"pending": "Eingereicht und wartet auf Bestätigung",
|
||||
"rejected": "abgelehnt"
|
||||
"pending": "Eingereicht und wartet auf Bestätigung"
|
||||
},
|
||||
"delete": "Beitrag löschen! Bist du sicher?",
|
||||
"deleted": "Der Beitrag wurde gelöscht! Wird aber sichtbar bleiben.",
|
||||
"formText": {
|
||||
"bringYourTalentsTo": "Bring dich mit deinen Talenten in die Gemeinschaft ein! Dein freiwilliges Engagement honorieren wir mit 20 GDD pro Stunde bis maximal 1.000 GDD im Monat.",
|
||||
"describeYourCommunity": "Beschreibe deine Gemeinwohl-Tätigkeit mit Angabe der Stunden und trage einen Betrag von 20 GDD pro Stunde ein! Nach Bestätigung durch einen Moderator wird der Betrag deinem Konto gutgeschrieben.",
|
||||
"describeYourCommunity": "Beschreibe deine Gemeinwohl-Tätigkeit und gib die Anzahl der Stunden an! Der Betrag von 20 GDD pro Stunde wird automatisch berechnet. Nach Bestätigung durch einen Moderator wird der Betrag deinem Konto gutgeschrieben.",
|
||||
"maxGDDforMonth": "Du kannst für den ausgewählten Monat nur noch maximal {amount} GDD einreichen.",
|
||||
"openAmountForMonth": "Für <b>{monthAndYear}</b> kannst du noch <b>{creation}</b> GDD einreichen.",
|
||||
"yourContribution": "Dein Beitrag zum Gemeinwohl"
|
||||
@ -251,7 +251,7 @@
|
||||
},
|
||||
"openHours": "Offene Stunden",
|
||||
"pageTitle": {
|
||||
"community": "Meine Gemeinschaft",
|
||||
"community": "Gradido schöpfen",
|
||||
"gdt": "Deine GDT Transaktionen",
|
||||
"information": "{community}",
|
||||
"overview": "Willkommen {name}",
|
||||
|
||||
@ -42,19 +42,19 @@
|
||||
"communityNoteList": "Here you will find all submitted and confirmed contributions from all members of this community.",
|
||||
"confirm": "confirmed",
|
||||
"deleted": "deleted",
|
||||
"denied": "rejected",
|
||||
"in_progress": "There is a question from the moderators.",
|
||||
"myContributionNoteList": "You can edit or delete entries that have not yet been confirmed at any time.",
|
||||
"pending": "Submitted and waiting for confirmation",
|
||||
"rejected": "deleted"
|
||||
"pending": "Submitted and waiting for confirmation"
|
||||
},
|
||||
"delete": "Delete Contribution! Are you sure?",
|
||||
"deleted": "The contribution has been deleted! But it will remain visible.",
|
||||
"formText": {
|
||||
"bringYourTalentsTo": "Bring your talents to the community! Your voluntary commitment will be rewarded with 20 GDD per hour up to a maximum of 1,000 GDD per month.",
|
||||
"describeYourCommunity": "Describe your community service activity with hours and enter an amount of 20 GDD per hour! After confirmation by a moderator, the amount will be credited to your account.",
|
||||
"bringYourTalentsTo": "Bring your talents to the community! We reward your voluntary commitment with 20 GDD per hour up to a maximum of 1,000 GDD per month.",
|
||||
"describeYourCommunity": "Describe your community service activity and specify the number of hours! The amount of 20 GDD per hour will be calculated automatically. After confirmation by a moderator, the amount will be credited to your account.",
|
||||
"maxGDDforMonth": "You can only submit a maximum of {amount} GDD for the selected month.",
|
||||
"openAmountForMonth": "For <b>{monthAndYear}</b>, you can still submit <b>{creation}</b> GDD.",
|
||||
"yourContribution": "Your contribution to the common good"
|
||||
"yourContribution": "Your Contributions to the Common Good"
|
||||
},
|
||||
"lastContribution": "Last Contributions",
|
||||
"noDateSelected": "Choose any date in the month",
|
||||
@ -251,7 +251,7 @@
|
||||
},
|
||||
"openHours": "Open Hours",
|
||||
"pageTitle": {
|
||||
"community": "My community",
|
||||
"community": "Create Gradido",
|
||||
"gdt": "Your GDT transactions",
|
||||
"information": "{community}",
|
||||
"overview": "Welcome {name}",
|
||||
|
||||
@ -39,10 +39,10 @@
|
||||
"answerQuestion": "Por favor, contesta las preguntas",
|
||||
"communityNoteList": "Aquí encontrarás todas las contribuciones enviadas y confirmadas de todos los miembros de esta comunidad.",
|
||||
"confirm": "confirmado",
|
||||
"denied": "rechazado",
|
||||
"in_progress": "Hay una pregunta de los moderatores.",
|
||||
"myContributionNoteList": "Puedes editar o eliminar las contribuciones enviadas que aún no han sido confirmadas en cualquier momento.",
|
||||
"pending": "Enviado y a la espera de confirmación",
|
||||
"rejected": "rechazado"
|
||||
"pending": "Enviado y a la espera de confirmación"
|
||||
},
|
||||
"date": "Contribución para:",
|
||||
"delete": "Eliminar la contribución. ¿Estás seguro?",
|
||||
|
||||
@ -41,10 +41,10 @@
|
||||
"communityNoteList": "Vous trouverez ci-contre toutes les contributions versées et certifiées de tous les membres de cette communauté.",
|
||||
"confirm": "Approuvé",
|
||||
"deleted": "Supprimé",
|
||||
"denied": "supprimé",
|
||||
"in_progress": "Il y a une question du modérateur.",
|
||||
"myContributionNoteList": "À tout moment vous pouvez éditer ou supprimer les données qui n´ont pas été confirmées.",
|
||||
"pending": "Inscription en attente de validation",
|
||||
"rejected": "supprimé"
|
||||
"pending": "Inscription en attente de validation"
|
||||
},
|
||||
"date": "Contribution pour:",
|
||||
"delete": "Supprimer la contribution! Êtes-vous sûr?",
|
||||
|
||||
@ -39,10 +39,10 @@
|
||||
"answerQuestion": "Please answer the question",
|
||||
"communityNoteList": "Hier vind je alle ingediende en bevestigde bijdragen van alle leden uit deze gemeenschap.",
|
||||
"confirm": "bevestigt",
|
||||
"denied": "afgewezen",
|
||||
"in_progress": "There is a question from the moderators.",
|
||||
"myContributionNoteList": "Ingediende bijdragen, die nog niet bevestigd zijn, kun je op elk moment wijzigen of verwijderen.",
|
||||
"pending": "Ingediend en wacht op bevestiging",
|
||||
"rejected": "afgewezen"
|
||||
"pending": "Ingediend en wacht op bevestiging"
|
||||
},
|
||||
"date": "Bijdrage voor:",
|
||||
"delete": "Bijdrage verwijderen! Weet je het zeker?",
|
||||
|
||||
@ -34,10 +34,10 @@
|
||||
"alert": {
|
||||
"communityNoteList": "Burada, bu topluluğun tüm üyelerinden gönderilen ve onaylanan bütün faydalı hizmetleri bulacaksın.",
|
||||
"confirm": "onaylandı",
|
||||
"denied": "reddedildi",
|
||||
"myContributionNoteList": "Bildirmiş olduğun henüz onaylanmamış olan faaliyetleri istediğin zaman düzenleyebilir veya silebilirsin.",
|
||||
"myContributionNoteSupport": "Yakın zamanda moderatörlerle aranda bir diyalog olasılığı olacak. Şu anda herhangi bir sorun yaşıyorsan, lütfen destek hattına başvur.",
|
||||
"pending": "Gönderildi ve onay bekleniyor",
|
||||
"rejected": "reddedildi"
|
||||
"pending": "Gönderildi ve onay bekleniyor"
|
||||
},
|
||||
"date": "Hizmet:",
|
||||
"delete": "Hizmeti sil! Emin misin?",
|
||||
|
||||
@ -1,58 +1,59 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Community from './Community'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
|
||||
import { toastErrorSpy, toastSuccessSpy, toastInfoSpy } from '@test/testSetup'
|
||||
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
||||
import { listContributions, listAllContributions } from '@/graphql/queries'
|
||||
import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
|
||||
import { createMockClient } from 'mock-apollo-client'
|
||||
import VueApollo from 'vue-apollo'
|
||||
|
||||
const mockClient = createMockClient()
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: mockClient,
|
||||
})
|
||||
|
||||
const localVue = global.localVue
|
||||
localVue.use(VueApollo)
|
||||
|
||||
const mockStoreDispach = jest.fn()
|
||||
const apolloQueryMock = jest.fn()
|
||||
const apolloMutationMock = jest.fn()
|
||||
const apolloRefetchMock = jest.fn()
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
describe('Community', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
mutate: apolloMutationMock,
|
||||
queries: {
|
||||
OpenCreations: {
|
||||
refetch: apolloRefetchMock,
|
||||
mockClient.setRequestHandler(
|
||||
openCreations,
|
||||
jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce({ message: 'Open Creations failed!' })
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
openCreations: [
|
||||
{
|
||||
month: 0,
|
||||
year: 2023,
|
||||
amount: '1000',
|
||||
},
|
||||
{
|
||||
month: 1,
|
||||
year: 2023,
|
||||
amount: '1000',
|
||||
},
|
||||
{
|
||||
month: 2,
|
||||
year: 2023,
|
||||
amount: '1000',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
$store: {
|
||||
dispatch: mockStoreDispach,
|
||||
state: {
|
||||
creation: ['1000', '1000', '1000'],
|
||||
},
|
||||
},
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
$router: {
|
||||
push: jest.fn(),
|
||||
},
|
||||
$route: {
|
||||
hash: 'my',
|
||||
},
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Community, {
|
||||
localVue,
|
||||
mocks,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
apolloQueryMock.mockResolvedValue({
|
||||
mockClient.setRequestHandler(
|
||||
listContributions,
|
||||
jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce({ message: 'List Contributions failed!' })
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
listContributions: {
|
||||
contributionList: [
|
||||
@ -64,10 +65,40 @@ describe('Community', () => {
|
||||
deletedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
firstName: 'Bibi',
|
||||
contributionDate: '2022-07-15T08:47:06.000Z',
|
||||
lastName: 'Bloxberg',
|
||||
state: 'IN_PROGRESS',
|
||||
messagesCount: 0,
|
||||
},
|
||||
{
|
||||
id: 1550,
|
||||
amount: '200',
|
||||
memo: 'Fleisig, fleisig am Arbeiten gewesen',
|
||||
createdAt: '2022-07-15T08:47:06.000Z',
|
||||
deletedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
firstName: 'Bibi',
|
||||
contributionDate: '2022-06-15T08:47:06.000Z',
|
||||
lastName: 'Bloxberg',
|
||||
state: 'CONFIRMED',
|
||||
messagesCount: 0,
|
||||
},
|
||||
],
|
||||
contributionCount: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
listAllContributions,
|
||||
jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce({ message: 'List All Contributions failed!' })
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
listAllContributions: {
|
||||
contributionList: [
|
||||
{
|
||||
@ -75,29 +106,137 @@ describe('Community', () => {
|
||||
amount: '200',
|
||||
memo: 'Fleisig, fleisig am Arbeiten mein lieber Freund, 50 Zeichen sind viel',
|
||||
createdAt: '2022-07-15T08:47:06.000Z',
|
||||
contributionDate: '2022-07-15T08:47:06.000Z',
|
||||
deletedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
},
|
||||
{
|
||||
id: 1550,
|
||||
amount: '200',
|
||||
memo: 'Fleisig, fleisig am Arbeiten gewesen',
|
||||
createdAt: '2022-07-15T08:47:06.000Z',
|
||||
deletedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
firstName: 'Bibi',
|
||||
contributionDate: '2022-06-15T08:47:06.000Z',
|
||||
lastName: 'Bloxberg',
|
||||
messagesCount: 0,
|
||||
},
|
||||
{
|
||||
id: 1556,
|
||||
amount: '400',
|
||||
memo: 'Ein anderer lieber Freund ist auch sehr felißig am Arbeiten!!!!',
|
||||
createdAt: '2022-07-16T08:47:06.000Z',
|
||||
contributionDate: '2022-07-16T08:47:06.000Z',
|
||||
deletedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
firstName: 'Bob',
|
||||
lastName: 'der Baumeister',
|
||||
},
|
||||
],
|
||||
contributionCount: 2,
|
||||
contributionCount: 3,
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
createContribution,
|
||||
jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce({ message: 'Create Contribution failed!' })
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
createContribution: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
updateContribution,
|
||||
jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce({ message: 'Update Contribution failed!' })
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
updateContribution: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
deleteContribution,
|
||||
jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce({ message: 'Delete Contribution failed!' })
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
deleteContribution: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$store: {
|
||||
dispatch: mockStoreDispach,
|
||||
state: {
|
||||
user: {
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
},
|
||||
},
|
||||
},
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
$router: {
|
||||
push: routerPushMock,
|
||||
},
|
||||
$route: {
|
||||
hash: '#edit',
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Community, {
|
||||
localVue,
|
||||
mocks,
|
||||
apolloProvider,
|
||||
})
|
||||
}
|
||||
|
||||
let apolloMutateSpy
|
||||
let refetchContributionsSpy
|
||||
let refetchAllContributionsSpy
|
||||
let refetchOpenCreationsSpy
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
apolloMutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate')
|
||||
refetchContributionsSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListContributions, 'refetch')
|
||||
refetchAllContributionsSpy = jest.spyOn(
|
||||
wrapper.vm.$apollo.queries.ListAllContributions,
|
||||
'refetch',
|
||||
)
|
||||
refetchOpenCreationsSpy = jest.spyOn(wrapper.vm.$apollo.queries.OpenCreations, 'refetch')
|
||||
})
|
||||
|
||||
it('has a DIV .community-page', () => {
|
||||
expect(wrapper.find('div.community-page').exists()).toBe(true)
|
||||
describe('server response for queries is error', () => {
|
||||
it('toasts three errors', () => {
|
||||
expect(toastErrorSpy).toBeCalledTimes(3)
|
||||
expect(toastErrorSpy).toBeCalledWith('Open Creations failed!')
|
||||
expect(toastErrorSpy).toBeCalledWith('List Contributions failed!')
|
||||
expect(toastErrorSpy).toBeCalledWith('List All Contributions failed!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('tabs', () => {
|
||||
@ -105,60 +244,49 @@ describe('Community', () => {
|
||||
expect(wrapper.findAll('div[role="tabpanel"]')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it.todo('check for correct tabIndex if state is "IN_PROGRESS" or not')
|
||||
it('check for correct tabIndex if state is "IN_PROGRESS" or not', () => {
|
||||
expect(routerPushMock).toBeCalledWith({ path: '/community#my' })
|
||||
})
|
||||
|
||||
it('toasts an info', () => {
|
||||
expect(toastInfoSpy).toBeCalledWith('contribution.alert.answerQuestionToast')
|
||||
})
|
||||
})
|
||||
|
||||
describe('API calls after creation', () => {
|
||||
it('has a DIV .community-page', () => {
|
||||
expect(wrapper.find('div.community-page').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits update transactions', () => {
|
||||
expect(wrapper.emitted('update-transactions')).toEqual([[0]])
|
||||
})
|
||||
|
||||
it('queries list of own contributions', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('queries list of all contributions', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listAllContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('server response is error', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
apolloQueryMock.mockRejectedValue({ message: 'Ups' })
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('toasts two errors', () => {
|
||||
expect(toastErrorSpy).toBeCalledTimes(2)
|
||||
expect(toastErrorSpy).toBeCalledWith('Ups')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('set contrubtion', () => {
|
||||
describe('save contrubtion', () => {
|
||||
describe('with error', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
form: {
|
||||
id: null,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '200',
|
||||
},
|
||||
})
|
||||
await wrapper.find('form').trigger('submit')
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Create Contribution failed!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with success', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockResolvedValue({
|
||||
data: {
|
||||
createContribution: true,
|
||||
},
|
||||
})
|
||||
await wrapper.setData({
|
||||
form: {
|
||||
id: null,
|
||||
@ -171,7 +299,7 @@ describe('Community', () => {
|
||||
})
|
||||
|
||||
it('calls the create contribution mutation', () => {
|
||||
expect(apolloMutationMock).toBeCalledWith({
|
||||
expect(apolloMutateSpy).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
@ -187,62 +315,49 @@ describe('Community', () => {
|
||||
})
|
||||
|
||||
it('updates the contribution list', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
expect(refetchContributionsSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloRefetchMock).toBeCalled()
|
||||
it('updates the all contribution list', () => {
|
||||
expect(refetchAllContributionsSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('set all data to the default values)', () => {
|
||||
it('updates the open creations', () => {
|
||||
expect(refetchOpenCreationsSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('sets all data to the default values)', () => {
|
||||
expect(wrapper.vm.form.id).toBe(null)
|
||||
expect(wrapper.vm.form.date).toBe('')
|
||||
expect(wrapper.vm.form.memo).toBe('')
|
||||
expect(wrapper.vm.form.amount).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockRejectedValue({
|
||||
message: 'Ouch!',
|
||||
})
|
||||
await wrapper.setData({
|
||||
form: {
|
||||
id: null,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '200',
|
||||
},
|
||||
})
|
||||
await wrapper.find('form').trigger('submit')
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update contrubtion', () => {
|
||||
describe('with error', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'ContributionForm' })
|
||||
.vm.$emit('update-contribution', {
|
||||
id: 2,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '400',
|
||||
})
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Update Contribution failed!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with success', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockResolvedValue({
|
||||
data: {
|
||||
updateContribution: true,
|
||||
},
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ name: 'ContributionForm' })
|
||||
.vm.$emit('update-contribution', {
|
||||
@ -254,7 +369,7 @@ describe('Community', () => {
|
||||
})
|
||||
|
||||
it('calls the update contribution mutation', () => {
|
||||
expect(apolloMutationMock).toBeCalledWith({
|
||||
expect(apolloMutateSpy).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
@ -271,40 +386,15 @@ describe('Community', () => {
|
||||
})
|
||||
|
||||
it('updates the contribution list', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
expect(refetchContributionsSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloRefetchMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockRejectedValue({
|
||||
message: 'Oh No!',
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ name: 'ContributionForm' })
|
||||
.vm.$emit('update-contribution', {
|
||||
id: 2,
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '400',
|
||||
})
|
||||
it('updates the all contribution list', () => {
|
||||
expect(refetchAllContributionsSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh No!')
|
||||
it('updates the open creations', () => {
|
||||
expect(refetchOpenCreationsSpy).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -314,22 +404,28 @@ describe('Community', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({ tabIndex: 1 })
|
||||
contributionListComponent = await wrapper.findComponent({ name: 'ContributionList' })
|
||||
contributionListComponent = wrapper.findComponent({ name: 'ContributionList' })
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
contributionListComponent.vm.$emit('delete-contribution', { id: 2 })
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Delete Contribution failed!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with success', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockResolvedValue({
|
||||
data: {
|
||||
deleteContribution: true,
|
||||
},
|
||||
})
|
||||
contributionListComponent.vm.$emit('delete-contribution', { id: 2 })
|
||||
await contributionListComponent.vm.$emit('delete-contribution', { id: 2 })
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutationMock).toBeCalledWith({
|
||||
expect(apolloMutateSpy).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
@ -343,37 +439,20 @@ describe('Community', () => {
|
||||
})
|
||||
|
||||
it('updates the contribution list', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
expect(refetchContributionsSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloRefetchMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutationMock.mockRejectedValue({
|
||||
message: 'Oh my god!',
|
||||
})
|
||||
contributionListComponent.vm.$emit('delete-contribution', { id: 2 })
|
||||
it('updates the all contribution list', () => {
|
||||
expect(refetchAllContributionsSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh my god!')
|
||||
it('updates the open creations', () => {
|
||||
expect(refetchOpenCreationsSpy).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('update contribution form', () => {
|
||||
describe('update contribution form', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({ tabIndex: 1 })
|
||||
@ -391,12 +470,58 @@ describe('Community', () => {
|
||||
expect(wrapper.vm.form.id).toBe(2)
|
||||
expect(wrapper.vm.form.date).toBe(now)
|
||||
expect(wrapper.vm.form.memo).toBe('Mein Beitrag zur Gemeinschaft für diesen Monat ...')
|
||||
expect(wrapper.vm.form.amount).toBe('400')
|
||||
expect(wrapper.vm.form.amount).toBe('400.00')
|
||||
})
|
||||
|
||||
it('sets tab index back to 0', () => {
|
||||
expect(wrapper.vm.tabIndex).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('update list all contributions', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.setData({ tabIndex: 2 })
|
||||
await wrapper
|
||||
.findAllComponents({ name: 'ContributionList' })
|
||||
.at(1)
|
||||
.vm.$emit('update-list-contributions', {
|
||||
currentPage: 2,
|
||||
pageSize: 5,
|
||||
})
|
||||
})
|
||||
|
||||
it('updates page size and current page', () => {
|
||||
expect(wrapper.vm.pageSizeAll).toBe(5)
|
||||
expect(wrapper.vm.currentPageAll).toBe(2)
|
||||
})
|
||||
|
||||
it('updates the all contribution list', () => {
|
||||
expect(refetchAllContributionsSpy).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('update list contributions', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.setData({ tabIndex: 1 })
|
||||
await wrapper
|
||||
.findAllComponents({ name: 'ContributionList' })
|
||||
.at(0)
|
||||
.vm.$emit('update-list-contributions', {
|
||||
currentPage: 2,
|
||||
pageSize: 5,
|
||||
})
|
||||
})
|
||||
|
||||
it('updates page size and current page', () => {
|
||||
expect(wrapper.vm.pageSize).toBe(5)
|
||||
expect(wrapper.vm.currentPage).toBe(2)
|
||||
})
|
||||
|
||||
it('updates the all contribution list', () => {
|
||||
expect(refetchContributionsSpy).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
/>
|
||||
<div class="mb-3"></div>
|
||||
<contribution-form
|
||||
@set-contribution="setContribution"
|
||||
@set-contribution="saveContribution"
|
||||
@update-contribution="updateContribution"
|
||||
v-model="form"
|
||||
:isThisMonth="isThisMonth"
|
||||
@ -70,6 +70,7 @@ export default {
|
||||
itemsAll: [],
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
currentPageAll: 1,
|
||||
pageSizeAll: 25,
|
||||
contributionCount: 0,
|
||||
contributionCountAll: 0,
|
||||
@ -107,6 +108,51 @@ export default {
|
||||
this.toastError(message)
|
||||
},
|
||||
},
|
||||
ListAllContributions: {
|
||||
query() {
|
||||
return listAllContributions
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
variables() {
|
||||
return {
|
||||
currentPage: this.currentPageAll,
|
||||
pageSize: this.pageSizeAll,
|
||||
}
|
||||
},
|
||||
update({ listAllContributions }) {
|
||||
this.contributionCountAll = listAllContributions.contributionCount
|
||||
this.itemsAll = listAllContributions.contributionList
|
||||
},
|
||||
error({ message }) {
|
||||
this.toastError(message)
|
||||
},
|
||||
},
|
||||
ListContributions: {
|
||||
query() {
|
||||
return listContributions
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
variables() {
|
||||
return {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
}
|
||||
},
|
||||
update({ listContributions }) {
|
||||
this.contributionCount = listContributions.contributionCount
|
||||
this.items = listContributions.contributionList
|
||||
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
|
||||
this.tabIndex = 1
|
||||
if (this.$route.hash !== '#my') {
|
||||
this.$router.push({ path: '/community#my' })
|
||||
}
|
||||
this.toastInfo(this.$t('contribution.alert.answerQuestionToast'))
|
||||
}
|
||||
},
|
||||
error({ message }) {
|
||||
this.toastError(message)
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
@ -160,7 +206,12 @@ export default {
|
||||
this.$root.$emit('bv::toggle::collapse', value.id)
|
||||
})
|
||||
},
|
||||
setContribution(data) {
|
||||
refetchData() {
|
||||
this.$apollo.queries.ListAllContributions.refetch()
|
||||
this.$apollo.queries.ListContributions.refetch()
|
||||
this.$apollo.queries.OpenCreations.refetch()
|
||||
},
|
||||
saveContribution(data) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
fetchPolicy: 'no-cache',
|
||||
@ -173,15 +224,7 @@ export default {
|
||||
})
|
||||
.then((result) => {
|
||||
this.toastSuccess(this.$t('contribution.submitted'))
|
||||
this.updateListContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateListAllContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.$apollo.queries.OpenCreations.refetch()
|
||||
this.refetchData()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
@ -201,15 +244,7 @@ export default {
|
||||
})
|
||||
.then((result) => {
|
||||
this.toastSuccess(this.$t('contribution.updated'))
|
||||
this.updateListContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateListAllContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.$apollo.queries.OpenCreations.refetch()
|
||||
this.refetchData()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
@ -226,68 +261,21 @@ export default {
|
||||
})
|
||||
.then((result) => {
|
||||
this.toastSuccess(this.$t('contribution.deleted'))
|
||||
this.updateListContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateListAllContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.$apollo.queries.OpenCreations.refetch()
|
||||
this.refetchData()
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
},
|
||||
updateListAllContributions(pagination) {
|
||||
this.$apollo
|
||||
.query({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listAllContributions,
|
||||
variables: {
|
||||
currentPage: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const {
|
||||
data: { listAllContributions },
|
||||
} = result
|
||||
this.contributionCountAll = listAllContributions.contributionCount
|
||||
this.itemsAll = listAllContributions.contributionList
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
this.currentPageAll = pagination.currentPage
|
||||
this.pageSizeAll = pagination.pageSize
|
||||
this.$apollo.queries.ListAllContributions.refetch()
|
||||
},
|
||||
updateListContributions(pagination) {
|
||||
this.$apollo
|
||||
.query({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: listContributions,
|
||||
variables: {
|
||||
currentPage: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const {
|
||||
data: { listContributions },
|
||||
} = result
|
||||
this.contributionCount = listContributions.contributionCount
|
||||
this.items = listContributions.contributionList
|
||||
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
|
||||
this.tabIndex = 1
|
||||
if (this.$route.hash !== '#my') {
|
||||
this.$router.push({ path: '/community#my' })
|
||||
}
|
||||
this.toastInfo(this.$t('contribution.alert.answerQuestionToast'))
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
this.currentPage = pagination.currentPage
|
||||
this.pageSize = pagination.pageSize
|
||||
this.$apollo.queries.ListContributions.refetch()
|
||||
},
|
||||
updateContributionForm(item) {
|
||||
this.form.id = item.id
|
||||
@ -306,19 +294,10 @@ export default {
|
||||
this.items.find((item) => item.id === id).state = 'PENDING'
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.updateListContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateListAllContributions({
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
this.updateTransactions(0)
|
||||
this.tabIndex = 1
|
||||
this.$router.push({ path: '/community#my' })
|
||||
this.tabIndex = 0
|
||||
this.$router.push({ path: '/community#edit' })
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -22,6 +22,7 @@ import { loadFilters } from '@/filters/amount'
|
||||
import { toasters } from '@/mixins/toaster'
|
||||
export const toastErrorSpy = jest.spyOn(toasters.methods, 'toastError')
|
||||
export const toastSuccessSpy = jest.spyOn(toasters.methods, 'toastSuccess')
|
||||
export const toastInfoSpy = jest.spyOn(toasters.methods, 'toastInfo')
|
||||
|
||||
Object.keys(rules).forEach((rule) => {
|
||||
extend(rule, {
|
||||
|
||||
@ -2,6 +2,25 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@apollo/client@^3.7.4":
|
||||
version "3.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.7.4.tgz#28c3fb7f89365ccaf185bc8b51860041f37629b3"
|
||||
integrity sha512-bgiCKRmLSBImX4JRrw8NjqGo0AQE/mowCdHX1PJp2r5zIXrJx0UeaAYmx1qJY69Oz/KR7SKlLt4xK+bOP1jx7A==
|
||||
dependencies:
|
||||
"@graphql-typed-document-node/core" "^3.1.1"
|
||||
"@wry/context" "^0.7.0"
|
||||
"@wry/equality" "^0.5.0"
|
||||
"@wry/trie" "^0.3.0"
|
||||
graphql-tag "^2.12.6"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
optimism "^0.16.1"
|
||||
prop-types "^15.7.2"
|
||||
response-iterator "^0.2.6"
|
||||
symbol-observable "^4.0.0"
|
||||
ts-invariant "^0.10.3"
|
||||
tslib "^2.3.0"
|
||||
zen-observable-ts "^1.2.5"
|
||||
|
||||
"@babel/code-frame@7.12.11":
|
||||
version "7.12.11"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
|
||||
@ -1904,6 +1923,11 @@
|
||||
minimatch "^3.0.4"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@graphql-typed-document-node/core@^3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052"
|
||||
integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==
|
||||
|
||||
"@hapi/address@2.x.x":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
||||
@ -3159,6 +3183,13 @@
|
||||
"@types/node" ">=6"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@wry/context@^0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.0.tgz#be88e22c0ddf62aeb0ae9f95c3d90932c619a5c8"
|
||||
integrity sha512-LcDAiYWRtwAoSOArfk7cuYvFXytxfVrdX7yxoUmK7pPITLk5jYh2F8knCwS7LjgYL8u1eidPlKKV6Ikqq0ODqQ==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@wry/equality@^0.1.2":
|
||||
version "0.1.11"
|
||||
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790"
|
||||
@ -3166,6 +3197,20 @@
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@wry/equality@^0.5.0":
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.3.tgz#fafebc69561aa2d40340da89fa7dc4b1f6fb7831"
|
||||
integrity sha512-avR+UXdSrsF2v8vIqIgmeTY0UR91UT+IyablCyKe/uk22uOJ8fusKZnH9JH9e1/EtLeNJBtagNmL3eJdnOV53g==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@wry/trie@^0.3.0":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.3.2.tgz#a06f235dc184bd26396ba456711f69f8c35097e6"
|
||||
integrity sha512-yRTyhWSls2OY/pYLfwff867r8ekooZ4UI+/gxot5Wj8EFwSf2rG+n+Mo/6LoLQm1TKA4GRj2+LCpbfS937dClQ==
|
||||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@xtuc/ieee754@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||
@ -7453,6 +7498,13 @@ graceful-fs@^4.2.4:
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
||||
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
||||
|
||||
graphql-tag@^2.12.6:
|
||||
version "2.12.6"
|
||||
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
|
||||
integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
graphql-tag@^2.4.2:
|
||||
version "2.12.5"
|
||||
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.5.tgz#5cff974a67b417747d05c8d9f5f3cb4495d0db8f"
|
||||
@ -7636,6 +7688,13 @@ hmac-drbg@^1.0.1:
|
||||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||
dependencies:
|
||||
react-is "^16.7.0"
|
||||
|
||||
homedir-polyfill@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
|
||||
@ -9915,7 +9974,7 @@ loglevel@^1.6.8:
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
||||
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.2.0:
|
||||
loose-envify@^1.0.0, loose-envify@^1.2.0, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
@ -10260,6 +10319,11 @@ mkdirp@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
mock-apollo-client@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/mock-apollo-client/-/mock-apollo-client-1.2.1.tgz#e3bfdc3ff73b1fea28fa7e91ec82e43ba8cbfa39"
|
||||
integrity sha512-QYQ6Hxo+t7hard1bcHHbsHxlNQYTQsaMNsm2Psh/NbwLMi2R4tGzplJKt97MUWuARHMq3GHB4PTLj/gxej4Caw==
|
||||
|
||||
moment@^2.19.2:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
@ -10767,6 +10831,14 @@ optimism@^0.10.0:
|
||||
dependencies:
|
||||
"@wry/context" "^0.4.0"
|
||||
|
||||
optimism@^0.16.1:
|
||||
version "0.16.2"
|
||||
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.16.2.tgz#519b0c78b3b30954baed0defe5143de7776bf081"
|
||||
integrity sha512-zWNbgWj+3vLEjZNIh/okkY2EUfX+vB9TJopzIZwT1xxaMqC5hRLLraePod4c5n4He08xuXNH+zhKFFCu390wiQ==
|
||||
dependencies:
|
||||
"@wry/context" "^0.7.0"
|
||||
"@wry/trie" "^0.3.0"
|
||||
|
||||
optionator@^0.8.1, optionator@^0.8.2:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
||||
@ -11721,6 +11793,15 @@ prompts@^2.0.1:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.7.2:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
proto-list@~1.2.1:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||
@ -11900,7 +11981,7 @@ raw-body@2.4.0:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
react-is@^16.8.4:
|
||||
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.4:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
@ -12269,6 +12350,11 @@ resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.1
|
||||
is-core-module "^2.2.0"
|
||||
path-parse "^1.0.6"
|
||||
|
||||
response-iterator@^0.2.6:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/response-iterator/-/response-iterator-0.2.6.tgz#249005fb14d2e4eeb478a3f735a28fd8b4c9f3da"
|
||||
integrity sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==
|
||||
|
||||
restore-cursor@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
|
||||
@ -13406,6 +13492,11 @@ symbol-observable@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||
|
||||
symbol-observable@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"
|
||||
integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==
|
||||
|
||||
symbol-tree@^3.2.2, symbol-tree@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
@ -13704,6 +13795,13 @@ tryer@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
|
||||
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
|
||||
|
||||
ts-invariant@^0.10.3:
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.10.3.tgz#3e048ff96e91459ffca01304dbc7f61c1f642f6c"
|
||||
integrity sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
ts-invariant@^0.4.0:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
|
||||
@ -13757,6 +13855,11 @@ tslib@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||
|
||||
tslib@^2.3.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
||||
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
||||
|
||||
tsutils@^3.17.1:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
@ -14919,7 +15022,14 @@ zen-observable-ts@^0.8.21:
|
||||
tslib "^1.9.3"
|
||||
zen-observable "^0.8.0"
|
||||
|
||||
zen-observable@^0.8.0:
|
||||
zen-observable-ts@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58"
|
||||
integrity sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==
|
||||
dependencies:
|
||||
zen-observable "0.8.15"
|
||||
|
||||
zen-observable@0.8.15, zen-observable@^0.8.0:
|
||||
version "0.8.15"
|
||||
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
|
||||
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user