Merge branch 'master' into eslint-dht-import

This commit is contained in:
Ulf Gebhardt 2023-06-30 14:30:07 +02:00 committed by GitHub
commit cde1e0a1f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 2595 additions and 1057 deletions

View File

@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesFormular from './ContributionMessagesFormular'
import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup'
import { adminCreateContributionMessage } from '@/graphql/adminCreateContributionMessage'
const localVue = global.localVue
@ -34,6 +35,7 @@ describe('ContributionMessagesFormular', () => {
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
jest.clearAllMocks()
})
it('has a DIV .contribution-messages-formular', () => {
@ -73,13 +75,65 @@ describe('ContributionMessagesFormular', () => {
)
})
it('emitted "update-state" with data', async () => {
expect(wrapper.emitted('update-state')).toEqual(
it('emitted "update-status" with data', async () => {
expect(wrapper.emitted('update-status')).toEqual(
expect.arrayContaining([expect.arrayContaining([42])]),
)
})
})
describe('send DIALOG contribution message with success', () => {
beforeEach(async () => {
await wrapper.setData({
form: {
text: 'text form message',
},
})
await wrapper.find('button[data-test="submit-dialog"]').trigger('click')
})
it('moderatorMesage has `DIALOG`', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: adminCreateContributionMessage,
variables: {
contributionId: 42,
message: 'text form message',
messageType: 'DIALOG',
},
})
})
it('toasts an success message', () => {
expect(toastSuccessSpy).toBeCalledWith('message.request')
})
})
describe('send MODERATOR contribution message with success', () => {
beforeEach(async () => {
await wrapper.setData({
form: {
text: 'text form message',
},
})
await wrapper.find('button[data-test="submit-moderator"]').trigger('click')
})
it('moderatorMesage has `MODERATOR`', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: adminCreateContributionMessage,
variables: {
contributionId: 42,
message: 'text form message',
messageType: 'MODERATOR',
},
})
})
it('toasts an success message', () => {
expect(toastSuccessSpy).toBeCalledWith('message.request')
})
})
describe('send contribution message with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
@ -91,21 +145,5 @@ describe('ContributionMessagesFormular', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
})
})
describe('send contribution message with success', () => {
beforeEach(async () => {
wrapper.setData({
form: {
text: 'text form message',
},
})
wrapper = Wrapper()
await wrapper.find('form').trigger('submit')
})
it('toasts an success message', () => {
expect(toastSuccessSpy).toBeCalledWith('message.request')
})
})
})
})

View File

@ -1,7 +1,7 @@
<template>
<div class="contribution-messages-formular">
<div class="mt-5">
<b-form @submit.prevent="onSubmit" @reset.prevent="onReset">
<b-form @reset.prevent="onReset" @submit="onSubmit(messageType.DIALOG)">
<b-form-textarea
id="textarea"
v-model="form.text"
@ -12,8 +12,27 @@
<b-col>
<b-button type="reset" variant="danger">{{ $t('form.cancel') }}</b-button>
</b-col>
<b-col class="text-center">
<b-button
type="button"
variant="warning"
class="text-black"
:disabled="disabled"
@click.prevent="onSubmit(messageType.MODERATOR)"
data-test="submit-moderator"
>
{{ $t('moderator.notice') }}
</b-button>
</b-col>
<b-col class="text-right">
<b-button type="submit" variant="primary" :disabled="disabled">
<b-button
type="submit"
variant="primary"
:disabled="disabled"
@click.prevent="onSubmit(messageType.DIALOG)"
data-test="submit-dialog"
>
{{ $t('form.submit') }}
</b-button>
</b-col>
@ -39,10 +58,14 @@ export default {
text: '',
},
loading: false,
messageType: {
DIALOG: 'DIALOG',
MODERATOR: 'MODERATOR',
},
}
},
methods: {
onSubmit(event) {
onSubmit(mType) {
this.loading = true
this.$apollo
.mutate({
@ -50,11 +73,12 @@ export default {
variables: {
contributionId: this.contributionId,
message: this.form.text,
messageType: mType,
},
})
.then((result) => {
this.$emit('get-list-contribution-messages', this.contributionId)
this.$emit('update-state', this.contributionId)
this.$emit('update-status', this.contributionId)
this.form.text = ''
this.toastSuccess(this.$t('message.request'))
this.loading = false

View File

@ -1,26 +1,102 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesList from './ContributionMessagesList'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { adminListContributionMessages } from '../../graphql/adminListContributionMessages.js'
import { toastErrorSpy } from '../../../test/testSetup'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue()
localVue.use(VueApollo)
const defaultData = () => {
return {
adminListContributionMessages: {
count: 4,
messages: [
{
id: 43,
message: 'A DIALOG message',
createdAt: new Date().toString(),
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Peter',
userLastName: 'Lustig',
userId: 1,
isModerator: true,
},
{
id: 44,
message: 'Another DIALOG message',
createdAt: new Date().toString(),
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Bibi',
userLastName: 'Bloxberg',
userId: 2,
isModerator: false,
},
{
id: 45,
message: `DATE
---
A HISTORY message
---
AMOUNT`,
createdAt: new Date().toString(),
updatedAt: null,
type: 'HISTORY',
userFirstName: 'Bibi',
userLastName: 'Bloxberg',
userId: 2,
isModerator: false,
},
{
id: 46,
message: 'A MODERATOR message',
createdAt: new Date().toString(),
updatedAt: null,
type: 'MODERATOR',
userFirstName: 'Peter',
userLastName: 'Lustig',
userId: 1,
isModerator: true,
},
],
},
}
}
describe('ContributionMessagesList', () => {
let wrapper
const adminListContributionMessagessMock = jest.fn()
mockClient.setRequestHandler(
adminListContributionMessages,
adminListContributionMessagessMock
.mockRejectedValueOnce({ message: 'Auaa!' })
.mockResolvedValue({ data: defaultData() }),
)
const propsData = {
contributionId: 42,
contributionState: 'PENDING',
contributionUserId: 108,
contributionStatus: 'PENDING',
}
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$n: jest.fn((n) => n),
$i18n: {
locale: 'en',
},
$apollo: {
query: apolloQueryMock,
},
}
const Wrapper = () => {
@ -28,30 +104,34 @@ describe('ContributionMessagesList', () => {
localVue,
mocks,
propsData,
apolloProvider,
})
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('sends query to Apollo when created', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
contributionId: propsData.contributionId,
},
}),
)
describe('server response for admin list contribution messages is error', () => {
it('toast an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Auaa!')
})
})
it('has a DIV .contribution-messages-list', () => {
expect(wrapper.find('div.contribution-messages-list').exists()).toBe(true)
})
describe('server response is succes', () => {
it('has a DIV .contribution-messages-list', () => {
expect(wrapper.find('div.contribution-messages-list').exists()).toBe(true)
})
it('has a Component ContributionMessagesFormular', () => {
expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true)
it('has 4 messages', () => {
expect(wrapper.findAll('div.contribution-messages-list-item')).toHaveLength(4)
})
it('has a Component ContributionMessagesFormular', () => {
expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true)
})
})
})
})

View File

