diff --git a/backend/jest.config.js b/backend/jest.config.js index d282f8361..8b6f53f9f 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -20,6 +20,7 @@ module.exports = { '@model/(.*)': '/src/graphql/model/$1', '@union/(.*)': '/src/graphql/union/$1', '@repository/(.*)': '/src/typeorm/repository/$1', + '@typeorm/(.*)': '/src/typeorm/$1', '@test/(.*)': '/test/$1', '@entity/(.*)': // eslint-disable-next-line n/no-process-env diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index cbb92eff8..a678c7531 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -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 { diff --git a/backend/src/graphql/resolver/util/findContributions.ts b/backend/src/graphql/resolver/util/findContributions.ts index 6ad896b9f..48d66d883 100644 --- a/backend/src/graphql/resolver/util/findContributions.ts +++ b/backend/src/graphql/resolver/util/findContributions.ts @@ -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, + 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() } diff --git a/backend/src/seeds/creation/index.ts b/backend/src/seeds/creation/index.ts index 3f2a545a4..c22a99b0c 100644 --- a/backend/src/seeds/creation/index.ts +++ b/backend/src/seeds/creation/index.ts @@ -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', diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 3dda2633c..949ed86d7 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -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 { diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 146cd41e3..6d27ca0fa 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -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/*"],