Merge pull request #3181 from gradido/fix-contribution-filtering-by-memo

fix(backend): contribution filtering by memo
This commit is contained in:
einhornimmond 2023-08-22 12:41:18 +02:00 committed by GitHub
commit 88cea8d2d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 61 deletions

View File

@ -20,6 +20,7 @@ module.exports = {
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
'@union/(.*)': '<rootDir>/src/graphql/union/$1',
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
'@typeorm/(.*)': '<rootDir>/src/typeorm/$1',
'@test/(.*)': '<rootDir>/test/$1',
'@entity/(.*)':
// eslint-disable-next-line n/no-process-env

View File

@ -2718,22 +2718,40 @@ describe('ContributionResolver', () => {
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: '#firefighters',
creationDate: new Date().toString(),
},
})
})
afterAll(() => {
resetToken()
})
it('returns 17 creations in total', async () => {
it('returns 18 creations in total', async () => {
const {
data: { adminListContributions: contributionListObject },
} = await query({
query: adminListContributions,
})
expect(contributionListObject.contributionList).toHaveLength(17)
// console.log('17 contributions: %s', JSON.stringify(contributionListObject, null, 2))
expect(contributionListObject.contributionList).toHaveLength(18)
expect(contributionListObject).toMatchObject({
contributionCount: 17,
contributionCount: 18,
contributionList: expect.arrayContaining([
expect.objectContaining({
amount: expect.decimalEqual(100),
firstName: 'Peter',
id: expect.any(Number),
lastName: 'Lustig',
memo: '#firefighters',
messagesCount: 0,
status: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(50),
firstName: 'Bibi',
@ -2905,8 +2923,17 @@ describe('ContributionResolver', () => {
})
expect(contributionListObject.contributionList).toHaveLength(2)
expect(contributionListObject).toMatchObject({
contributionCount: 4,
contributionCount: 5,
contributionList: expect.arrayContaining([
expect.objectContaining({
amount: '100',
firstName: 'Peter',
id: expect.any(Number),
lastName: 'Lustig',
memo: '#firefighters',
messagesCount: 0,
status: 'PENDING',
}),
expect.objectContaining({
amount: '400',
firstName: 'Peter',
@ -2916,15 +2943,6 @@ describe('ContributionResolver', () => {
messagesCount: 0,
status: 'PENDING',
}),
expect.objectContaining({
amount: '100',
firstName: 'Peter',
id: expect.any(Number),
lastName: 'Lustig',
memo: 'Test env contribution',
messagesCount: 0,
status: 'PENDING',
}),
expect.not.objectContaining({
status: 'DENIED',
}),
@ -2951,6 +2969,60 @@ describe('ContributionResolver', () => {
query: 'Peter',
},
})
expect(contributionListObject.contributionList).toHaveLength(4)
expect(contributionListObject).toMatchObject({
contributionCount: 4,
contributionList: expect.arrayContaining([
expect.objectContaining({
amount: expect.decimalEqual(100),
firstName: 'Peter',
id: expect.any(Number),
lastName: 'Lustig',
memo: '#firefighters',
messagesCount: 0,
status: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(400),
firstName: 'Peter',
id: expect.any(Number),
lastName: 'Lustig',
memo: 'Herzlich Willkommen bei Gradido!',
messagesCount: 0,
status: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
firstName: 'Peter',
id: expect.any(Number),
lastName: 'Lustig',
memo: 'Test env contribution',
messagesCount: 0,
status: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(200),
firstName: 'Peter',
id: expect.any(Number),
lastName: 'Lustig',
memo: 'Das war leider zu Viel!',
messagesCount: 0,
status: 'DELETED',
}),
]),
})
})
it('returns only contributions of the queried user without hashtags', async () => {
const {
data: { adminListContributions: contributionListObject },
} = await query({
query: adminListContributions,
variables: {
query: 'Peter',
noHashtag: true,
},
})
expect(contributionListObject.contributionList).toHaveLength(3)
expect(contributionListObject).toMatchObject({
contributionCount: 3,
@ -2986,6 +3058,48 @@ describe('ContributionResolver', () => {
})
})
it('returns only contributions with #firefighter', async () => {
const {
data: { adminListContributions: contributionListObject },
} = await query({
query: adminListContributions,
variables: {
query: '#firefighter',
},
})
expect(contributionListObject.contributionList).toHaveLength(1)
expect(contributionListObject).toMatchObject({
contributionCount: 1,
contributionList: expect.arrayContaining([
expect.objectContaining({
amount: expect.decimalEqual(100),
firstName: 'Peter',
id: expect.any(Number),
lastName: 'Lustig',
memo: '#firefighters',
messagesCount: 0,
status: 'PENDING',
}),
]),
})
})
it('returns no contributions with #firefighter and no hashtag', async () => {
const {
data: { adminListContributions: contributionListObject },
} = await query({
query: adminListContributions,
variables: {
query: '#firefighter',
noHashtag: true,
},
})
expect(contributionListObject.contributionList).toHaveLength(0)
expect(contributionListObject).toMatchObject({
contributionCount: 0,
})
})
// test for case sensitivity and email
it('returns only contributions of the queried user email', async () => {
const {

View File

@ -1,68 +1,69 @@
import { In, Like, Not } from '@dbTools/typeorm'
/* eslint-disable security/detect-object-injection */
import { Brackets, In, Like, Not, SelectQueryBuilder } from '@dbTools/typeorm'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Paginated } from '@arg/Paginated'
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
import { Connection } from '@typeorm/connection'
import { LogError } from '@/server/LogError'
interface Relations {
[key: string]: boolean | Relations
}
function joinRelationsRecursive(
relations: Relations,
queryBuilder: SelectQueryBuilder<DbContribution>,
currentPath: string,
): void {
for (const key in relations) {
// console.log('leftJoin: %s, %s', `${currentPath}.${key}`, key)
queryBuilder.leftJoinAndSelect(`${currentPath}.${key}`, key)
if (typeof relations[key] === 'object') {
// If it's a nested relation
joinRelationsRecursive(relations[key] as Relations, queryBuilder, key)
}
}
}
export const findContributions = async (
paginate: Paginated,
filter: SearchContributionsFilterArgs,
withDeleted = false,
relations: Relations | undefined = undefined,
): Promise<[DbContribution[], number]> => {
const requiredWhere = {
const connection = await Connection.getInstance()
if (!connection) {
throw new LogError('Cannot connect to db')
}
const queryBuilder = connection.getRepository(DbContribution).createQueryBuilder('Contribution')
if (relations) joinRelationsRecursive(relations, queryBuilder, 'Contribution')
if (withDeleted) queryBuilder.withDeleted()
queryBuilder.where({
...(filter.statusFilter?.length && { contributionStatus: In(filter.statusFilter) }),
...(filter.userId && { userId: filter.userId }),
...(filter.noHashtag && { memo: Not(Like(`%#%`)) }),
}
let where =
filter.query && relations?.user
? [
{
...requiredWhere, // And
user: {
firstName: Like(`%${filter.query}%`),
},
}, // Or
{
...requiredWhere,
user: {
lastName: Like(`%${filter.query}%`),
},
}, // Or
{
...requiredWhere, // And
user: {
emailContact: {
email: Like(`%${filter.query}%`),
},
},
}, // Or
{
...requiredWhere, // And
memo: Like(`%${filter.query}%`),
},
]
: requiredWhere
if (!relations?.user && filter.query) {
where = [{ ...requiredWhere, memo: Like(`%${filter.query}%`) }]
}
return DbContribution.findAndCount({
relations,
where,
withDeleted,
order: {
createdAt: paginate.order,
id: paginate.order,
},
skip: (paginate.currentPage - 1) * paginate.pageSize,
take: paginate.pageSize,
})
queryBuilder.printSql()
if (filter.query) {
const queryString = '%' + filter.query + '%'
queryBuilder.andWhere(
new Brackets((qb) => {
qb.where({ memo: Like(queryString) })
if (relations?.user) {
qb.orWhere('user.first_name LIKE :firstName', { firstName: queryString })
.orWhere('user.last_name LIKE :lastName', { lastName: queryString })
.orWhere('emailContact.email LIKE :emailContact', { emailContact: queryString })
.orWhere({ memo: Like(queryString) })
}
}),
)
}
return queryBuilder
.orderBy('Contribution.createdAt', paginate.order)
.addOrderBy('Contribution.id', paginate.order)
.skip((paginate.currentPage - 1) * paginate.pageSize)
.take(paginate.pageSize)
.getManyAndCount()
}

View File

@ -136,6 +136,14 @@ export const creations: CreationInterface[] = [
confirmed: true,
moveCreationDate: 12,
},
{
email: 'bibi@bloxberg.de',
amount: 1000,
memo: '#Hexen',
creationDate: nMonthsBefore(new Date()),
confirmed: true,
moveCreationDate: 12,
},
...bobsTransactions,
{
email: 'raeuber@hotzenplotz.de',

View File

@ -251,6 +251,7 @@ export const adminListContributions = gql`
$statusFilter: [ContributionStatus!]
$userId: Int
$query: String
$noHashtag: Boolean
) {
adminListContributions(
currentPage: $currentPage
@ -259,6 +260,7 @@ export const adminListContributions = gql`
statusFilter: $statusFilter
userId: $userId
query: $query
noHashtag: $noHashtag
) {
contributionCount
contributionList {

View File

@ -53,6 +53,7 @@
"@model/*": ["src/graphql/model/*"],
"@union/*": ["src/graphql/union/*"],
"@repository/*": ["src/typeorm/repository/*"],
"@typeorm/*": ["src/typeorm/*"],
"@test/*": ["test/*"],
/* external */
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],