@ -2,15 +2,17 @@
<div class="contribution-messages-list">
<b-container>
<div v-for="message in messages" v-bind:key="message.id">
<contribution-messages-list-item :message="message" />
<contribution-messages-list-item
:message="message"
:contributionUserId="contributionUserId"
/>
</div>
</b-container>
<div v-if="contributionState === 'PENDING' || contributionState === 'IN_PROGRESS'">
<div v-if="contributionStatus === 'PENDING' || contributionStatus === 'IN_PROGRESS'">
<contribution-messages-formular
:contributionId="contributionId"
@get-list-contribution-messages="getListContributionMessages"
@update-state="updateState"
@get-list-contribution-messages="$apollo.queries.Messages.refetch()"
@update-status="updateStatus"
/>
</div>
</div>
@ -18,7 +20,7 @@
<script>
import ContributionMessagesListItem from './slots/ContributionMessagesListItem'
import ContributionMessagesFormular from '../ContributionMessages/ContributionMessagesFormular'
import { listContributionMessages } from '../../graphql/listContributionMessages.js'
import { adminListContributionMessages } from '../../graphql/adminListContributionMessages.js'
export default {
name: 'ContributionMessagesList',
@ -31,39 +33,43 @@ export default {
type: Number,
required: true,
},
contributionState: {
contributionStatus: {
type: String,
required: true,
},
contributionUserId: {
type: Number,
required: true,
},
},
data() {
return {
messages: [],
}
},
methods: {
getListContributionMessages(id) {
this.$apollo
.query({
query: listContributionMessages,
variables: {
contributionId: id,
},
fetchPolicy: 'no-cache',
})
.then((result) => {
this.messages = result.data.listContributionMessages.messages
})
.catch((error) => {
this.toastError(error.message)
})
},
updateState(id) {
this.$emit('update-state', id)
apollo: {
Messages: {
query() {
return adminListContributionMessages
},
variables() {
return {
contributionId: this.contributionId,
}
},
fetchPolicy: 'no-cache',
update({ adminListContributionMessages }) {
this.messages = adminListContributionMessages.messages
},
error({ message }) {
this.toastError(message)
},
},
},
created() {
this.getListContributionMessages(this.contributionId)
methods: {
updateStatus(id) {
this.$emit('update-status', id)
},
},
}
</script>

View File

@ -13,11 +13,20 @@ describe('ContributionMessagesListItem', () => {
$t: jest.fn((t) => t),
$d: dateMock,
$n: numberMock,
$store: {
state: {
moderator: {
firstName: 'Peter',
lastName: 'Lustig',
},
},
},
}
describe('if message author has moderator role', () => {
const propsData = {
contributionId: 42,
contributionUserId: 108,
state: 'PENDING',
message: {
id: 111,
@ -51,27 +60,21 @@ describe('ContributionMessagesListItem', () => {
})
it('has the complete user name', () => {
expect(wrapper.find('div.text-right.is-moderator > span:nth-child(2)').text()).toBe(
'Peter Lustig',
)
expect(wrapper.find('[data-test="moderator-name"]').text()).toBe('Peter Lustig')
})
it('has the message creation date', () => {
expect(wrapper.find('div.text-right.is-moderator > span:nth-child(3)').text()).toMatch(
expect(wrapper.find('[data-test="moderator-date"]').text()).toMatch(
'Mon Aug 29 2022 12:23:27 GMT+0000',
)
})
it('has the moderator label', () => {
expect(wrapper.find('div.text-right.is-moderator > small:nth-child(4)').text()).toBe(
'moderator',
)
expect(wrapper.find('[data-test="moderator-label"]').text()).toBe('moderator.moderator')
})
it('has the message', () => {
expect(wrapper.find('div.text-right.is-moderator > div:nth-child(5)').text()).toBe(
'Lorem ipsum?',
)
expect(wrapper.find('[data-test="moderator-message"]').text()).toBe('Lorem ipsum?')
})
})
})
@ -79,6 +82,7 @@ describe('ContributionMessagesListItem', () => {
describe('if message author does not have moderator role', () => {
const propsData = {
contributionId: 42,
contributionUserId: 108,
state: 'PENDING',
message: {
id: 113,
@ -107,23 +111,21 @@ describe('ContributionMessagesListItem', () => {
})
it('has a DIV .text-left.is-not-moderator', () => {
expect(wrapper.find('div.text-left.is-not-moderator').exists()).toBe(true)
expect(wrapper.find('div.text-left.is-user').exists()).toBe(true)
})
it('has the complete user name', () => {
expect(wrapper.find('div.is-not-moderator.text-left > span:nth-child(2)').text()).toBe(
'Bibi Bloxberg',
)
expect(wrapper.find('[data-test="user-name"]').text()).toBe('Bibi Bloxberg')
})
it('has the message creation date', () => {
expect(wrapper.find('div.is-not-moderator.text-left > span:nth-child(3)').text()).toMatch(
expect(wrapper.find('[data-test="user-date"]').text()).toMatch(
'Mon Aug 29 2022 12:25:34 GMT+0000',
)
})
it('has the message', () => {
expect(wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)').text()).toBe(
expect(wrapper.find('[data-test="user-message"]').text()).toBe(
'Asda sdad ad asdasd, das Ass das Das.',
)
})
@ -132,6 +134,7 @@ describe('ContributionMessagesListItem', () => {
describe('links in contribtion message', () => {
const propsData = {
contributionUserId: 108,
message: {
id: 111,
message: 'Lorem ipsum?',
@ -159,7 +162,7 @@ describe('ContributionMessagesListItem', () => {
beforeEach(() => {
propsData.message.message = 'https://gradido.net/de/'
wrapper = ModeratorItemWrapper()
messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
messageField = wrapper.find('[data-test="moderator-message"]')
})
it('contains the link as text', () => {
@ -176,7 +179,7 @@ describe('ContributionMessagesListItem', () => {
propsData.message.message = `Here you find all you need to know about Gradido: https://gradido.net/de/
and here is the link to the repository: https://github.com/gradido/gradido`
wrapper = ModeratorItemWrapper()
messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
messageField = wrapper.find('[data-test="moderator-message"]')
})
it('contains the whole text', () => {
@ -196,6 +199,7 @@ and here is the link to the repository: https://github.com/gradido/gradido`)
describe('contribution message type HISTORY', () => {
const propsData = {
contributionUserId: 108,
message: {
id: 111,
message: `Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time)
@ -227,7 +231,7 @@ This message also contains a link: https://gradido.net/de/
beforeEach(() => {
jest.clearAllMocks()
wrapper = itemWrapper()
messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
messageField = wrapper
})
it('renders the date', () => {

View File

@ -1,17 +1,37 @@
<template>
<div class="contribution-messages-list-item">
<div v-if="message.isModerator" class="text-right is-moderator">
<div v-if="isModeratorMessage" class="text-right p-2 rounded-sm mb-3" :class="boxClass">
<small class="ml-4" data-test="moderator-label">
{{ $t('moderator.moderator') }}
</small>
<small class="ml-2" data-test="moderator-date">
{{ $d(new Date(message.createdAt), 'short') }}
</small>
<span class="ml-2 mr-2" data-test="moderator-name">
{{ message.userFirstName }} {{ message.userLastName }}
</span>
<b-avatar square variant="warning"></b-avatar>
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<small class="ml-4 text-success">{{ $t('moderator') }}</small>
<parse-message v-bind="message"></parse-message>
<parse-message v-bind="message" data-test="moderator-message"></parse-message>
<small v-if="isModeratorHiddenMessage">
<hr />
{{ $t('moderator.request') }}
</small>
</div>
<div v-else class="text-left is-not-moderator">
<div v-else class="text-left p-2 rounded-sm mb-3" :class="boxClass">
<b-avatar variant="info"></b-avatar>
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<parse-message v-bind="message"></parse-message>
<span class="ml-2 mr-2" data-test="user-name">
{{ message.userFirstName }} {{ message.userLastName }}
</span>
<small class="ml-2" data-test="user-date">
{{ $d(new Date(message.createdAt), 'short') }}
</small>
<small v-if="isHistory">
<hr />
{{ $t('moderator.history') }}
<hr />
</small>
<parse-message v-bind="message" data-test="user-message"></parse-message>
</div>
</div>
</template>
@ -28,22 +48,50 @@ export default {
type: Object,
required: true,
},
contributionUserId: {
type: Number,
required: true,
},
},
computed: {
isModeratorMessage() {
return this.contributionUserId !== this.message.userId
},
isModeratorHiddenMessage() {
return this.message.type === 'MODERATOR'
},
isHistory() {
return this.message.type === 'HISTORY'
},
boxClass() {
if (this.isModeratorHiddenMessage) return 'is-moderator is-moderator-hidden-message'
if (this.isHistory) return 'is-user is-user-history-message'
if (this.isModeratorMessage) return 'is-moderator is-moderator-message'
return 'is-user is-user-message'
},
},
}
</script>
<style>
.is-not-moderator {
clear: both;
width: 75%;
margin-top: 20px;
/* background-color: rgb(261, 204, 221); */
}
.is-moderator {
clear: both;
float: right;
width: 75%;
margin-top: 20px;
margin-bottom: 20px;
/* background-color: rgb(255, 255, 128); */
}
.is-moderator-message {
background-color: rgb(228, 237, 245);
}
.is-moderator-hidden-message {
background-color: rgb(217, 161, 228);
}
.is-user {
clear: both;
width: 75%;
}
.is-user-message {
background-color: rgb(236, 235, 213);
}
.is-user-history-message {
background-color: rgb(235, 226, 57);
}
</style>

View File

@ -28,7 +28,7 @@ const defaultData = () => {
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -39,6 +39,7 @@ const defaultData = () => {
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
moderatorId: null,
},
{
id: 2,
@ -50,7 +51,7 @@ const defaultData = () => {
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -61,6 +62,7 @@ const defaultData = () => {
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
moderatorId: null,
},
],
},

View File

@ -34,8 +34,8 @@
{{ $t('help.transactionlist.confirmed') }}
</div>
<div>
{{ $t('transactionlist.state') }} {{ $t('math.equals') }}
{{ $t('help.transactionlist.state') }}
{{ $t('transactionlist.status') }} {{ $t('math.equals') }}
{{ $t('help.transactionlist.status') }}
</div>
</b-collapse>
</div>
@ -78,8 +78,8 @@ export default {
},
},
{
key: 'state',
label: this.$t('transactionlist.state'),
key: 'status',
label: this.$t('transactionlist.status'),
},
{
key: 'amount',

View File

@ -131,13 +131,13 @@ describe('OpenCreationsTable', () => {
})
})
describe('call updateState', () => {
describe('call updateStatus', () => {
beforeEach(() => {
wrapper.vm.updateState(4)
wrapper.vm.updateStatus(4)
})
it('emits update-state', () => {
expect(wrapper.vm.$root.$emit('update-state', 4)).toBeTruthy()
it('emits update-status', () => {
expect(wrapper.vm.$root.$emit('update-status', 4)).toBeTruthy()
})
})
})

View File

@ -9,8 +9,8 @@
stacked="md"
:tbody-tr-class="rowClass"
>
<template #cell(state)="row">
<b-icon :icon="getStatusIcon(row.item.state)"></b-icon>
<template #cell(status)="row">
<b-icon :icon="getStatusIcon(row.item.status)"></b-icon>
</template>
<template #cell(bookmark)="row">
<div v-if="!myself(row.item)">
@ -39,12 +39,12 @@
<b-button v-else @click="rowToggleDetails(row, 0)">
<b-icon icon="chat-dots"></b-icon>
<b-icon
v-if="row.item.state === 'PENDING' && row.item.messagesCount > 0"
v-if="row.item.status === 'PENDING' && row.item.messagesCount > 0"
icon="exclamation-circle-fill"
variant="warning"
></b-icon>
<b-icon
v-if="row.item.state === 'IN_PROGRESS' && row.item.messagesCount > 0"
v-if="row.item.status === 'IN_PROGRESS' && row.item.messagesCount > 0"
icon="question-diamond"
variant="warning"
class="pl-1"
@ -102,8 +102,9 @@
<div v-else>
<contribution-messages-list
:contributionId="row.item.id"
:contributionState="row.item.state"
@update-state="updateState"
:contributionStatus="row.item.status"
:contributionUserId="row.item.userId"
@update-status="updateStatus"
/>
</div>
</template>
@ -154,14 +155,14 @@ export default {
},
rowClass(item, type) {
if (!item || type !== 'row') return
if (item.state === 'CONFIRMED') return 'table-success'
if (item.state === 'DENIED') return 'table-warning'
if (item.state === 'DELETED') return 'table-danger'
if (item.state === 'IN_PROGRESS') return 'table-primary'
if (item.state === 'PENDING') return 'table-primary'
if (item.status === 'CONFIRMED') return 'table-success'
if (item.status === 'DENIED') return 'table-warning'
if (item.status === 'DELETED') return 'table-danger'
if (item.status === 'IN_PROGRESS') return 'table-primary'
if (item.status === 'PENDING') return 'table-primary'
},
updateState(id) {
this.$emit('update-state', id)
updateStatus(id) {
this.$emit('update-status', id)
},
},
}

View File

@ -1,8 +1,12 @@
import gql from 'graphql-tag'
export const adminCreateContributionMessage = gql`
mutation ($contributionId: Int!, $message: String!) {
adminCreateContributionMessage(contributionId: $contributionId, message: $message) {
mutation ($contributionId: Int!, $message: String!, $messageType: ContributionMessageType) {
adminCreateContributionMessage(
contributionId: $contributionId
message: $message
messageType: $messageType
) {
id
message
createdAt

View File

@ -1,8 +1,8 @@
import gql from 'graphql-tag'
export const listContributionMessages = gql`
export const adminListContributionMessages = gql`
query ($contributionId: Int!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
listContributionMessages(
adminListContributionMessages(
contributionId: $contributionId
pageSize: $pageSize
currentPage: $currentPage

View File

@ -26,7 +26,7 @@ export const adminListContributions = gql`
contributionDate
confirmedAt
confirmedBy
state
status
messagesCount
deniedAt
deniedBy

View File

@ -94,7 +94,7 @@
"transactionlist": {
"confirmed": "Wann wurde es von einem Moderator / Admin bestätigt.",
"periods": "Für welchen Zeitraum wurde vom Mitglied eingereicht.",
"state": "[PENDING = eingereicht, DELETED = gelöscht, IN_PROGRESS = im Dialog mit Moderator, DENIED = abgelehnt, CONFIRMED = bestätigt]",
"status": "[PENDING = eingereicht, DELETED = gelöscht, IN_PROGRESS = im Dialog mit Moderator, DENIED = abgelehnt, CONFIRMED = bestätigt]",
"submitted": "Wann wurde es vom Mitglied eingereicht"
}
},
@ -108,7 +108,12 @@
"message": {
"request": "Die Anfrage wurde gesendet."
},
"moderator": "Moderator",
"moderator": {
"history": "Die Daten wurden geändert. Dies sind die alten Daten.",
"moderator": "Moderator",
"notice": "Moderator Notiz",
"request": "Diese Nachricht ist nur für die Moderatoren sichtbar!"
},
"name": "Name",
"navbar": {
"automaticContributions": "Automatische Beiträge",
@ -184,7 +189,7 @@
"confirmed": "Bestätigt",
"memo": "Nachricht",
"period": "Zeitraum",
"state": "Status",
"status": "Status",
"submitted": "Eingereicht",
"title": "Alle geschöpften Transaktionen für den Nutzer"
},

View File

@ -94,7 +94,7 @@
"transactionlist": {
"confirmed": "When was it confirmed by a moderator / admin.",
"periods": "For what period was it submitted by the member.",
"state": "[PENDING = submitted, DELETED = deleted, IN_PROGRESS = in dialogue with moderator, DENIED = rejected, CONFIRMED = confirmed]",
"status": "[PENDING = submitted, DELETED = deleted, IN_PROGRESS = in dialogue with moderator, DENIED = rejected, CONFIRMED = confirmed]",
"submitted": "When was it submitted by the member"
}
},
@ -108,7 +108,12 @@
"message": {
"request": "Request has been sent."
},
"moderator": "Moderator",
"moderator": {
"history": "The data has been changed. This is the old data.",
"moderator": "Moderator",
"notice": "Moderator note",
"request": "This message is only visible to the moderators!"
},
"name": "Name",
"navbar": {
"automaticContributions": "Automatic Contributions",
@ -184,7 +189,7 @@
"confirmed": "Confirmed",
"memo": "Message",
"period": "Period",
"state": "State",
"status": "State",
"submitted": "Submitted",
"title": "All creation-transactions for the user"
},

View File

@ -51,7 +51,7 @@ const defaultData = () => {
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -73,7 +73,7 @@ const defaultData = () => {
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -451,12 +451,12 @@ describe('CreationConfirm', () => {
describe('update status', () => {
beforeEach(async () => {
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-state', 2)
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-status', 2)
})
it('updates the status', () => {
expect(wrapper.vm.items.find((obj) => obj.id === 2).messagesCount).toBe(1)
expect(wrapper.vm.items.find((obj) => obj.id === 2).state).toBe('IN_PROGRESS')
expect(wrapper.vm.items.find((obj) => obj.id === 2).status).toBe('IN_PROGRESS')
})
})

View File

@ -43,7 +43,7 @@
:items="items"
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateStatus"
@update-status="updateStatus"
@update-contributions="$apollo.queries.ListAllContributions.refetch()"
/>
@ -187,7 +187,7 @@ export default {
},
updateStatus(id) {
this.items.find((obj) => obj.id === id).messagesCount++
this.items.find((obj) => obj.id === id).state = 'IN_PROGRESS'
this.items.find((obj) => obj.id === id).status = 'IN_PROGRESS'
},
formatDateOrDash(value) {
return value ? this.$d(new Date(value), 'short') : '—'
@ -217,7 +217,7 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'moderatorId', label: this.$t('moderator') },
{ key: 'moderatorId', label: this.$t('moderator.moderator') },
{ key: 'editCreation', label: this.$t('chat') },
{ key: 'confirm', label: this.$t('save') },
],
@ -254,7 +254,7 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'confirmedBy', label: this.$t('moderator') },
{ key: 'confirmedBy', label: this.$t('moderator.moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
@ -290,7 +290,7 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'deniedBy', label: this.$t('moderator') },
{ key: 'deniedBy', label: this.$t('moderator.moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
@ -326,12 +326,12 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'deletedBy', label: this.$t('moderator') },
{ key: 'deletedBy', label: this.$t('moderator.moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// all contributions
{ key: 'state', label: this.$t('status') },
{ key: 'status', label: this.$t('status') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
@ -363,7 +363,7 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'confirmedBy', label: this.$t('moderator') },
{ key: 'confirmedBy', label: this.$t('moderator.moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
][this.tabIndex]

View File

@ -43,7 +43,7 @@ const defaultData = () => {
memo: 'Danke für alles',
date: new Date(),
moderatorId: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -65,7 +65,7 @@ const defaultData = () => {
memo: 'Gut Ergattert',
date: new Date(),
moderatorId: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,

View File

@ -40,7 +40,7 @@ module.exports = {
],
// import
'import/export': 'error',
'import/no-deprecated': 'error',
// 'import/no-deprecated': 'error',
'import/no-empty-named-blocks': 'error',
'import/no-extraneous-dependencies': 'error',
'import/no-mutable-exports': 'error',
@ -197,6 +197,9 @@ module.exports = {
{
files: ['*.test.ts'],
plugins: ['jest'],
env: {
jest: true,
},
rules: {
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error',

View File

@ -53,4 +53,5 @@ export enum RIGHTS {
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
ADMIN_OPEN_CREATIONS = 'ADMIN_OPEN_CREATIONS',
ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES = 'ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES',
}

View File

@ -1,5 +1,7 @@
import { ArgsType, Field, Int, InputType } from 'type-graphql'
import { ContributionMessageType } from '@enum/ContributionMessageType'
@InputType()
@ArgsType()
export class ContributionMessageArgs {
@ -8,4 +10,7 @@ export class ContributionMessageArgs {
@Field(() => String)
message: string
@Field(() => ContributionMessageType, { defaultValue: ContributionMessageType.DIALOG })
messageType: ContributionMessageType
}

View File

@ -3,6 +3,7 @@ import { registerEnumType } from 'type-graphql'
export enum ContributionMessageType {
HISTORY = 'HISTORY',
DIALOG = 'DIALOG',
MODERATOR = 'MODERATOR', // messages for moderator communication, can only be seen by moderators
}
registerEnumType(ContributionMessageType, {

View File

@ -15,7 +15,7 @@ export class Contribution {
this.confirmedAt = contribution.confirmedAt
this.confirmedBy = contribution.confirmedBy
this.contributionDate = contribution.contributionDate
this.state = contribution.contributionStatus
this.status = contribution.contributionStatus
this.messagesCount = contribution.messages ? contribution.messages.length : 0
this.deniedAt = contribution.deniedAt
this.deniedBy = contribution.deniedBy
@ -68,7 +68,7 @@ export class Contribution {
messagesCount: number
@Field(() => String)
state: string
status: string
@Field(() => Int, { nullable: true })
moderatorId: number | null

View File

@ -1,25 +1,39 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field, Float, Int } from 'type-graphql'
import { GdtEntryType } from '@enum/GdtEntryType'
@ObjectType()
export class GdtEntry {
constructor(json: any) {
this.id = json.id
this.amount = json.amount
this.date = json.date
this.email = json.email
this.comment = json.comment
this.couponCode = json.coupon_code
this.gdtEntryType = json.gdt_entry_type_id
this.factor = json.factor
this.amount2 = json.amount2
this.factor2 = json.factor2
this.gdt = json.gdt
constructor({
id,
amount,
date,
email,
comment,
// eslint-disable-next-line camelcase
coupon_code,
// eslint-disable-next-line camelcase
gdt_entry_type_id,
factor,
amount2,
factor2,
gdt,
}: any) {
this.id = id
this.amount = amount
this.date = date
this.email = email
this.comment = comment
// eslint-disable-next-line camelcase
this.couponCode = coupon_code
// eslint-disable-next-line camelcase
this.gdtEntryType = gdt_entry_type_id
this.factor = factor
this.amount2 = amount2
this.factor2 = factor2
this.gdt = gdt
}
@Field(() => Int)

View File

@ -1,24 +1,19 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field, Int, Float } from 'type-graphql'
import { GdtEntry } from './GdtEntry'
@ObjectType()
export class GdtEntryList {
constructor(json: any) {
this.state = json.state
this.count = json.count
this.gdtEntries = json.gdtEntries ? json.gdtEntries.map((json: any) => new GdtEntry(json)) : []
this.gdtSum = json.gdtSum
this.timeUsed = json.timeUsed
constructor(status = '', count = 0, gdtEntries = [], gdtSum = 0, timeUsed = 0) {
this.status = status
this.count = count
this.gdtEntries = gdtEntries
this.gdtSum = gdtSum
this.timeUsed = timeUsed
}
@Field(() => String)
state: string
status: string
@Field(() => Int)
count: number

View File

@ -16,7 +16,7 @@ export class UnconfirmedContribution {
this.email = user ? user.emailContact.email : ''
this.moderator = contribution.moderatorId
this.creation = creations
this.state = contribution.contributionStatus
this.status = contribution.contributionStatus
this.messageCount = contribution.messages ? contribution.messages.length : 0
}
@ -51,7 +51,7 @@ export class UnconfirmedContribution {
creation: Decimal[]
@Field(() => String)
state: string
status: string
@Field(() => Int)
messageCount: number

View File

@ -1,12 +1,11 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { getCustomRepository } from '@dbTools/typeorm'
import { IsNull } from '@dbTools/typeorm'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { Decimal } from 'decimal.js-light'
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import { Balance } from '@model/Balance'
import { TransactionLinkRepository } from '@repository/TransactionLink'
import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser } from '@/server/context'
@ -15,6 +14,7 @@ import { calculateDecay } from '@/util/decay'
import { GdtResolver } from './GdtResolver'
import { getLastTransaction } from './util/getLastTransaction'
import { transactionLinkSummary } from './util/transactionLinkSummary'
@Resolver()
export class BalanceResolver {
@ -57,7 +57,7 @@ export class BalanceResolver {
const linkCount = await dbTransactionLink.count({
where: {
userId: user.id,
redeemedAt: null,
redeemedAt: IsNull(),
// validUntil: MoreThan(new Date()),
},
})
@ -77,10 +77,9 @@ export class BalanceResolver {
)
// The final balance is reduced by the link amount withheld
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
const { sumHoldAvailableAmount } = context.sumHoldAvailableAmount
? { sumHoldAvailableAmount: context.sumHoldAvailableAmount }
: await transactionLinkRepository.summary(user.id, now)
: await transactionLinkSummary(user.id, now)
logger.debug(`context.sumHoldAvailableAmount=${context.sumHoldAvailableAmount}`)
logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`)

View File

@ -542,7 +542,7 @@ describe('Contribution Links', () => {
})
it('updated the DB record', async () => {
await expect(DbContributionLink.findOne(linkId)).resolves.toEqual(
await expect(DbContributionLink.findOne({ where: { id: linkId } })).resolves.toEqual(
expect.objectContaining({
id: linkId,
name: 'Dokumenta 2023',

View File

@ -103,7 +103,7 @@ export class ContributionLinkResolver {
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise<boolean> {
const dbContributionLink = await DbContributionLink.findOne(id)
const dbContributionLink = await DbContributionLink.findOne({ where: { id } })
if (!dbContributionLink) {
throw new LogError('Contribution Link not found', id)
}
@ -130,7 +130,7 @@ export class ContributionLinkResolver {
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise<ContributionLink> {
const dbContributionLink = await DbContributionLink.findOne(id)
const dbContributionLink = await DbContributionLink.findOne({ where: { id } })
if (!dbContributionLink) {
throw new LogError('Contribution Link not found', id)
}

View File

@ -20,7 +20,7 @@ import {
createContributionMessage,
login,
} from '@/seeds/graphql/mutations'
import { listContributionMessages } from '@/seeds/graphql/queries'
import { listContributionMessages, adminListContributionMessages } from '@/seeds/graphql/queries'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig'
@ -217,6 +217,33 @@ describe('ContributionMessageResolver', () => {
)
})
})
describe('contribution message type MODERATOR', () => {
it('creates ContributionMessage', async () => {
await expect(
mutate({
mutation: adminCreateContributionMessage,
variables: {
contributionId: result.data.createContribution.id,
message: 'Internal moderator communication',
messageType: 'MODERATOR',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
adminCreateContributionMessage: expect.objectContaining({
id: expect.any(Number),
message: 'Internal moderator communication',
type: 'MODERATOR',
userFirstName: 'Peter',
userLastName: 'Lustig',
}),
},
}),
)
})
})
})
})
@ -385,7 +412,7 @@ describe('ContributionMessageResolver', () => {
resetToken()
})
it('returns a list of contributionmessages', async () => {
it('returns a list of contributionmessages without type MODERATOR', async () => {
await expect(
mutate({
mutation: listContributionMessages,
@ -419,4 +446,96 @@ describe('ContributionMessageResolver', () => {
})
})
})
describe('adminListContributionMessages', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: adminListContributionMessages,
variables: { contributionId: 1 },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated as user', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
it('returns an error', async () => {
await expect(
mutate({
mutation: adminListContributionMessages,
variables: { contributionId: 1 },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated as admin', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
it('returns a list of contributionmessages with type MODERATOR', async () => {
await expect(
mutate({
mutation: adminListContributionMessages,
variables: { contributionId: result.data.createContribution.id },
}),
).resolves.toEqual(
expect.objectContaining({
data: {
adminListContributionMessages: {
count: 3,
messages: expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
message: 'Admin Test',
type: 'DIALOG',
userFirstName: 'Peter',
userLastName: 'Lustig',
}),
expect.objectContaining({
id: expect.any(Number),
message: 'User Test',
type: 'DIALOG',
userFirstName: 'Bibi',
userLastName: 'Bloxberg',
}),
expect.objectContaining({
id: expect.any(Number),
message: 'Internal moderator communication',
type: 'MODERATOR',
userFirstName: 'Peter',
userLastName: 'Lustig',
}),
]),
},
},
}),
)
})
})
})
})

View File

@ -8,8 +8,8 @@ import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type
import { ContributionMessageArgs } from '@arg/ContributionMessageArgs'
import { Paginated } from '@arg/Paginated'
import { ContributionMessageType } from '@enum/ContributionMessageType'
import { ContributionStatus } from '@enum/ContributionStatus'
import { ContributionMessageType } from '@enum/MessageType'
import { Order } from '@enum/Order'
import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage'
@ -22,6 +22,8 @@ import {
import { Context, getUser } from '@/server/context'
import { LogError } from '@/server/LogError'
import { findContributionMessages } from './util/findContributionMessages'
@Resolver()
export class ContributionMessageResolver {
@Authorized([RIGHTS.CREATE_CONTRIBUTION_MESSAGE])
@ -36,7 +38,7 @@ export class ContributionMessageResolver {
await queryRunner.startTransaction('REPEATABLE READ')
const contributionMessage = DbContributionMessage.create()
try {
const contribution = await DbContribution.findOne({ id: contributionId })
const contribution = await DbContribution.findOne({ where: { id: contributionId } })
if (!contribution) {
throw new LogError('Contribution not found', contributionId)
}
@ -82,16 +84,35 @@ export class ContributionMessageResolver {
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
): Promise<ContributionMessageListResult> {
const [contributionMessages, count] = await getConnection()
.createQueryBuilder()
.select('cm')
.from(DbContributionMessage, 'cm')
.leftJoinAndSelect('cm.user', 'u')
.where({ contributionId })
.orderBy('cm.createdAt', order)
.limit(pageSize)
.offset((currentPage - 1) * pageSize)
.getManyAndCount()
const [contributionMessages, count] = await findContributionMessages({
contributionId,
currentPage,
pageSize,
order,
})
return {
count,
messages: contributionMessages.map(
(message) => new ContributionMessage(message, message.user),
),
}
}
@Authorized([RIGHTS.ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES])
@Query(() => ContributionMessageListResult)
async adminListContributionMessages(
@Arg('contributionId', () => Int) contributionId: number,
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
): Promise<ContributionMessageListResult> {
const [contributionMessages, count] = await findContributionMessages({
contributionId,
currentPage,
pageSize,
order,
showModeratorType: true,
})
return {
count,
@ -104,7 +125,7 @@ export class ContributionMessageResolver {
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE])
@Mutation(() => ContributionMessage)
async adminCreateContributionMessage(
@Args() { contributionId, message }: ContributionMessageArgs,
@Args() { contributionId, message, messageType }: ContributionMessageArgs,
@Ctx() context: Context,
): Promise<ContributionMessage> {
const moderator = getUser(context)
@ -124,7 +145,7 @@ export class ContributionMessageResolver {
if (contribution.userId === moderator.id) {
throw new LogError('Admin can not answer on his own contribution', contributionId)
}
if (!contribution.user.emailContact) {
if (!contribution.user.emailContact && contribution.user.emailId) {
contribution.user.emailContact = await DbUserContact.findOneOrFail({
where: { id: contribution.user.emailId },
})
@ -133,7 +154,7 @@ export class ContributionMessageResolver {
contributionMessage.createdAt = new Date()
contributionMessage.message = message
contributionMessage.userId = moderator.id
contributionMessage.type = ContributionMessageType.DIALOG
contributionMessage.type = messageType
contributionMessage.isModerator = true
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Connection } from '@dbTools/typeorm'
import { Connection, Equal } from '@dbTools/typeorm'
import { Contribution } from '@entity/Contribution'
import { Event as DbEvent } from '@entity/Event'
import { Transaction as DbTransaction } from '@entity/Transaction'
@ -457,7 +457,7 @@ describe('ContributionResolver', () => {
describe('contribution has wrong status', () => {
beforeAll(async () => {
const contribution = await Contribution.findOneOrFail({
id: pendingContribution.data.createContribution.id,
where: { id: pendingContribution.data.createContribution.id },
})
contribution.contributionStatus = ContributionStatus.DELETED
await contribution.save()
@ -469,7 +469,7 @@ describe('ContributionResolver', () => {
afterAll(async () => {
const contribution = await Contribution.findOneOrFail({
id: pendingContribution.data.createContribution.id,
where: { id: pendingContribution.data.createContribution.id },
})
contribution.contributionStatus = ContributionStatus.PENDING
await contribution.save()
@ -1092,29 +1092,29 @@ describe('ContributionResolver', () => {
contributionCount: 4,
contributionList: expect.arrayContaining([
expect.not.objectContaining({
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.objectContaining({
id: pendingContribution.data.createContribution.id,
state: 'PENDING',
status: 'PENDING',
memo: 'Test PENDING contribution update',
amount: '10',
}),
expect.objectContaining({
id: contributionToDeny.data.createContribution.id,
state: 'DENIED',
status: 'DENIED',
memo: 'Test contribution to deny',
amount: '100',
}),
expect.objectContaining({
id: contributionToDelete.data.createContribution.id,
state: 'DELETED',
status: 'DELETED',
memo: 'Test contribution to delete',
amount: '100',
}),
expect.objectContaining({
id: inProgressContribution.data.createContribution.id,
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
memo: 'Test IN_PROGRESS contribution',
amount: '100',
}),
@ -1223,47 +1223,47 @@ describe('ContributionResolver', () => {
contributionCount: 7,
contributionList: expect.arrayContaining([
expect.not.objectContaining({
state: 'DELETED',
status: 'DELETED',
}),
expect.objectContaining({
amount: '100',
state: 'CONFIRMED',
status: 'CONFIRMED',
id: contributionToConfirm.data.createContribution.id,
memo: 'Test contribution to confirm',
}),
expect.objectContaining({
id: pendingContribution.data.createContribution.id,
state: 'PENDING',
status: 'PENDING',
memo: 'Test PENDING contribution update',
amount: '10',
}),
expect.objectContaining({
id: contributionToDeny.data.createContribution.id,
state: 'DENIED',
status: 'DENIED',
memo: 'Test contribution to deny',
amount: '100',
}),
expect.objectContaining({
id: inProgressContribution.data.createContribution.id,
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
memo: 'Test IN_PROGRESS contribution',
amount: '100',
}),
expect.objectContaining({
id: bibiCreatedContribution.id,
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Herzlich Willkommen bei Gradido!',
amount: '1000',
}),
expect.objectContaining({
id: expect.any(Number),
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Whatever contribution',
amount: '166',
}),
expect.objectContaining({
id: expect.any(Number),
state: 'DENIED',
status: 'DENIED',
memo: 'Whatever contribution',
amount: '166',
}),
@ -1288,47 +1288,47 @@ describe('ContributionResolver', () => {
contributionCount: 7,
contributionList: expect.arrayContaining([
expect.not.objectContaining({
state: 'DELETED',
status: 'DELETED',
}),
expect.objectContaining({
amount: '100',
state: 'CONFIRMED',
status: 'CONFIRMED',
id: contributionToConfirm.data.createContribution.id,
memo: 'Test contribution to confirm',
}),
expect.objectContaining({
id: pendingContribution.data.createContribution.id,
state: 'PENDING',
status: 'PENDING',
memo: 'Test PENDING contribution update',
amount: '10',
}),
expect.objectContaining({
id: contributionToDeny.data.createContribution.id,
state: 'DENIED',
status: 'DENIED',
memo: 'Test contribution to deny',
amount: '100',
}),
expect.objectContaining({
id: inProgressContribution.data.createContribution.id,
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
memo: 'Test IN_PROGRESS contribution',
amount: '100',
}),
expect.objectContaining({
id: bibiCreatedContribution.id,
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Herzlich Willkommen bei Gradido!',
amount: '1000',
}),
expect.objectContaining({
id: expect.any(Number),
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Whatever contribution',
amount: '166',
}),
expect.objectContaining({
id: expect.any(Number),
state: 'DENIED',
status: 'DENIED',
memo: 'Whatever contribution',
amount: '166',
}),
@ -1353,47 +1353,47 @@ describe('ContributionResolver', () => {
contributionCount: 7,
contributionList: expect.arrayContaining([
expect.not.objectContaining({
state: 'DELETED',
status: 'DELETED',
}),
expect.objectContaining({
amount: '100',
state: 'CONFIRMED',
status: 'CONFIRMED',
id: contributionToConfirm.data.createContribution.id,
memo: 'Test contribution to confirm',
}),
expect.objectContaining({
id: pendingContribution.data.createContribution.id,
state: 'PENDING',
status: 'PENDING',
memo: 'Test PENDING contribution update',
amount: '10',
}),
expect.objectContaining({
id: contributionToDeny.data.createContribution.id,
state: 'DENIED',
status: 'DENIED',
memo: 'Test contribution to deny',
amount: '100',
}),
expect.objectContaining({
id: inProgressContribution.data.createContribution.id,
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
memo: 'Test IN_PROGRESS contribution',
amount: '100',
}),
expect.objectContaining({
id: bibiCreatedContribution.id,
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Herzlich Willkommen bei Gradido!',
amount: '1000',
}),
expect.objectContaining({
id: expect.any(Number),
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Whatever contribution',
amount: '166',
}),
expect.objectContaining({
id: expect.any(Number),
state: 'DENIED',
status: 'DENIED',
memo: 'Whatever contribution',
amount: '166',
}),
@ -1419,33 +1419,33 @@ describe('ContributionResolver', () => {
contributionList: expect.arrayContaining([
expect.objectContaining({
amount: '100',
state: 'CONFIRMED',
status: 'CONFIRMED',
id: contributionToConfirm.data.createContribution.id,
memo: 'Test contribution to confirm',
}),
expect.objectContaining({
id: bibiCreatedContribution.id,
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Herzlich Willkommen bei Gradido!',
amount: '1000',
}),
expect.objectContaining({
id: expect.any(Number),
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Whatever contribution',
amount: '166',
}),
expect.not.objectContaining({
state: 'PENDING',
status: 'PENDING',
}),
expect.not.objectContaining({
state: 'DENIED',
status: 'DENIED',
}),
expect.not.objectContaining({
state: 'DELETED',
status: 'DELETED',
}),
expect.not.objectContaining({
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
}),
]),
})
@ -1468,20 +1468,20 @@ describe('ContributionResolver', () => {
contributionCount: 1,
contributionList: expect.arrayContaining([
expect.not.objectContaining({
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.not.objectContaining({
state: 'DENIED',
status: 'DENIED',
}),
expect.not.objectContaining({
state: 'DELETED',
status: 'DELETED',
}),
expect.not.objectContaining({
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
}),
expect.objectContaining({
id: pendingContribution.data.createContribution.id,
state: 'PENDING',
status: 'PENDING',
memo: 'Test PENDING contribution update',
amount: '10',
}),
@ -1506,20 +1506,20 @@ describe('ContributionResolver', () => {
contributionCount: 1,
contributionList: expect.arrayContaining([
expect.not.objectContaining({
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.not.objectContaining({
state: 'PENDING',
status: 'PENDING',
}),
expect.not.objectContaining({
state: 'DENIED',
status: 'DENIED',
}),
expect.not.objectContaining({
state: 'DELETED',
status: 'DELETED',
}),
expect.objectContaining({
id: inProgressContribution.data.createContribution.id,
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
memo: 'Test IN_PROGRESS contribution',
amount: '100',
}),
@ -1545,27 +1545,27 @@ describe('ContributionResolver', () => {
contributionList: expect.arrayContaining([
expect.objectContaining({
id: contributionToDeny.data.createContribution.id,
state: 'DENIED',
status: 'DENIED',
memo: 'Test contribution to deny',
amount: '100',
}),
expect.objectContaining({
id: expect.any(Number),
state: 'DENIED',
status: 'DENIED',
memo: 'Whatever contribution',
amount: '166',
}),
expect.not.objectContaining({
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.not.objectContaining({
state: 'DELETED',
status: 'DELETED',
}),
expect.not.objectContaining({
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
}),
expect.not.objectContaining({
state: 'PENDING',
status: 'PENDING',
}),
]),
})
@ -1608,36 +1608,36 @@ describe('ContributionResolver', () => {
contributionList: expect.arrayContaining([
expect.objectContaining({
amount: '100',
state: 'CONFIRMED',
status: 'CONFIRMED',
id: contributionToConfirm.data.createContribution.id,
memo: 'Test contribution to confirm',
}),
expect.objectContaining({
id: pendingContribution.data.createContribution.id,
state: 'PENDING',
status: 'PENDING',
memo: 'Test PENDING contribution update',
amount: '10',
}),
expect.objectContaining({
id: bibiCreatedContribution.id,
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Herzlich Willkommen bei Gradido!',
amount: '1000',
}),
expect.objectContaining({
id: expect.any(Number),
state: 'CONFIRMED',
status: 'CONFIRMED',
memo: 'Whatever contribution',
amount: '166',
}),
expect.not.objectContaining({
state: 'DENIED',
status: 'DENIED',
}),
expect.not.objectContaining({
state: 'DELETED',
status: 'DELETED',
}),
expect.not.objectContaining({
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
}),
]),
})
@ -1828,7 +1828,7 @@ describe('ContributionResolver', () => {
creation = await Contribution.findOneOrFail({
where: {
memo: 'Herzlich Willkommen bei Gradido!',
amount: 400,
amount: Equal(new Decimal('400')),
},
})
})
@ -2691,7 +2691,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
messagesCount: 0,
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(50),
@ -2700,7 +2700,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
messagesCount: 0,
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(450),
@ -2709,7 +2709,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
messagesCount: 0,
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(400),
@ -2718,7 +2718,7 @@ describe('ContributionResolver', () => {
lastName: 'Lustig',
memo: 'Herzlich Willkommen bei Gradido!',
messagesCount: 0,
state: 'PENDING',
status: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
@ -2727,7 +2727,7 @@ describe('ContributionResolver', () => {
lastName: 'der Baumeister',
memo: 'Confirmed Contribution',
messagesCount: 0,
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
@ -2736,7 +2736,7 @@ describe('ContributionResolver', () => {
lastName: 'Lustig',
memo: 'Test env contribution',
messagesCount: 0,
state: 'PENDING',
status: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(200),
@ -2745,7 +2745,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Aktives Grundeinkommen',
messagesCount: 0,
state: 'PENDING',
status: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(200),
@ -2754,7 +2754,7 @@ describe('ContributionResolver', () => {
lastName: 'Lustig',
memo: 'Das war leider zu Viel!',
messagesCount: 0,
state: 'DELETED',
status: 'DELETED',
}),
expect.objectContaining({
amount: expect.decimalEqual(166),
@ -2763,7 +2763,7 @@ describe('ContributionResolver', () => {
lastName: 'Hotzenplotz',
memo: 'Whatever contribution',
messagesCount: 0,
state: 'DENIED',
status: 'DENIED',
}),
expect.objectContaining({
amount: expect.decimalEqual(166),
@ -2772,7 +2772,7 @@ describe('ContributionResolver', () => {
lastName: 'Hotzenplotz',
memo: 'Whatever contribution',
messagesCount: 0,
state: 'DELETED',
status: 'DELETED',
}),
expect.objectContaining({
amount: expect.decimalEqual(166),
@ -2781,7 +2781,7 @@ describe('ContributionResolver', () => {
lastName: 'Hotzenplotz',
memo: 'Whatever contribution',
messagesCount: 0,
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
@ -2790,7 +2790,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Test contribution to delete',
messagesCount: 0,
state: 'DELETED',
status: 'DELETED',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
@ -2799,7 +2799,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Test contribution to deny',
messagesCount: 0,
state: 'DENIED',
status: 'DENIED',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
@ -2808,7 +2808,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Test contribution to confirm',
messagesCount: 0,
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
@ -2817,7 +2817,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Test IN_PROGRESS contribution',
messagesCount: 1,
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
}),
expect.objectContaining({
amount: expect.decimalEqual(10),
@ -2826,7 +2826,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Test PENDING contribution update',
messagesCount: 1,
state: 'PENDING',
status: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(1000),
@ -2835,7 +2835,7 @@ describe('ContributionResolver', () => {
lastName: 'Bloxberg',
memo: 'Herzlich Willkommen bei Gradido!',
messagesCount: 0,
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
]),
})
@ -2864,7 +2864,7 @@ describe('ContributionResolver', () => {
lastName: 'Lustig',
memo: 'Herzlich Willkommen bei Gradido!',
messagesCount: 0,
state: 'PENDING',
status: 'PENDING',
}),
expect.objectContaining({
amount: '100',
@ -2873,19 +2873,19 @@ describe('ContributionResolver', () => {
lastName: 'Lustig',
memo: 'Test env contribution',
messagesCount: 0,
state: 'PENDING',
status: 'PENDING',
}),
expect.not.objectContaining({
state: 'DENIED',
status: 'DENIED',
}),
expect.not.objectContaining({
state: 'DELETED',
status: 'DELETED',
}),
expect.not.objectContaining({
state: 'CONFIRMED',
status: 'CONFIRMED',
}),
expect.not.objectContaining({
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
}),
]),
})

View File

@ -11,9 +11,9 @@ import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs'
import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
import { ContributionArgs } from '@arg/ContributionArgs'
import { Paginated } from '@arg/Paginated'
import { ContributionMessageType } from '@enum/ContributionMessageType'
import { ContributionStatus } from '@enum/ContributionStatus'
import { ContributionType } from '@enum/ContributionType'
import { ContributionMessageType } from '@enum/MessageType'
import { Order } from '@enum/Order'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
@ -101,7 +101,7 @@ export class ContributionResolver {
@Ctx() context: Context,
): Promise<boolean> {
const user = getUser(context)
const contribution = await DbContribution.findOne(id)
const contribution = await DbContribution.findOne({ where: { id } })
if (!contribution) {
throw new LogError('Contribution not found', id)
}
@ -395,7 +395,7 @@ export class ContributionResolver {
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise<boolean> {
const contribution = await DbContribution.findOne(id)
const contribution = await DbContribution.findOne({ where: { id } })
if (!contribution) {
throw new LogError('Contribution not found', id)
}
@ -409,10 +409,10 @@ export class ContributionResolver {
) {
throw new LogError('Own contribution can not be deleted as admin')
}
const user = await DbUser.findOneOrFail(
{ id: contribution.userId },
{ relations: ['emailContact'] },
)
const user = await DbUser.findOneOrFail({
where: { id: contribution.userId },
relations: ['emailContact'],
})
contribution.contributionStatus = ContributionStatus.DELETED
contribution.deletedBy = moderator.id
await contribution.save()
@ -447,7 +447,7 @@ export class ContributionResolver {
const releaseLock = await TRANSACTIONS_LOCK.acquire()
try {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const contribution = await DbContribution.findOne(id)
const contribution = await DbContribution.findOne({ where: { id } })
if (!contribution) {
throw new LogError('Contribution not found', id)
}
@ -461,10 +461,11 @@ export class ContributionResolver {
if (moderatorUser.id === contribution.userId) {
throw new LogError('Moderator can not confirm own contribution')
}
const user = await DbUser.findOneOrFail(
{ id: contribution.userId },
{ withDeleted: true, relations: ['emailContact'] },
)
const user = await DbUser.findOneOrFail({
where: { id: contribution.userId },
withDeleted: true,
relations: ['emailContact'],
})
if (user.deletedAt) {
throw new LogError('Can not confirm contribution since the user was deleted')
}
@ -565,9 +566,11 @@ export class ContributionResolver {
@Ctx() context: Context,
): Promise<boolean> {
const contributionToUpdate = await DbContribution.findOne({
id,
confirmedAt: IsNull(),
deniedBy: IsNull(),
where: {
id,
confirmedAt: IsNull(),
deniedBy: IsNull(),
},
})
if (!contributionToUpdate) {
throw new LogError('Contribution not found', id)
@ -582,10 +585,10 @@ export class ContributionResolver {
)
}
const moderator = getUser(context)
const user = await DbUser.findOne(
{ id: contributionToUpdate.userId },
{ relations: ['emailContact'] },
)
const user = await DbUser.findOne({
where: { id: contributionToUpdate.userId },
relations: ['emailContact'],
})
if (!user) {
throw new LogError('Could not find User of the Contribution', contributionToUpdate.userId)
}

View File

@ -6,6 +6,7 @@ import { Resolver, Query, Args, Ctx, Authorized, Arg, Int, Float } from 'type-gr
import { Paginated } from '@arg/Paginated'
import { Order } from '@enum/Order'
import { GdtEntry } from '@model/GdtEntry'
import { GdtEntryList } from '@model/GdtEntryList'
import { apiGet, apiPost } from '@/apis/HttpRequest'
@ -31,9 +32,17 @@ export class GdtResolver {
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
)
if (!resultGDT.success) {
throw new LogError(resultGDT.data)
return new GdtEntryList()
}
return new GdtEntryList(resultGDT.data)
const { state, count, gdtEntries, gdtSum, timeUsed } = resultGDT.data
return new GdtEntryList(
state,
count,
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
gdtEntries ? gdtEntries.map((data: any) => new GdtEntry(data)) : [],
gdtSum,
timeUsed,
)
} catch (err) {
throw new LogError('GDT Server is not reachable')
}

View File

@ -72,10 +72,10 @@ describe('KlicktippResolver', () => {
})
it('stores the NEWSLETTER_SUBSCRIBE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.NEWSLETTER_SUBSCRIBE,
@ -121,10 +121,10 @@ describe('KlicktippResolver', () => {
})
it('stores the NEWSLETTER_UNSUBSCRIBE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.NEWSLETTER_UNSUBSCRIBE,

View File

@ -456,10 +456,10 @@ describe('TransactionLinkResolver', () => {
})
it('stores the CONTRIBUTION_LINK_REDEEM event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.CONTRIBUTION_LINK_REDEEM,
@ -611,10 +611,10 @@ describe('TransactionLinkResolver', () => {
})
it('stores the TRANSACTION_LINK_CREATE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.TRANSACTION_LINK_CREATE,
@ -664,10 +664,10 @@ describe('TransactionLinkResolver', () => {
})
it('stores the TRANSACTION_LINK_DELETE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.TRANSACTION_LINK_DELETE,
@ -719,14 +719,14 @@ describe('TransactionLinkResolver', () => {
})
it('stores the TRANSACTION_LINK_REDEEM event in the database', async () => {
const creator = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const redeemer = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
const creator = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
const redeemer = await UserContact.findOneOrFail({
where: { email: 'peter@lustig.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.TRANSACTION_LINK_REDEEM,

View File

@ -33,6 +33,7 @@ import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { calculateDecay } from '@/util/decay'
import { TRANSACTION_LINK_LOCK } from '@/util/TRANSACTION_LINK_LOCK'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate'
@ -80,6 +81,7 @@ export class TransactionLinkResolver {
// validate amount
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
if (!sendBalance) {
throw new LogError('User has not enough GDD', user.id)
}
@ -108,7 +110,7 @@ export class TransactionLinkResolver {
): Promise<boolean> {
const user = getUser(context)
const transactionLink = await DbTransactionLink.findOne({ id })
const transactionLink = await DbTransactionLink.findOne({ where: { id } })
if (!transactionLink) {
throw new LogError('Transaction link not found', id)
}
@ -138,17 +140,22 @@ export class TransactionLinkResolver {
@Query(() => QueryLinkResult)
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
if (code.match(/^CL-/)) {
const contributionLink = await DbContributionLink.findOneOrFail(
{ code: code.replace('CL-', '') },
{ withDeleted: true },
)
const contributionLink = await DbContributionLink.findOneOrFail({
where: { code: code.replace('CL-', '') },
withDeleted: true,
})
return new ContributionLink(contributionLink)
} else {
const transactionLink = await DbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
const user = await DbUser.findOneOrFail({ id: transactionLink.userId })
const transactionLink = await DbTransactionLink.findOneOrFail({
where: { code },
withDeleted: true,
})
const user = await DbUser.findOneOrFail({ where: { id: transactionLink.userId } })
let redeemedBy: User | null = null
if (transactionLink?.redeemedBy) {
redeemedBy = new User(await DbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
redeemedBy = new User(
await DbUser.findOneOrFail({ where: { id: transactionLink.redeemedBy } }),
)
}
return new TransactionLink(transactionLink, new User(user), redeemedBy)
}
@ -191,7 +198,7 @@ export class TransactionLinkResolver {
throw new LogError('Contribution link is no longer valid', contributionLink.validTo)
}
}
let alreadyRedeemed: DbContribution | undefined
let alreadyRedeemed: DbContribution | null
switch (contributionLink.cycle) {
case ContributionCycleType.ONCE: {
alreadyRedeemed = await queryRunner.manager
@ -302,49 +309,51 @@ export class TransactionLinkResolver {
return true
} else {
const now = new Date()
const transactionLink = await DbTransactionLink.findOne({ code })
if (!transactionLink) {
throw new LogError('Transaction link not found', code)
const releaseLinkLock = await TRANSACTION_LINK_LOCK.acquire()
try {
const transactionLink = await DbTransactionLink.findOne({ where: { code } })
if (!transactionLink) {
throw new LogError('Transaction link not found', code)
}
const linkedUser = await DbUser.findOne({
where: {
id: transactionLink.userId,
},
relations: ['emailContact'],
})
if (!linkedUser) {
throw new LogError('Linked user not found for given link', transactionLink.userId)
}
if (user.id === linkedUser.id) {
throw new LogError('Cannot redeem own transaction link', user.id)
}
if (transactionLink.validUntil.getTime() < now.getTime()) {
throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil)
}
if (transactionLink.redeemedBy) {
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
}
await executeTransaction(
transactionLink.amount,
transactionLink.memo,
linkedUser,
user,
transactionLink,
)
await EVENT_TRANSACTION_LINK_REDEEM(
user,
{ id: transactionLink.userId } as DbUser,
transactionLink,
transactionLink.amount,
)
} finally {
releaseLinkLock()
}
const linkedUser = await DbUser.findOne(
{ id: transactionLink.userId },
{ relations: ['emailContact'] },
)
if (!linkedUser) {
throw new LogError('Linked user not found for given link', transactionLink.userId)
}
if (user.id === linkedUser.id) {
throw new LogError('Cannot redeem own transaction link', user.id)
}
// TODO: The now check should be done within the semaphore lock,
// since the program might wait a while till it is ready to proceed
// writing the transaction.
if (transactionLink.validUntil.getTime() < now.getTime()) {
throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil)
}
if (transactionLink.redeemedBy) {
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
}
await executeTransaction(
transactionLink.amount,
transactionLink.memo,
linkedUser,
user,
transactionLink,
)
await EVENT_TRANSACTION_LINK_REDEEM(
user,
{ id: transactionLink.userId } as DbUser,
transactionLink,
transactionLink.amount,
)
return true
}
}
@ -378,7 +387,7 @@ export class TransactionLinkResolver {
@Arg('userId', () => Int)
userId: number,
): Promise<TransactionLinkResult> {
const user = await DbUser.findOne({ id: userId })
const user = await DbUser.findOne({ where: { id: userId } })
if (!user) {
throw new LogError('Could not find requested User', userId)
}

View File

@ -346,8 +346,10 @@ describe('send coins', () => {
it('stores the TRANSACTION_SEND event in the database', async () => {
// Find the exact transaction (sent one is the one with user[1] as user)
const transaction = await Transaction.find({
userId: user[1].id,
memo: 'unrepeatable memo',
where: {
userId: user[1].id,
memo: 'unrepeatable memo',
},
})
await expect(DbEvent.find()).resolves.toContainEqual(
@ -364,8 +366,10 @@ describe('send coins', () => {
it('stores the TRANSACTION_RECEIVE event in the database', async () => {
// Find the exact transaction (received one is the one with user[0] as user)
const transaction = await Transaction.find({
userId: user[0].id,
memo: 'unrepeatable memo',
where: {
userId: user[0].id,
memo: 'unrepeatable memo',
},
})
await expect(DbEvent.find()).resolves.toContainEqual(

View File

@ -2,7 +2,7 @@
/* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getCustomRepository, getConnection, In } from '@dbTools/typeorm'
import { getConnection, In } from '@dbTools/typeorm'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { User as dbUser } from '@entity/User'
@ -16,7 +16,6 @@ import { TransactionTypeId } from '@enum/TransactionTypeId'
import { Transaction } from '@model/Transaction'
import { TransactionList } from '@model/TransactionList'
import { User } from '@model/User'
import { TransactionLinkRepository } from '@repository/TransactionLink'
import { RIGHTS } from '@/auth/RIGHTS'
import {
@ -38,6 +37,7 @@ import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { findUserByIdentifier } from './util/findUserByIdentifier'
import { getLastTransaction } from './util/getLastTransaction'
import { getTransactionList } from './util/getTransactionList'
import { transactionLinkSummary } from './util/transactionLinkSummary'
export const executeTransaction = async (
amount: Decimal,
@ -245,9 +245,8 @@ export class TransactionResolver {
const self = new User(user)
const transactions: Transaction[] = []
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
await transactionLinkRepository.summary(user.id, now)
await transactionLinkSummary(user.id, now)
context.linkCount = transactionLinkcount
logger.debug(`transactionLinkcount=${transactionLinkcount}`)
context.sumHoldAvailableAmount = sumHoldAvailableAmount

View File

@ -195,10 +195,12 @@ describe('UserResolver', () => {
})
it('stores the USER_REGISTER event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: {
email: 'peter@lustig.de',
},
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_REGISTER,
@ -271,10 +273,10 @@ describe('UserResolver', () => {
})
it('stores the EMAIL_ACCOUNT_MULTIREGISTRATION event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'peter@lustig.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.EMAIL_ACCOUNT_MULTIREGISTRATION,
@ -292,7 +294,7 @@ describe('UserResolver', () => {
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
})
await expect(
UserContact.findOne({ email: 'bibi@bloxberg.de' }, { relations: ['user'] }),
UserContact.findOne({ where: { email: 'bibi@bloxberg.de' }, relations: ['user'] }),
).resolves.toEqual(
expect.objectContaining({
email: 'bibi@bloxberg.de',
@ -334,7 +336,7 @@ describe('UserResolver', () => {
})
// make Peter Lustig Admin
const peter = await User.findOneOrFail({ id: user[0].id })
const peter = await User.findOneOrFail({ where: { id: user[0].id } })
peter.isAdmin = new Date()
await peter.save()
@ -365,7 +367,7 @@ describe('UserResolver', () => {
it('sets the contribution link id', async () => {
await expect(
UserContact.findOne({ email: 'ein@besucher.de' }, { relations: ['user'] }),
UserContact.findOne({ where: { email: 'ein@besucher.de' }, relations: ['user'] }),
).resolves.toEqual(
expect.objectContaining({
user: expect.objectContaining({
@ -445,7 +447,7 @@ describe('UserResolver', () => {
memo: `testing transaction link`,
})
transactionLink = await TransactionLink.findOneOrFail()
transactionLink = await TransactionLink.findOneOrFail({ where: { userId: bob.id } })
resetToken()
@ -462,7 +464,7 @@ describe('UserResolver', () => {
it('sets the referrer id to bob baumeister id', async () => {
await expect(
UserContact.findOne({ email: 'which@ever.de' }, { relations: ['user'] }),
UserContact.findOne({ where: { email: 'which@ever.de' }, relations: ['user'] }),
).resolves.toEqual(
expect.objectContaining({
user: expect.objectContaining({ referrerId: bob.data.login.id }),
@ -529,16 +531,18 @@ describe('UserResolver', () => {
beforeAll(async () => {
await mutate({ mutation: createUser, variables: createUserVariables })
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
const emailContact = await UserContact.findOneOrFail({
where: { email: createUserVariables.email },
})
emailVerificationCode = emailContact.emailVerificationCode.toString()
result = await mutate({
mutation: setPassword,
variables: { code: emailVerificationCode, password: 'Aa12345_' },
})
newUser = await User.findOneOrFail(
{ id: emailContact.userId },
{ relations: ['emailContact'] },
)
newUser = await User.findOneOrFail({
where: { id: emailContact.userId },
relations: ['emailContact'],
})
})
afterAll(async () => {
@ -571,7 +575,9 @@ describe('UserResolver', () => {
describe('no valid password', () => {
beforeAll(async () => {
await mutate({ mutation: createUser, variables: createUserVariables })
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
const emailContact = await UserContact.findOneOrFail({
where: { email: createUserVariables.email },
})
emailVerificationCode = emailContact.emailVerificationCode.toString()
})
@ -697,10 +703,10 @@ describe('UserResolver', () => {
})
it('stores the USER_LOGIN event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_LOGIN,
@ -879,10 +885,10 @@ describe('UserResolver', () => {
})
it('stores the USER_LOGOUT event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_LOGOUT,
@ -1047,10 +1053,10 @@ describe('UserResolver', () => {
})
it('stores the EMAIL_FORGOT_PASSWORD event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.EMAIL_FORGOT_PASSWORD,
@ -1083,7 +1089,7 @@ describe('UserResolver', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
emailContact = await UserContact.findOneOrFail({ email: bibiBloxberg.email })
emailContact = await UserContact.findOneOrFail({ where: { email: bibiBloxberg.email } })
})
afterAll(async () => {
@ -1100,7 +1106,9 @@ describe('UserResolver', () => {
errors: [
// keep Whitspace in error message!
new GraphQLError(`Could not find any entity of type "UserContact" matching: {
"emailVerificationCode": "not-valid"
"where": {
"emailVerificationCode": "not-valid"
}
}`),
],
}),
@ -1175,20 +1183,20 @@ describe('UserResolver', () => {
locale: 'en',
},
})
await expect(User.findOne()).resolves.toEqual(
await expect(User.find()).resolves.toEqual([
expect.objectContaining({
firstName: 'Benjamin',
lastName: 'Blümchen',
language: 'en',
}),
)
])
})
it('stores the USER_INFO_UPDATE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_INFO_UPDATE,
@ -1212,11 +1220,11 @@ describe('UserResolver', () => {
alias: 'bibi_Bloxberg',
},
})
await expect(User.findOne()).resolves.toEqual(
await expect(User.find()).resolves.toEqual([
expect.objectContaining({
alias: 'bibi_Bloxberg',
}),
)
])
})
})
})
@ -1433,10 +1441,10 @@ describe('UserResolver', () => {
let bibi: User
beforeAll(async () => {
const usercontact = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const usercontact = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
bibi = usercontact.user
bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL
bibi.password = SecretKeyCryptographyCreateKey(
@ -1450,10 +1458,10 @@ describe('UserResolver', () => {
it('changes to gradidoID on login', async () => {
await mutate({ mutation: login, variables })
const usercontact = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const usercontact = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
bibi = usercontact.user
expect(bibi).toEqual(
@ -1590,14 +1598,14 @@ describe('UserResolver', () => {
})
it('stores the ADMIN_USER_ROLE_SET event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const adminConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
const adminConatct = await UserContact.findOneOrFail({
where: { email: 'peter@lustig.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_USER_ROLE_SET,
@ -1792,14 +1800,15 @@ describe('UserResolver', () => {
})
it('stores the ADMIN_USER_DELETE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'], withDeleted: true },
)
const adminConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
withDeleted: true,
})
const adminConatct = await UserContact.findOneOrFail({
where: { email: 'peter@lustig.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_USER_DELETE,
@ -1943,10 +1952,10 @@ describe('UserResolver', () => {
})
it('sends an account activation email', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{optin}/g,
userConatct.emailVerificationCode.toString(),
@ -1965,10 +1974,10 @@ describe('UserResolver', () => {
})
it('stores the EMAIL_ADMIN_CONFIRMATION event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.EMAIL_ADMIN_CONFIRMATION,
@ -2086,14 +2095,14 @@ describe('UserResolver', () => {
})
it('stores the ADMIN_USER_UNDELETE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const adminConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
const adminConatct = await UserContact.findOneOrFail({
where: { email: 'peter@lustig.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_USER_UNDELETE,

View File

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
import { getConnection, IsNull, Not } from '@dbTools/typeorm'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { User as DbUser } from '@entity/User'
@ -23,7 +23,6 @@ import { UserContactType } from '@enum/UserContactType'
import { SearchAdminUsersResult } from '@model/AdminUser'
import { User } from '@model/User'
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
import { UserRepository } from '@repository/User'
import { subscribe } from '@/apis/KlicktippController'
import { encode } from '@/auth/JWT'
@ -65,6 +64,7 @@ import { randombytes_random } from 'sodium-native'
import { FULL_CREATION_AVAILABLE } from './const/const'
import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier'
import { findUsers } from './util/findUsers'
import { getKlicktippState } from './util/getKlicktippState'
import { validateAlias } from './util/validateAlias'
@ -82,13 +82,13 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
emailContact.type = UserContactType.USER_CONTACT_EMAIL
emailContact.emailChecked = false
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
emailContact.emailVerificationCode = random(64)
emailContact.emailVerificationCode = random(64).toString()
logger.debug('newEmailContact...successful', emailContact)
return emailContact
}
// eslint-disable-next-line @typescript-eslint/ban-types
export const activationLink = (verificationCode: BigInt): string => {
export const activationLink = (verificationCode: string): string => {
logger.debug(`activationLink(${verificationCode})...`)
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
}
@ -270,7 +270,7 @@ export class UserResolver {
if (redeemCode) {
if (redeemCode.match(/^CL-/)) {
const contributionLink = await DbContributionLink.findOne({
code: redeemCode.replace('CL-', ''),
where: { code: redeemCode.replace('CL-', '') },
})
logger.info('redeemCode found contributionLink', contributionLink)
if (contributionLink) {
@ -278,7 +278,7 @@ export class UserResolver {
eventRegisterRedeem.involvedContributionLink = contributionLink
}
} else {
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
const transactionLink = await DbTransactionLink.findOne({ where: { code: redeemCode } })
logger.info('redeemCode found transactionLink', transactionLink)
if (transactionLink) {
dbUser.referrerId = transactionLink.userId
@ -352,7 +352,8 @@ export class UserResolver {
const user = await findUserByEmail(email).catch(() => {
logger.warn(`fail on find UserContact per ${email}`)
})
if (!user) {
if (!user || user.deletedAt) {
logger.warn(`no user found with ${email}`)
return true
}
@ -365,7 +366,7 @@ export class UserResolver {
user.emailContact.updatedAt = new Date()
user.emailContact.emailResendCount++
user.emailContact.emailVerificationCode = random(64)
user.emailContact.emailVerificationCode = random(64).toString()
user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD
await user.emailContact.save().catch(() => {
throw new LogError('Unable to save email verification code', user.emailContact)
@ -403,10 +404,10 @@ export class UserResolver {
}
// load code
const userContact = await DbUserContact.findOneOrFail(
{ emailVerificationCode: code },
{ relations: ['user'] },
).catch(() => {
const userContact = await DbUserContact.findOneOrFail({
where: { emailVerificationCode: code },
relations: ['user'],
}).catch(() => {
throw new LogError('Could not login with emailVerificationCode')
})
logger.debug('userContact loaded...')
@ -474,7 +475,9 @@ export class UserResolver {
@Query(() => Boolean)
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
logger.info(`queryOptIn(${optIn})...`)
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
const userContact = await DbUserContact.findOneOrFail({
where: { emailVerificationCode: optIn },
})
logger.debug('found optInCode', userContact)
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
@ -601,9 +604,7 @@ export class UserResolver {
@Args()
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
): Promise<SearchAdminUsersResult> {
const userRepository = getCustomRepository(UserRepository)
const [users, count] = await userRepository.findAndCount({
const [users, count] = await DbUser.findAndCount({
where: {
isAdmin: Not(IsNull()),
},
@ -636,7 +637,6 @@ export class UserResolver {
@Ctx() context: Context,
): Promise<SearchUsersResult> {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const userRepository = getCustomRepository(UserRepository)
const userFields = [
'id',
'firstName',
@ -646,7 +646,7 @@ export class UserResolver {
'deletedAt',
'isAdmin',
]
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
const [users, count] = await findUsers(
userFields.map((fieldName) => {
return 'user.' + fieldName
}),
@ -705,7 +705,7 @@ export class UserResolver {
@Ctx()
context: Context,
): Promise<Date | null> {
const user = await DbUser.findOne({ id: userId })
const user = await DbUser.findOne({ where: { id: userId } })
// user exists ?
if (!user) {
throw new LogError('Could not find user with given ID', userId)
@ -734,7 +734,7 @@ export class UserResolver {
}
await user.save()
await EVENT_ADMIN_USER_ROLE_SET(user, moderator)
const newUser = await DbUser.findOne({ id: userId })
const newUser = await DbUser.findOne({ where: { id: userId } })
return newUser ? newUser.isAdmin : null
}
@ -744,7 +744,7 @@ export class UserResolver {
@Arg('userId', () => Int) userId: number,
@Ctx() context: Context,
): Promise<Date | null> {
const user = await DbUser.findOne({ id: userId })
const user = await DbUser.findOne({ where: { id: userId } })
// user exists ?
if (!user) {
throw new LogError('Could not find user with given ID', userId)
@ -757,7 +757,7 @@ export class UserResolver {
// soft-delete user
await user.softRemove()
await EVENT_ADMIN_USER_DELETE(user, moderator)
const newUser = await DbUser.findOne({ id: userId }, { withDeleted: true })
const newUser = await DbUser.findOne({ where: { id: userId }, withDeleted: true })
return newUser ? newUser.deletedAt : null
}
@ -767,7 +767,7 @@ export class UserResolver {
@Arg('userId', () => Int) userId: number,
@Ctx() context: Context,
): Promise<Date | null> {
const user = await DbUser.findOne({ id: userId }, { withDeleted: true })
const user = await DbUser.findOne({ where: { id: userId }, withDeleted: true })
if (!user) {
throw new LogError('Could not find user with given ID', userId)
}
@ -819,10 +819,11 @@ export class UserResolver {
}
export async function findUserByEmail(email: string): Promise<DbUser> {
const dbUserContact = await DbUserContact.findOneOrFail(
{ email },
{ withDeleted: true, relations: ['user'] },
).catch(() => {
const dbUserContact = await DbUserContact.findOneOrFail({
where: { email },
withDeleted: true,
relations: ['user'],
}).catch(() => {
throw new LogError('No user with this credentials', email)
})
const dbUser = dbUserContact.user
@ -831,7 +832,10 @@ export async function findUserByEmail(email: string): Promise<DbUser> {
}
async function checkEmailExists(email: string): Promise<boolean> {
const userContact = await DbUserContact.findOne({ email }, { withDeleted: true })
const userContact = await DbUserContact.findOne({
where: { email },
withDeleted: true,
})
if (userContact) {
return true
}

View File

@ -4,6 +4,7 @@
import { Connection } from '@dbTools/typeorm'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { Decimal } from 'decimal.js-light'
import { GraphQLError } from 'graphql'
import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers'
@ -219,7 +220,7 @@ describe('semaphore', () => {
})
})
it('does not throw, but should', async () => {
it('does throw error on second redeem call', async () => {
const redeem1 = mutate({
mutation: redeemTransactionLink,
variables: {
@ -236,7 +237,7 @@ describe('semaphore', () => {
errors: undefined,
})
await expect(redeem2).resolves.toMatchObject({
errors: undefined,
errors: [new GraphQLError('Transaction link already redeemed')],
})
})
})

View File

@ -0,0 +1,36 @@
import { In } from '@dbTools/typeorm'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { ContributionMessageType } from '@enum/ContributionMessageType'
import { Order } from '@enum/Order'
interface FindContributionMessagesOptions {
contributionId: number
pageSize: number
currentPage: number
order: Order
showModeratorType?: boolean
}
export const findContributionMessages = async (
options: FindContributionMessagesOptions,
): Promise<[DbContributionMessage[], number]> => {
const { contributionId, pageSize, currentPage, order, showModeratorType } = options
const messageTypes = [ContributionMessageType.DIALOG, ContributionMessageType.HISTORY]
if (showModeratorType) messageTypes.push(ContributionMessageType.MODERATOR)
return DbContributionMessage.findAndCount({
where: {
contributionId,
type: In(messageTypes),
},
relations: ['user'],
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
}

View File

@ -7,20 +7,20 @@ import { LogError } from '@/server/LogError'
import { VALID_ALIAS_REGEX } from './validateAlias'
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
let user: DbUser | undefined
let user: DbUser | null
if (validate(identifier) && version(identifier) === 4) {
user = await DbUser.findOne({ where: { gradidoID: identifier }, relations: ['emailContact'] })
if (!user) {
throw new LogError('No user found to given identifier', identifier)
}
} else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) {
const userContact = await DbUserContact.findOne(
{
const userContact = await DbUserContact.findOne({
where: {
email: identifier,
emailChecked: true,
},
{ relations: ['user'] },
)
relations: ['user'],
})
if (!userContact) {
throw new LogError('No user with this credentials', identifier)
}

View File

@ -1,20 +1,24 @@
import { Brackets, EntityRepository, IsNull, Not, Repository } from '@dbTools/typeorm'
import { getConnection, Brackets, IsNull, Not } from '@dbTools/typeorm'
import { User as DbUser } from '@entity/User'
import { SearchUsersFilters } from '@arg/SearchUsersFilters'
import { Order } from '@enum/Order'
@EntityRepository(DbUser)
export class UserRepository extends Repository<DbUser> {
async findBySearchCriteriaPagedFiltered(
select: string[],
searchCriteria: string,
filters: SearchUsersFilters | null,
currentPage: number,
pageSize: number,
order = Order.ASC,
): Promise<[DbUser[], number]> {
const query = this.createQueryBuilder('user')
import { LogError } from '@/server/LogError'
export const findUsers = async (
select: string[],
searchCriteria: string,
filters: SearchUsersFilters | null,
currentPage: number,
pageSize: number,
order = Order.ASC,
): Promise<[DbUser[], number]> => {
const queryRunner = getConnection().createQueryRunner()
try {
await queryRunner.connect()
const query = queryRunner.manager
.createQueryBuilder(DbUser, 'user')
.select(select)
.withDeleted()
.leftJoinAndSelect('user.emailContact', 'emailContact')
@ -30,27 +34,24 @@ export class UserRepository extends Repository<DbUser> {
)
}),
)
/*
filterCriteria.forEach((filter) => {
query.andWhere(filter)
})
*/
if (filters) {
if (filters.byActivated !== null) {
query.andWhere('emailContact.emailChecked = :value', { value: filters.byActivated })
// filterCriteria.push({ 'emailContact.emailChecked': filters.byActivated })
}
if (filters.byDeleted !== null) {
// filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
query.andWhere({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
}
}
return query
return await query
.orderBy({ 'user.id': order })
.take(pageSize)
.skip((currentPage - 1) * pageSize)
.getManyAndCount()
} catch (err) {
throw new LogError('Unable to search users', err)
} finally {
await queryRunner.release()
}
}

View File

@ -3,12 +3,10 @@ import { Transaction as DbTransaction } from '@entity/Transaction'
export const getLastTransaction = async (
userId: number,
relations?: string[],
): Promise<DbTransaction | undefined> => {
return DbTransaction.findOne(
{ userId },
{
order: { balanceDate: 'DESC', id: 'DESC' },
relations,
},
)
): Promise<DbTransaction | null> => {
return DbTransaction.findOne({
where: { userId },
order: { balanceDate: 'DESC', id: 'DESC' },
relations,
})
}

View File

@ -1,4 +1,4 @@
import { MoreThan } from '@dbTools/typeorm'
import { MoreThan, IsNull } from '@dbTools/typeorm'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { User as DbUser } from '@entity/User'
@ -22,7 +22,7 @@ export async function transactionLinkList(
const [transactionLinks, count] = await DbTransactionLink.findAndCount({
where: {
userId: user.id,
...(!withRedeemed && { redeemedBy: null }),
...(!withRedeemed && { redeemedBy: IsNull() }),
...(!withExpired && { validUntil: MoreThan(new Date()) }),
},
withDeleted,

View File

@ -0,0 +1,49 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { getConnection } from '@dbTools/typeorm'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { Decimal } from 'decimal.js-light'
import { LogError } from '@/server/LogError'
export const transactionLinkSummary = async (
userId: number,
date: Date,
): Promise<{
sumHoldAvailableAmount: Decimal
sumAmount: Decimal
lastDate: Date | null
firstDate: Date | null
transactionLinkcount: number
}> => {
const queryRunner = getConnection().createQueryRunner()
try {
await queryRunner.connect()
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, count } =
await queryRunner.manager
.createQueryBuilder(DbTransactionLink, 'transactionLink')
.select('SUM(transactionLink.holdAvailableAmount)', 'sumHoldAvailableAmount')
.addSelect('SUM(transactionLink.amount)', 'sumAmount')
.addSelect('MAX(transactionLink.validUntil)', 'lastDate')
.addSelect('MIN(transactionLink.createdAt)', 'firstDate')
.addSelect('COUNT(*)', 'count')
.where('transactionLink.userId = :userId', { userId })
.andWhere('transactionLink.redeemedAt is NULL')
.andWhere('transactionLink.validUntil > :date', { date })
.orderBy('transactionLink.createdAt', 'DESC')
.getRawOne()
return {
sumHoldAvailableAmount: sumHoldAvailableAmount
? new Decimal(sumHoldAvailableAmount)
: new Decimal(0),
sumAmount: sumAmount ? new Decimal(sumAmount) : new Decimal(0),
lastDate: lastDate || null,
firstDate: firstDate || null,
transactionLinkcount: count || 0,
}
} catch (err) {
throw new LogError('Unable to get transaction link summary', err)
} finally {
await queryRunner.release()
}
}

View File

@ -95,7 +95,7 @@ describe('validate alias', () => {
describe('test against existing alias in database', () => {
beforeAll(async () => {
const bibi = await userFactory(testEnv, bibiBloxberg)
const user = await User.findOne({ id: bibi.id })
const user = await User.findOne({ where: { id: bibi.id } })
if (user) {
user.alias = 'b-b'
await user.save()

View File

@ -19,7 +19,10 @@ export const creationFactory = async (
creation: CreationInterface,
): Promise<Contribution> => {
const { mutate } = client
await mutate({ mutation: login, variables: { email: creation.email, password: 'Aa12345_' } })
await mutate({
mutation: login,
variables: { email: creation.email, password: 'Aa12345_' },
})
const {
data: { createContribution: contribution },
@ -30,7 +33,9 @@ export const creationFactory = async (
await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
await mutate({ mutation: confirmContribution, variables: { id: contribution.id } })
const confirmedContribution = await Contribution.findOneOrFail({ id: contribution.id })
const confirmedContribution = await Contribution.findOneOrFail({
where: { id: contribution.id },
})
if (creation.moveCreationDate) {
const transaction = await Transaction.findOneOrFail({

View File

@ -32,7 +32,7 @@ export const transactionLinkFactory = async (
} = await mutate({ mutation: createTransactionLink, variables })
if (transactionLink.createdAt || transactionLink.deletedAt) {
const dbTransactionLink = await TransactionLink.findOneOrFail({ id })
const dbTransactionLink = await TransactionLink.findOneOrFail({ where: { id } })
if (transactionLink.createdAt) {
dbTransactionLink.createdAt = transactionLink.createdAt

View File

@ -19,7 +19,7 @@ export const userFactory = async (
} = await mutate({ mutation: createUser, variables: user })
// console.log('creatUser:', { id }, { user })
// get user from database
let dbUser = await User.findOneOrFail({ id }, { relations: ['emailContact'] })
let dbUser = await User.findOneOrFail({ where: { id }, relations: ['emailContact'] })
// console.log('dbUser:', dbUser)
const emailContact = dbUser.emailContact
@ -33,7 +33,7 @@ export const userFactory = async (
}
// get last changes of user from database
dbUser = await User.findOneOrFail({ id })
dbUser = await User.findOneOrFail({ where: { id } })
if (user.createdAt || user.deletedAt || user.isAdmin) {
if (user.createdAt) dbUser.createdAt = user.createdAt

View File

@ -284,8 +284,12 @@ export const createContributionMessage = gql`
`
export const adminCreateContributionMessage = gql`
mutation ($contributionId: Int!, $message: String!) {
adminCreateContributionMessage(contributionId: $contributionId, message: $message) {
mutation ($contributionId: Int!, $message: String!, $messageType: ContributionMessageType) {
adminCreateContributionMessage(
contributionId: $contributionId
message: $message
messageType: $messageType
) {
id
message
createdAt

View File

@ -195,7 +195,7 @@ export const listContributions = gql`
confirmedAt
confirmedBy
deletedAt
state
status
messagesCount
deniedAt
deniedBy
@ -218,7 +218,7 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
confirmedAt
confirmedBy
contributionDate
state
status
messagesCount
deniedAt
deniedBy
@ -254,7 +254,7 @@ export const adminListContributions = gql`
confirmedAt
confirmedBy
contributionDate
state
status
messagesCount
deniedAt
deniedBy
@ -349,6 +349,29 @@ export const listContributionMessages = gql`
}
`
export const adminListContributionMessages = gql`
query ($contributionId: Int!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
adminListContributionMessages(
contributionId: $contributionId
pageSize: $pageSize
currentPage: $currentPage
order: $order
) {
count
messages {
id
message
createdAt
updatedAt
type
userFirstName
userLastName
userId
}
}
}
`
export const user = gql`
query ($identifier: String!) {
user(identifier: $identifier) {

View File

@ -1,3 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { entities } from '@entity/index'
import { createTestClient } from 'apollo-server-testing'
import { name, internet, datatype } from 'faker'
@ -36,12 +42,10 @@ export const cleanDB = async () => {
}
}
const [entityTypes] = entities
const resetEntity = async (entity: typeof entityTypes) => {
const resetEntity = async (entity: any) => {
const items = await entity.find({ withDeleted: true })
if (items.length > 0) {
const ids = items.map((i) => i.id)
const ids = items.map((e: any) => e.id)
await entity.delete(ids)
}
}

View File

@ -15,7 +15,7 @@ export interface Context {
clientTimezoneOffset?: number
gradidoID?: string
// hack to use less DB calls for Balance Resolver
lastTransaction?: dbTransaction
lastTransaction?: dbTransaction | null
transactionCount?: number
linkCount?: number
sumHoldAvailableAmount?: Decimal

View File

@ -4,7 +4,7 @@ import { backendLogger as logger } from '@/server/logger'
const getDBVersion = async (): Promise<string | null> => {
try {
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
const [dbVersion] = await Migration.find({ order: { version: 'DESC' }, take: 1 })
return dbVersion ? dbVersion.fileName : null
} catch (error) {
logger.error(error)

View File

@ -1,41 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Repository, EntityRepository } from '@dbTools/typeorm'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { Decimal } from 'decimal.js-light'
@EntityRepository(dbTransactionLink)
export class TransactionLinkRepository extends Repository<dbTransactionLink> {
async summary(
userId: number,
date: Date,
): Promise<{
sumHoldAvailableAmount: Decimal
sumAmount: Decimal
lastDate: Date | null
firstDate: Date | null
transactionLinkcount: number
}> {
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, count } =
await this.createQueryBuilder('transactionLinks')
.select('SUM(transactionLinks.holdAvailableAmount)', 'sumHoldAvailableAmount')
.addSelect('SUM(transactionLinks.amount)', 'sumAmount')
.addSelect('MAX(transactionLinks.validUntil)', 'lastDate')
.addSelect('MIN(transactionLinks.createdAt)', 'firstDate')
.addSelect('COUNT(*)', 'count')
.where('transactionLinks.userId = :userId', { userId })
.andWhere('transactionLinks.redeemedAt is NULL')
.andWhere('transactionLinks.validUntil > :date', { date })
.orderBy('transactionLinks.createdAt', 'DESC')
.getRawOne()
return {
sumHoldAvailableAmount: sumHoldAvailableAmount
? new Decimal(sumHoldAvailableAmount)
: new Decimal(0),
sumAmount: sumAmount ? new Decimal(sumAmount) : new Decimal(0),
lastDate: lastDate || null,
firstDate: firstDate || null,
transactionLinkcount: count || 0,
}
}
}

View File

@ -0,0 +1,4 @@
import { Semaphore } from 'await-semaphore'
const CONCURRENT_TRANSACTIONS = 1
export const TRANSACTION_LINK_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)

View File

@ -1,6 +1,6 @@
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
export async function hasElopageBuys(email: string): Promise<boolean> {
const elopageBuyCount = await LoginElopageBuys.count({ payerEmail: email })
const elopageBuyCount = await LoginElopageBuys.count({ where: { payerEmail: email } })
return elopageBuyCount > 0
}

View File

@ -1,11 +1,10 @@
import { getCustomRepository } from '@dbTools/typeorm'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { Decimal } from 'decimal.js-light'
import { Decay } from '@model/Decay'
import { TransactionLinkRepository } from '@repository/TransactionLink'
import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction'
import { transactionLinkSummary } from '@/graphql/resolver/util/transactionLinkSummary'
import { calculateDecay } from './decay'
@ -29,8 +28,7 @@ async function calculateBalance(
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
const balance = decay.balance.add(amount.toString())
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time)
const { sumHoldAvailableAmount } = await transactionLinkSummary(userId, time)
// If we want to redeem a link we need to make sure that the link amount is not considered as blocked
// else we cannot redeem links which are more or equal to half of what an account actually owns

View File

@ -135,7 +135,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
// Do we already have such a user?
// if ((await dbUser.count({ email })) !== 0) {
if ((await dbUserContact.count({ email })) !== 0) {
if ((await dbUserContact.count({ where: { email } })) !== 0) {
// eslint-disable-next-line no-console
console.log(`Did not create User - already exists with email: ${email}`)
return

View File

@ -1,4 +1,10 @@
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { entities } from '@entity/index'
import { createTestClient } from 'apollo-server-testing'
@ -7,7 +13,6 @@ import { createServer } from '@/server/createServer'
import { i18n, logger } from './testSetup'
export const headerPushMock = jest.fn((t) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
context.token = t.value
})
@ -21,7 +26,7 @@ const context = {
}
export const cleanDB = async () => {
// this only works as lond we do not have foreign key constraints
// this only works as long we do not have foreign key constraints
for (const entity of entities) {
await resetEntity(entity)
}
@ -36,12 +41,10 @@ export const testEnvironment = async (testLogger = logger, testI18n = i18n) => {
return { mutate, query, con }
}
const [entityTypes] = entities
export const resetEntity = async (entity: typeof entityTypes) => {
export const resetEntity = async (entity: any) => {
const items = await entity.find({ withDeleted: true })
if (items.length > 0) {
const ids = items.map((i) => i.id)
const ids = items.map((e: any) => e.id)
await entity.delete(ids)
}
}

View File

@ -32,7 +32,7 @@ export class UserContact extends BaseEntity {
email: string
@Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true })
emailVerificationCode: BigInt
emailVerificationCode: string
@Column({ name: 'email_opt_in_type_id' })
emailOptInTypeId: number

View File

@ -46,7 +46,7 @@
"mysql2": "^2.3.0",
"reflect-metadata": "^0.1.13",
"ts-mysql-migrate": "^1.0.2",
"typeorm": "^0.2.38",
"typeorm": "^0.3.16",
"uuid": "^8.3.2"
},
"engines": {

View File

@ -2,6 +2,13 @@
# yarn lockfile v1
"@babel/runtime@^7.21.0":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
dependencies:
regenerator-runtime "^0.13.11"
"@cspotcode/source-map-consumer@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
@ -106,10 +113,10 @@
picocolors "^1.0.0"
tslib "^2.5.0"
"@sqltools/formatter@^1.2.2":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.3.tgz#1185726610acc37317ddab11c3c7f9066966bd20"
integrity sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==
"@sqltools/formatter@^1.2.5":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12"
integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==
"@tsconfig/node10@^1.0.7":
version "1.0.8"
@ -173,11 +180,6 @@
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/zen-observable@0.8.3":
version "0.8.3"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3"
integrity sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==
"@typescript-eslint/eslint-plugin@^5.57.1":
version "5.59.9"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz#2604cfaf2b306e120044f901e20c8ed926debf15"
@ -292,11 +294,6 @@ ajv@^6.10.0, ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
ansi-regex@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
@ -307,11 +304,6 @@ ansi-regex@^5.0.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
@ -324,10 +316,10 @@ any-promise@^1.0.0:
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
app-root-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad"
integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==
app-root-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86"
integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==
arg@^4.1.0:
version "4.1.3"
@ -423,6 +415,13 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@ -465,18 +464,7 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
chalk@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
dependencies:
ansi-styles "^2.2.1"
escape-string-regexp "^1.0.2"
has-ansi "^2.0.0"
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^4.0.0, chalk@^4.1.0:
chalk@^4.0.0, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -505,6 +493,15 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@ -553,6 +550,13 @@ crypto@^1.0.1:
resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037"
integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==
date-fns@^2.29.3:
version "2.30.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
dependencies:
"@babel/runtime" "^7.21.0"
debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
@ -560,7 +564,7 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@^4.1.1, debug@^4.3.1:
debug@^4.1.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@ -658,10 +662,10 @@ dotenv@^10.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
dotenv@^8.2.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
dotenv@^16.0.3:
version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
emoji-regex@^8.0.0:
version "8.0.0"
@ -746,7 +750,7 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
@ -1054,11 +1058,6 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
figlet@^1.1.1:
version "1.5.2"
resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.5.2.tgz#dda34ff233c9a48e36fcff6741aeb5bafe49b634"
integrity sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -1203,17 +1202,16 @@ glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.1.6:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
glob@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
minimatch "^5.0.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^13.19.0:
version "13.20.0"
@ -1274,13 +1272,6 @@ graphemer@^1.4.0:
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
dependencies:
ansi-regex "^2.0.0"
has-bigints@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
@ -1594,7 +1585,7 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
js-yaml@^4.0.0, js-yaml@^4.1.0:
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
@ -1705,6 +1696,13 @@ minimatch@^3.0.5, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.0:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
@ -1715,10 +1713,10 @@ minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mkdirp@^2.1.3:
version "2.1.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19"
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
ms@2.1.2:
version "2.1.2"
@ -1897,11 +1895,6 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
parent-require@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/parent-require/-/parent-require-1.0.0.tgz#746a167638083a860b0eef6732cb27ed46c32977"
integrity sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc=
parse5-htmlparser2-tree-adapter@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
@ -2014,6 +2007,11 @@ reflect-metadata@^0.1.13:
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
regenerator-runtime@^0.13.11:
version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regexp-tree@~0.1.1:
version "0.1.27"
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd"
@ -2114,11 +2112,6 @@ safe-regex@^2.1.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sax@>=0.6.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@ -2199,6 +2192,15 @@ string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string.prototype.trim@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533"
@ -2233,13 +2235,6 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
strip-ansi@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
dependencies:
ansi-regex "^2.0.0"
strip-ansi@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
@ -2274,11 +2269,6 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@ -2376,11 +2366,6 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
tslib@^2.5.0:
version "2.5.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913"
@ -2414,28 +2399,26 @@ typed-array-length@^1.0.4:
for-each "^0.3.3"
is-typed-array "^1.1.9"
typeorm@^0.2.38:
version "0.2.38"
resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.38.tgz#2af08079919f6ab04cd17017f9faa2c8d5cd566f"
integrity sha512-M6Y3KQcAREQcphOVJciywf4mv6+A0I/SeR+lWNjKsjnQ+a3XcMwGYMGL0Jonsx3H0Cqlf/3yYqVki1jIXSK/xg==
typeorm@^0.3.16:
version "0.3.17"
resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.17.tgz#a73c121a52e4fbe419b596b244777be4e4b57949"
integrity sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==
dependencies:
"@sqltools/formatter" "^1.2.2"
app-root-path "^3.0.0"
"@sqltools/formatter" "^1.2.5"
app-root-path "^3.1.0"
buffer "^6.0.3"
chalk "^4.1.0"
chalk "^4.1.2"
cli-highlight "^2.1.11"
debug "^4.3.1"
dotenv "^8.2.0"
glob "^7.1.6"
js-yaml "^4.0.0"
mkdirp "^1.0.4"
date-fns "^2.29.3"
debug "^4.3.4"
dotenv "^16.0.3"
glob "^8.1.0"
mkdirp "^2.1.3"
reflect-metadata "^0.1.13"
sha.js "^2.4.11"
tslib "^2.1.0"
xml2js "^0.4.23"
yargonaut "^1.1.4"
yargs "^17.0.1"
zen-observable-ts "^1.0.0"
tslib "^2.5.0"
uuid "^9.0.0"
yargs "^17.6.2"
typescript@^4.3.5:
version "4.3.5"
@ -2474,6 +2457,11 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
@ -2523,19 +2511,6 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
xml2js@^0.4.23:
version "0.4.23"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
dependencies:
sax ">=0.6.0"
xmlbuilder "~11.0.0"
xmlbuilder@~11.0.0:
version "11.0.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
@ -2551,20 +2526,16 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yargonaut@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/yargonaut/-/yargonaut-1.1.4.tgz#c64f56432c7465271221f53f5cc517890c3d6e0c"
integrity sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==
dependencies:
chalk "^1.1.1"
figlet "^1.1.1"
parent-require "^1.0.0"
yargs-parser@^20.2.2:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs@^16.0.0:
version "16.2.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
@ -2578,18 +2549,18 @@ yargs@^16.0.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs@^17.0.1:
version "17.2.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea"
integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==
yargs@^17.6.2:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
dependencies:
cliui "^7.0.2"
cliui "^8.0.1"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.0"
string-width "^4.2.3"
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs-parser "^21.1.1"
yn@3.1.1:
version "3.1.1"
@ -2600,16 +2571,3 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zen-observable-ts@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz#2d1aa9d79b87058e9b75698b92791c1838551f83"
integrity sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==
dependencies:
"@types/zen-observable" "0.8.3"
zen-observable "0.8.15"
zen-observable@0.8.15:
version "0.8.15"
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==

View File

@ -234,7 +234,8 @@ crontab -l
This show all existing entries of the crontab for user `gradido`
To install/add the cronjob for a daily backup at 3:00am please
To install/add the cronjob for a daily backup at 3:00am please,
To install/add the cronjob for a daily klicktipp export at 4:00am please,
Run:
@ -244,4 +245,5 @@ crontab -e
and insert the following line
```bash
0 3 * * * ~/gradido/deployment/bare_metal/backup.sh
0 4 * * * cd ~/gradido/backend/ && yarn klicktipp && cd
```

View File

@ -22,9 +22,7 @@
"gradido-database": "file:../database",
"log4js": "^6.7.1",
"nodemon": "^2.0.20",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.2",
"typescript": "^4.9.4",
"uuid": "^8.3.2"
},
"devDependencies": {
@ -47,7 +45,9 @@
"eslint-plugin-security": "^1.7.1",
"prettier": "^2.8.7",
"jest": "^27.2.4",
"ts-jest": "^27.0.5"
"ts-jest": "^27.0.5",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"engines": {
"node": ">=14"

View File

@ -343,7 +343,7 @@ describe('federation', () => {
},
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbFederatedCommunity.find({ foreign: true })
result = await DbFederatedCommunity.find({ where: { foreign: true } })
})
afterAll(async () => {
@ -604,7 +604,7 @@ describe('federation', () => {
{ api: 'toolong api', url: 'some valid url' },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbFederatedCommunity.find({ foreign: true })
result = await DbFederatedCommunity.find({ where: { foreign: true } })
})
afterAll(async () => {
@ -658,7 +658,7 @@ describe('federation', () => {
},
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbFederatedCommunity.find({ foreign: true })
result = await DbFederatedCommunity.find({ where: { foreign: true } })
})
afterAll(async () => {
@ -794,7 +794,7 @@ describe('federation', () => {
]),
),
)
result = await DbFederatedCommunity.find({ foreign: true })
result = await DbFederatedCommunity.find({ where: { foreign: true } })
})
afterAll(async () => {

View File

@ -218,7 +218,7 @@ async function writeFederatedHomeCommunityEntries(pubKey: string): Promise<Commu
async function writeHomeCommunityEntry(keyPair: KeyPair): Promise<void> {
try {
// check for existing homeCommunity entry
let homeCom = await DbCommunity.findOne({ foreign: false })
let homeCom = await DbCommunity.findOne({ where: { foreign: false } })
if (homeCom) {
// simply update the existing entry, but it MUST keep the ID and UUID because of possible relations
homeCom.publicKey = keyPair.publicKey

View File

@ -4,7 +4,7 @@ import { logger } from '@/server/logger'
const getDBVersion = async (): Promise<string | null> => {
try {
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
const [dbVersion] = await Migration.find({ order: { version: 'DESC' }, take: 1 })
return dbVersion ? dbVersion.fileName : null
} catch (error) {
logger.error(error)

View File

@ -1 +1,2 @@
node_modules
playwright

View File

@ -2,13 +2,13 @@ module.exports = {
root: true,
env: {
node: true,
cypress: true,
},
parser: '@typescript-eslint/parser',
plugins: ['cypress', 'prettier', '@typescript-eslint' /*, 'jest' */],
extends: [
'standard',
'eslint:recommended',
'plugin:cypress/recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
],

View File

@ -6,7 +6,7 @@ let emailLink: string
async function setupNodeEvents(
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
config: Cypress.PluginConfigOptions,
): Promise<Cypress.PluginConfigOptions> {
await addCucumberPreprocessorPlugin(on, config)
@ -14,7 +14,7 @@ async function setupNodeEvents(
'file:preprocessor',
browserify(config, {
typescript: require.resolve('typescript'),
})
}),
)
on('task', {
@ -41,7 +41,6 @@ export default defineConfig({
e2e: {
specPattern: '**/*.feature',
excludeSpecPattern: '*.js',
experimentalSessionAndOrigin: true,
baseUrl: 'http://localhost:3000',
chromeWebSecurity: false,
defaultCommandTimeout: 10000,
@ -49,10 +48,7 @@ export default defineConfig({
viewportHeight: 720,
viewportWidth: 1280,
video: false,
retries: {
runMode: 2,
openMode: 0,
},
retries: 0,
env: {
backendURL: 'http://localhost:4000',
mailserverURL: 'http://localhost:1080',

View File

@ -0,0 +1,38 @@
Feature: Send coins
As a user
I want to send and receive GDD
I want to see transaction details on overview and transactions pages
# Background:
# Given the following "users" are in the database:
# | email | password | name |
# | bob@baumeister.de | Aa12345_ | Bob Baumeister |
# | raeuber@hotzenplotz.de | Aa12345_ | Räuber Hotzenplotz |
Scenario: Send GDD to other user
Given the user is logged in as "bob@baumeister.de" "Aa12345_"
And the user navigates to page "/send"
When the user fills the send form with "<receiverEmail>" "<amount>" "<memoText>"
And the user submits the send form
Then the transaction details are presented for confirmation "<receiverEmail>" "<amount>" "<memoText>" "<senderBalance>" "<newSenderBalance>"
When the user submits the transaction by confirming
Then the "<receiverName>" and "<amount>" are displayed on the "send" page
When the user navigates to page "/transactions"
Then the "<receiverName>" and "<amount>" are displayed on the "transactions" page
Examples:
| receiverName | receiverEmail | amount | memoText | senderBalance | newSenderBalance |
| Räuber Hotzenplotz | raeuber@hotzenplotz.de | 120.50 | Some memo text | 515.11 | 394.61 |
Scenario: Receive GDD from other user
Given the user is logged in as "raeuber@hotzenplotz.de" "Aa12345_"
And the user receives the transaction e-mail about "<amount>" GDD from "<senderName>"
When the user opens the "transaction" link in the browser
Then the "<senderName>" and "120.50" are displayed on the "overview" page
When the user navigates to page "/transactions"
Then the "<senderName>" and "120.50" are displayed on the "transactions" page
Examples:
| senderName | amount |
| Bob der Baumeister | 120,50 |

View File

@ -2,13 +2,27 @@ Feature: User Authentication - reset password
As a user
I want to reset my password from the sign in page
# TODO for these pre-conditions utilize seeding or API check, if user exists in test system
# Background:
# Given the following "users" are in the database:
# | email | password | name |
# | raeuber@hotzenplotz.de | Aa12345_ | Räuber Hotzenplotz |
Scenario: Reset password as not registered user
Given the user navigates to page "/login"
And the user navigates to the forgot password page
When the user enters the e-mail address "not@registered.com"
And the user submits the e-mail form
Then the user receives no password reset e-mail
Scenario: Reset password as deleted user
# Given the following "users" are in the database:
# | email | password | name |
# | stephen@hawking.uk | Aa12345_ | Stephen Hawking |
Given the user navigates to page "/login"
And the user navigates to the forgot password page
When the user enters the e-mail address "stephen@hawking.uk"
And the user submits the e-mail form
Then the user receives no password reset e-mail
Scenario: Reset password from signin page successfully
# Given the following "users" are in the database:
# | email | password | name |
# | raeuber@hotzenplotz.de | Aa12345_ | Räuber Hotzenplotz |
Given the user navigates to page "/login"
And the user navigates to the forgot password page
When the user enters the e-mail address "raeuber@hotzenplotz.de"
@ -23,3 +37,6 @@ Feature: User Authentication - reset password
And the user cannot login
But the user submits the credentials "raeuber@hotzenplotz.de" "12345Aa_"
And the user is logged in with username "Räuber Hotzenplotz"

View File

@ -2,6 +2,7 @@
export class OverviewPage {
navbarName = '[data-test="navbar-item-username"]'
rightLastTransactionsList = '.rightside-last-transactions'
goto() {
cy.visit('/overview')

View File

@ -14,9 +14,7 @@ export class ResetPasswordPage {
}
repeatNewPassword(password: string) {
cy.get(this.newPasswordRepeatInput)
.find('input[type=password]')
.type(password)
cy.get(this.newPasswordRepeatInput).find('input[type=password]').type(password)
return this
}

View File

@ -0,0 +1,25 @@
/// <reference types='cypress' />
export class SendPage {
confirmationBox = '.transaction-confirm-send'
submitBtn = '.btn-gradido'
enterReceiverEmail(email: string) {
cy.get('[data-test="input-identifier"]').find('input').clear().type(email)
return this
}
enterAmount(amount: string) {
cy.get('[data-test="input-amount"]').find('input').clear().type(amount)
return this
}
enterMemoText(text: string) {
cy.get('[data-test="input-textarea"]').find('textarea').clear().type(text)
return this
}
submit() {
cy.get(this.submitBtn).click()
}
}

View File

@ -8,10 +8,7 @@ export class UserEMailSite {
emailSubject = '.subject'
openRecentPasswordResetEMail() {
cy.get(this.emailList)
.find('email-item')
.filter(':contains(asswor)')
.click()
cy.get(this.emailList).find('email-item').filter(':contains(asswor)').click()
expect(cy.get(this.emailSubject)).to('contain', 'asswor')
}
}

View File

@ -7,6 +7,7 @@ import './e2e'
declare global {
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): Chainable<any>
}

View File

@ -1,4 +1,4 @@
import { Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
import { OverviewPage } from '../../e2e/models/OverviewPage'
import { SideNavMenu } from '../../e2e/models/SideNavMenu'
import { Toasts } from '../../e2e/models/Toasts'
@ -9,12 +9,9 @@ Given('the user navigates to page {string}', (page: string) => {
// login related
Given(
'the user is logged in as {string} {string}',
(email: string, password: string) => {
cy.login(email, password)
}
)
Given('the user is logged in as {string} {string}', (email: string, password: string) => {
cy.login(email, password)
})
Then('the user is logged in with username {string}', (username: string) => {
const overviewPage = new OverviewPage()

View File

@ -1,9 +1,9 @@
import { Then, When } from '@badeball/cypress-cucumber-preprocessor'
import { OverviewPage } from '../../e2e/models/OverviewPage'
import { ResetPasswordPage } from '../../e2e/models/ResetPasswordPage'
import { UserEMailSite } from '../../e2e/models/UserEMailSite'
const userEMailSite = new UserEMailSite()
const resetPasswordPage = new ResetPasswordPage()
Then('the user receives an e-mail containing the {string} link', (linkName: string) => {
let emailSubject: string
@ -18,14 +18,20 @@ Then('the user receives an e-mail containing the {string} link', (linkName: stri
emailSubject = 'asswor'
linkPattern = /\/reset-password\/[0-9]+\d/
break
case 'transaction':
emailSubject = 'Gradido gesendet'
linkPattern = /\/overview/
break
default:
throw new Error(`Error in "Then the user receives an e-mail containing the {string} link" step: incorrect linkname string "${linkName}"`)
throw new Error(
`Error in "Then the user receives an e-mail containing the {string} link" step: incorrect linkname string "${linkName}"`,
)
}
cy.origin(
Cypress.env('mailserverURL'),
{ args: { emailSubject, linkPattern, userEMailSite } },
({ emailSubject, linkPattern, userEMailSite }) => {
({ emailSubject, linkPattern, userEMailSite }) => {
cy.visit('/') // navigate to user's e-mail site (on fake mail server)
cy.get(userEMailSite.emailInbox).should('be.visible')
@ -35,11 +41,9 @@ Then('the user receives an e-mail containing the {string} link', (linkName: stri
.first()
.click()
cy.get(userEMailSite.emailMeta)
.find(userEMailSite.emailSubject)
.contains(emailSubject)
cy.get(userEMailSite.emailMeta).find(userEMailSite.emailSubject).contains(emailSubject)
cy.get('.email-content', { timeout: 2000})
cy.get('.email-content', { timeout: 2000 })
.find('.plain-text')
.contains(linkPattern)
.invoke('text')
@ -47,13 +51,79 @@ Then('the user receives an e-mail containing the {string} link', (linkName: stri
const emailLink = text.match(linkPattern)[0]
cy.task('setEmailLink', emailLink)
})
}
},
)
})
Then('the user receives no password reset e-mail', () => {
cy.origin(Cypress.env('mailserverURL'), { args: { userEMailSite } }, ({ userEMailSite }) => {
cy.visit('/')
cy.wait(300)
cy.get(userEMailSite.emailInbox).should('be.visible')
cy.get(userEMailSite.emailList).then(($emailList) => {
const emailItems = $emailList.find('.email-item')
if (emailItems.length > 0) {
expect(emailItems.filter(`:contains("asswor")`).length).to.equal(0)
}
})
})
})
When(
'the user receives the transaction e-mail about {string} GDD from {string}',
(amount: string, senderName: string) => {
cy.origin(
Cypress.env('mailserverURL'),
{ args: { amount, senderName, userEMailSite } },
({ amount, senderName, userEMailSite }) => {
const subject = `${senderName} hat dir ${amount} Gradido gesendet`
const linkPattern = /\/transactions/
cy.visit('/')
cy.get(userEMailSite.emailInbox).should('be.visible')
cy.get(userEMailSite.emailList)
.find('.email-item')
.filter(`:contains(${subject})`)
.first()
.click()
cy.get(userEMailSite.emailMeta).find(userEMailSite.emailSubject).contains(subject)
cy.get('.email-content', { timeout: 2000 })
.find('.plain-text')
.contains(linkPattern)
.invoke('text')
.then((text) => {
const emailLink = text.match(linkPattern)[0]
cy.task('setEmailLink', emailLink)
})
},
)
},
)
When('the user opens the {string} link in the browser', (linkName: string) => {
const resetPasswordPage = new ResetPasswordPage()
cy.task('getEmailLink').then((emailLink) => {
cy.visit(emailLink)
})
cy.get(resetPasswordPage.newPasswordInput).should('be.visible')
switch (linkName) {
case 'activation':
cy.get(resetPasswordPage.newPasswordInput).should('be.visible')
break
case 'password reset':
cy.get(resetPasswordPage.newPasswordInput).should('be.visible')
break
case 'transaction':
// eslint-disable-next-line no-case-declarations
const overviewPage = new OverviewPage()
cy.get(overviewPage.rightLastTransactionsList).should('be.visible')
break
default:
throw new Error(
`Error in "Then the user receives an e-mail containing the {string} link" step: incorrect link name string "${linkName}"`,
)
}
})

View File

@ -0,0 +1,90 @@
import { Then, When } from '@badeball/cypress-cucumber-preprocessor'
import { SendPage } from '../../e2e/models/SendPage'
const sendPage = new SendPage()
When(
'the user fills the send form with {string} {string} {string}',
(email: string, amount: string, memoText: string) => {
sendPage.enterReceiverEmail(email)
sendPage.enterAmount(amount)
sendPage.enterMemoText(memoText)
},
)
When('the user submits the send form', () => {
sendPage.submit()
cy.get(sendPage.confirmationBox).should('be.visible')
})
Then(
'the transaction details are presented for confirmation {string} {string} {string} {string} {string}',
(
receiverEmail: string,
sendAmount: string,
memoText: string,
senderBalance: string,
newSenderBalance: string,
) => {
cy.get('.transaction-confirm-send').contains(receiverEmail)
cy.get('.transaction-confirm-send').contains(`+ ${sendAmount} GDD`)
cy.get('.transaction-confirm-send').contains(memoText)
cy.get('.transaction-confirm-send').contains(`+ ${senderBalance} GDD`)
cy.get('.transaction-confirm-send').contains(` ${sendAmount} GDD`)
cy.get('.transaction-confirm-send').contains(`+ ${newSenderBalance} GDD`)
},
)
When('the user submits the transaction by confirming', () => {
cy.intercept({
method: 'POST',
url: '/graphql',
hostname: 'localhost',
}).as('sendCoins')
sendPage.submit()
cy.wait('@sendCoins').then((interception) => {
cy.wrap(interception.response?.statusCode).should('eq', 200)
cy.wrap(interception.request.body).should(
'have.property',
'query',
`mutation ($identifier: String!, $amount: Decimal!, $memo: String!) {
sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
}
`,
)
cy.wrap(interception.response?.body)
.should('have.nested.property', 'data.sendCoins')
.and('equal', true)
})
cy.get('[data-test="send-transaction-success-text"]').should('be.visible')
})
Then(
'the {string} and {string} are displayed on the {string} page',
(name: string, amount: string, page: string) => {
switch (page) {
case 'overview':
cy.get('.align-items-center').contains(`${name}`)
cy.get('.align-items-center').contains(`${amount} GDD`)
break
case 'send':
cy.get('.align-items-center').contains(`${name}`)
cy.get('.align-items-center').contains(`${amount} GDD`)
break
case 'transactions':
cy.get('div.mt-3 > div > div.test-list-group-item')
.eq(0)
.contains('div.gdd-transaction-list-item-name', `${name}`)
cy.get('div.mt-3 > div > div.test-list-group-item')
.eq(0)
.contains('[data-test="transaction-amount"]', `${amount} GDD`)
break
default:
throw new Error(
`Error in "Then the {string} and {string} are displayed on the {string}} page" step: incorrect page name string "${page}"`,
)
}
},
)

View File

@ -1,4 +1,4 @@
import { When, And } from '@badeball/cypress-cucumber-preprocessor'
import { When } from '@badeball/cypress-cucumber-preprocessor'
import { ForgotPasswordPage } from '../../e2e/models/ForgotPasswordPage'
import { LoginPage } from '../../e2e/models/LoginPage'
import { ResetPasswordPage } from '../../e2e/models/ResetPasswordPage'
@ -13,30 +13,25 @@ When('the user submits no credentials', () => {
loginPage.submitLogin()
})
When(
'the user submits the credentials {string} {string}',
(email: string, password: string) => {
cy.intercept('POST', '/graphql', (req) => {
if (
req.body.hasOwnProperty('query') &&
req.body.query.includes('mutation')
) {
req.alias = 'login'
}
})
When('the user submits the credentials {string} {string}', (email: string, password: string) => {
cy.intercept('POST', '/graphql', (req) => {
// eslint-disable-next-line no-prototype-builtins
if (req.body.hasOwnProperty('query') && req.body.query.includes('mutation')) {
req.alias = 'login'
}
})
loginPage.enterEmail(email)
loginPage.enterPassword(password)
loginPage.submitLogin()
cy.wait('@login').then((interception) => {
expect(interception.response.statusCode).equals(200)
})
}
)
loginPage.enterEmail(email)
loginPage.enterPassword(password)
loginPage.submitLogin()
cy.wait('@login').then((interception) => {
expect(interception.response.statusCode).equals(200)
})
})
// password reset related
And('the user navigates to the forgot password page', () => {
When('the user navigates to the forgot password page', () => {
loginPage.openForgotPasswordPage()
cy.url().should('include', '/forgot-password')
})
@ -45,25 +40,25 @@ When('the user enters the e-mail address {string}', (email: string) => {
forgotPasswordPage.enterEmail(email)
})
And('the user submits the e-mail form', () => {
When('the user submits the e-mail form', () => {
forgotPasswordPage.submitEmail()
cy.get(forgotPasswordPage.successComponent).should('be.visible')
})
And('the user enters the password {string}', (password: string) => {
When('the user enters the password {string}', (password: string) => {
resetPasswordPage.enterNewPassword(password)
})
And('the user repeats the password {string}', (password: string) => {
When('the user repeats the password {string}', (password: string) => {
resetPasswordPage.repeatNewPassword(password)
})
And('the user submits the new password', () => {
When('the user submits the new password', () => {
resetPasswordPage.submitNewPassword()
cy.get(resetPasswordPage.resetPasswordMessageBlock).should('be.visible')
})
And('the user clicks the sign in button', () => {
When('the user clicks the sign in button', () => {
resetPasswordPage.openSigninPage()
cy.url().should('contain', '/login')
})

View File

@ -1,28 +1,28 @@
import { And, When } from '@badeball/cypress-cucumber-preprocessor'
import { DataTable, When } from '@badeball/cypress-cucumber-preprocessor'
import { ProfilePage } from '../../e2e/models/ProfilePage'
import { Toasts } from '../../e2e/models/Toasts'
const profilePage = new ProfilePage()
And('the user opens the change password menu', () => {
When('the user opens the change password menu', () => {
cy.get(profilePage.openChangePassword).click()
cy.get(profilePage.newPasswordRepeatInput).should('be.visible')
cy.get(profilePage.submitNewPasswordBtn).should('be.disabled')
})
When('the user fills the password form with:', (table) => {
let hashedTableRows = table.rowsHash()
When('the user fills the password form with:', (table: DataTable) => {
const hashedTableRows = table.rowsHash()
profilePage.enterOldPassword(hashedTableRows['Old password'])
profilePage.enterNewPassword(hashedTableRows['New password'])
profilePage.enterRepeatPassword(hashedTableRows['Repeat new password'])
cy.get(profilePage.submitNewPasswordBtn).should('be.enabled')
})
And('the user submits the password form', () => {
When('the user submits the password form', () => {
profilePage.submitPasswordForm()
})
When('the user is presented a {string} message', (type: string) => {
When('the user is presented a {string} message', () => {
const toast = new Toasts()
cy.get(toast.toastSlot).within(() => {
cy.get(toast.toastTypeSuccess)

View File

@ -1,4 +1,4 @@
import { And, When } from '@badeball/cypress-cucumber-preprocessor'
import { When } from '@badeball/cypress-cucumber-preprocessor'
import { RegistrationPage } from '../../e2e/models/RegistrationPage'
const registrationPage = new RegistrationPage()
@ -10,14 +10,14 @@ When(
registrationPage.enterFirstname(firstname)
registrationPage.enterLastname(lastname)
registrationPage.enterEmail(email)
}
},
)
And('the user agrees to the privacy policy', () => {
When('the user agrees to the privacy policy', () => {
registrationPage.checkPrivacyCheckbox()
})
And('the user submits the registration form', () => {
When('the user submits the registration form', () => {
registrationPage.submitRegistrationForm()
cy.get(registrationPage.RegistrationThanxHeadline).should('be.visible')
cy.get(registrationPage.RegistrationThanxText).should('be.visible')

View File

@ -18,20 +18,20 @@
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
},
"dependencies": {
"@badeball/cypress-cucumber-preprocessor": "^12.0.0",
"@badeball/cypress-cucumber-preprocessor": "^18.0.1",
"@cypress/browserify-preprocessor": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"cypress": "^12.7.0",
"cypress": "^12.16.0",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
"eslint-loader": "^4.0.2",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-cypress": "^2.13.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-promise": "^6.1.1",
"jwt-decode": "^3.1.2",
"prettier": "^2.7.1",
"typescript": "^4.7.4"

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,10 @@ export class PublicKeyResolver {
async getPublicKey(): Promise<GetPublicKeyResult> {
logger.debug(`getPublicKey() via apiVersion=1_0 ...`)
const homeCom = await DbFederatedCommunity.findOneOrFail({
foreign: false,
apiVersion: '1_0',
where: {
foreign: false,
apiVersion: '1_0',
},
})
logger.info(`getPublicKey()-1_0... return publicKey=${homeCom.publicKey}`)
return new GetPublicKeyResult(homeCom.publicKey.toString())

View File

@ -3,7 +3,7 @@ import { federationLogger as logger } from '@/server/logger'
const getDBVersion = async (): Promise<string | null> => {
try {
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
const [dbVersion] = await Migration.find({ order: { version: 'DESC' }, take: 1 })
return dbVersion ? dbVersion.fileName : null
} catch (error) {
logger.error(error)

View File

@ -73,8 +73,8 @@ describe('ContributionMessagesFormular', () => {
)
})
it('emitted "update-state" with data', async () => {
expect(wrapper.emitted('update-state')).toEqual(
it('emitted "update-status" with data', async () => {
expect(wrapper.emitted('update-status')).toEqual(
expect.arrayContaining([expect.arrayContaining([42])]),
)
})

View File

@ -55,7 +55,7 @@ export default {
})
.then((result) => {
this.$emit('get-list-contribution-messages', false)
this.$emit('update-state', this.contributionId)
this.$emit('update-status', this.contributionId)
this.form.text = ''
this.toastSuccess(this.$t('message.reply'))
this.isSubmitting = false

View File

@ -8,7 +8,7 @@ describe('ContributionMessagesList', () => {
const propsData = {
contributionId: 42,
state: 'IN_PROGRESS',
status: 'IN_PROGRESS',
messages: [],
}
@ -40,13 +40,13 @@ describe('ContributionMessagesList', () => {
expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true)
})
describe('update State', () => {
describe('update Status', () => {
beforeEach(() => {
wrapper.vm.updateState()
wrapper.vm.updateStatus()
})
it('emits getListContributionMessages', async () => {
expect(wrapper.vm.$emit('update-state')).toBeTruthy()
expect(wrapper.vm.$emit('update-status')).toBeTruthy()
})
})
})

View File

@ -7,10 +7,10 @@
</div>
<div>
<contribution-messages-formular
v-if="['PENDING', 'IN_PROGRESS'].includes(state)"
v-if="['PENDING', 'IN_PROGRESS'].includes(status)"
:contributionId="contributionId"
v-on="$listeners"
@update-state="updateState"
@update-status="updateStatus"
/>
</div>
@ -37,7 +37,7 @@ export default {
type: Number,
required: true,
},
state: {
status: {
type: String,
required: true,
},
@ -47,8 +47,8 @@ export default {
},
},
methods: {
updateState(id) {
this.$emit('update-state', id)
updateStatus(id) {
this.$emit('update-status', id)
},
},
}

View File

@ -21,7 +21,7 @@ const mocks = {
describe('ContributionMessagesList', () => {
const propsData = {
contributionId: 42,
state: 'PENDING',
status: 'PENDING',
messages: [
{
id: 111,

View File

@ -119,11 +119,11 @@ describe('ContributionList', () => {
describe('update status', () => {
beforeEach(() => {
wrapper.findComponent({ name: 'ContributionListItem' }).vm.$emit('update-state', { id: 2 })
wrapper.findComponent({ name: 'ContributionListItem' }).vm.$emit('update-status', { id: 2 })
})
it('emits update status', () => {
expect(wrapper.emitted('update-state')).toEqual([[{ id: 2 }]])
expect(wrapper.emitted('update-status')).toEqual([[{ id: 2 }]])
})
})
})

View File

@ -2,26 +2,26 @@
<div class="contribution-list">
<div class="mb-3" v-for="item in items" :key="item.id + 'a'">
<contribution-list-item
v-if="item.state === 'IN_PROGRESS'"
v-if="item.status === 'IN_PROGRESS'"
v-bind="item"
@closeAllOpenCollapse="$emit('closeAllOpenCollapse')"
:contributionId="item.id"
:allContribution="allContribution"
@update-contribution-form="updateContributionForm"
@delete-contribution="deleteContribution"
@update-state="updateState"
@update-status="updateStatus"
/>
</div>
<div class="mb-3" v-for="item2 in items" :key="item2.id">
<contribution-list-item
v-if="item2.state !== 'IN_PROGRESS'"
v-if="item2.status !== 'IN_PROGRESS'"
v-bind="item2"
@closeAllOpenCollapse="$emit('closeAllOpenCollapse')"
:contributionId="item2.id"
:allContribution="allContribution"
@update-contribution-form="updateContributionForm"
@delete-contribution="deleteContribution"
@update-state="updateState"
@update-status="updateStatus"
/>
</div>
<b-pagination
@ -85,8 +85,8 @@ export default {
deleteContribution(item) {
this.$emit('delete-contribution', item)
},
updateState(id) {
this.$emit('update-state', id)
updateStatus(id) {
this.$emit('update-status', id)
},
},
computed: {

View File

@ -14,7 +14,7 @@ describe('ContributionListItem', () => {
const propsData = {
contributionId: 42,
state: 'PENDING',
status: 'PENDING',
messagesCount: 2,
id: 1,
createdAt: '26/07/2022',
@ -72,8 +72,8 @@ describe('ContributionListItem', () => {
expect(wrapper.vm.variant).toBe('success')
})
it('is warning at when state is IN_PROGRESS', async () => {
await wrapper.setProps({ state: 'IN_PROGRESS' })
it('is warning at when status is IN_PROGRESS', async () => {
await wrapper.setProps({ status: 'IN_PROGRESS' })
expect(wrapper.vm.variant).toBe('205')
})
})
@ -134,13 +134,13 @@ describe('ContributionListItem', () => {
})
})
describe('updateState', () => {
describe('updateStatus', () => {
beforeEach(async () => {
await wrapper.vm.updateState()
await wrapper.vm.updateStatus()
})
it('emit update-state', () => {
expect(wrapper.vm.$emit('update-state')).toBeTruthy()
it('emit update-status', () => {
expect(wrapper.vm.$emit('update-status')).toBeTruthy()
})
})
})

View File

@ -2,7 +2,7 @@
<div>
<div
class="contribution-list-item bg-white appBoxShadow gradido-border-radius pt-3 px-3"
:class="state === 'IN_PROGRESS' && !allContribution ? 'pulse border border-205' : ''"
:class="status === 'IN_PROGRESS' && !allContribution ? 'pulse border border-205' : ''"
>
<b-row>
<b-col cols="3" lg="2" md="2">
@ -26,7 +26,7 @@
<div class="mt-3 font-weight-bold">{{ $t('contributionText') }}</div>
<div class="mb-3 text-break word-break">{{ memo }}</div>
<div
v-if="state === 'IN_PROGRESS'"
v-if="status === 'IN_PROGRESS'"
class="text-205 pointer hover-font-bold"
@click="visible = !visible"
>
@ -37,11 +37,11 @@
<div class="small">
{{ $t('creation') }} {{ $t('(') }}{{ amount / 20 }} {{ $t('h') }}{{ $t(')') }}
</div>
<div v-if="state === 'DENIED' && allContribution" class="font-weight-bold">
<div v-if="status === '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">
<div v-if="status === 'DELETED'" class="small">
{{ $t('contribution.deleted') }}
</div>
<div v-else class="font-weight-bold">{{ amount | GDD }}</div>
@ -53,12 +53,12 @@
</b-col>
</b-row>
<b-row
v-if="(!['CONFIRMED', 'DELETED'].includes(state) && !allContribution) || messagesCount > 0"
v-if="(!['CONFIRMED', 'DELETED'].includes(status) && !allContribution) || messagesCount > 0"
class="p-2"
>
<b-col cols="3" class="mr-auto text-center">
<div
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !allContribution && !moderatorId"
v-if="!['CONFIRMED', 'DELETED'].includes(status) && !allContribution && !moderatorId"
class="test-delete-contribution pointer mr-3"
@click="deleteContribution({ id })"
>
@ -69,7 +69,7 @@
</b-col>
<b-col cols="3" class="text-center">
<div
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !allContribution && !moderatorId"
v-if="!['CONFIRMED', 'DELETED'].includes(status) && !allContribution && !moderatorId"
class="test-edit-contribution pointer mr-3"
@click="
$emit('update-contribution-form', {
@ -95,10 +95,10 @@
<b-collapse :id="collapsId" class="mt-2" v-model="visible">
<contribution-messages-list
:messages="messages_get"
:state="state"
:status="status"
:contributionId="contributionId"
@get-list-contribution-messages="getListContributionMessages"
@update-state="updateState"
@update-status="updateStatus"
/>
</b-collapse>
</div>
@ -161,7 +161,7 @@ export default {
type: String,
required: false,
},
state: {
status: {
type: String,
required: false,
default: '',
@ -197,14 +197,14 @@ export default {
if (this.deletedAt) return 'trash'
if (this.deniedAt) return 'x-circle'
if (this.confirmedAt) return 'check'
if (this.state === 'IN_PROGRESS') return 'question'
if (this.status === 'IN_PROGRESS') return 'question'
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 '205'
if (this.status === 'IN_PROGRESS') return '205'
return 'primary'
},
date() {
@ -245,8 +245,8 @@ export default {
this.toastError(error.message)
})
},
updateState(id) {
this.$emit('update-state', id)
updateStatus(id) {
this.$emit('update-status', id)
},
},
watch: {

Some files were not shown because too many files have changed in this diff Show More