From e00719714e1d271d675add56606115d435a98fac Mon Sep 17 00:00:00 2001 From: Einhornimmond Date: Fri, 18 Aug 2023 18:42:35 +0200 Subject: [PATCH 1/4] start using query builder instead of options, because it cannot help us here --- .../src/graphql/resolver/util/findContributions.ts | 13 +++++++++++++ backend/tsconfig.json | 1 + 2 files changed, 14 insertions(+) diff --git a/backend/src/graphql/resolver/util/findContributions.ts b/backend/src/graphql/resolver/util/findContributions.ts index 6ad896b9f..f8fd581e4 100644 --- a/backend/src/graphql/resolver/util/findContributions.ts +++ b/backend/src/graphql/resolver/util/findContributions.ts @@ -3,6 +3,9 @@ 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 @@ -14,11 +17,20 @@ export const findContributions = async ( withDeleted = false, relations: Relations | undefined = undefined, ): Promise<[DbContribution[], number]> => { + const connection = await Connection.getInstance() + if (!connection) { + throw new LogError('Cannot connect to db') + } const requiredWhere = { ...(filter.statusFilter?.length && { contributionStatus: In(filter.statusFilter) }), ...(filter.userId && { userId: filter.userId }), ...(filter.noHashtag && { memo: Not(Like(`%#%`)) }), } + const queryBuilder = connection.getRepository(DbContribution).createQueryBuilder('Contribution') + queryBuilder.where(requiredWhere) + return queryBuilder.getManyAndCount() + /* + let where = filter.query && relations?.user @@ -65,4 +77,5 @@ export const findContributions = async ( skip: (paginate.currentPage - 1) * paginate.pageSize, take: paginate.pageSize, }) + */ } 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/*"], From 69f400a9300740cd1531c79fa586a4118fe005bd Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Fri, 18 Aug 2023 21:19:19 +0200 Subject: [PATCH 2/4] rewrite findContributions using QueryBuilder --- backend/jest.config.js | 1 + .../resolver/util/findContributions.ts | 96 ++++++++----------- backend/src/seeds/creation/index.ts | 8 ++ 3 files changed, 51 insertions(+), 54 deletions(-) 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/util/findContributions.ts b/backend/src/graphql/resolver/util/findContributions.ts index f8fd581e4..48d66d883 100644 --- a/backend/src/graphql/resolver/util/findContributions.ts +++ b/backend/src/graphql/resolver/util/findContributions.ts @@ -1,4 +1,5 @@ -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' @@ -11,6 +12,21 @@ 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, @@ -21,61 +37,33 @@ export const findContributions = async ( if (!connection) { throw new LogError('Cannot connect to db') } - const requiredWhere = { + 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(`%#%`)) }), - } - const queryBuilder = connection.getRepository(DbContribution).createQueryBuilder('Contribution') - queryBuilder.where(requiredWhere) - return queryBuilder.getManyAndCount() - /* - - - 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', From e310d46ac908f52dd81ba75b49dcdb25b377a693 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 19 Aug 2023 09:27:12 +0200 Subject: [PATCH 3/4] add tests for hashtag --- .../resolver/ContributionResolver.test.ts | 140 ++++++++++++++++-- backend/src/seeds/graphql/queries.ts | 2 + 2 files changed, 129 insertions(+), 13 deletions(-) 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/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index f016102a2..3a2823a65 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -236,6 +236,7 @@ export const adminListContributions = gql` $statusFilter: [ContributionStatus!] $userId: Int $query: String + $noHashtag: Boolean ) { adminListContributions( currentPage: $currentPage @@ -244,6 +245,7 @@ export const adminListContributions = gql` statusFilter: $statusFilter userId: $userId query: $query + noHashtag: $noHashtag ) { contributionCount contributionList { From 3333184542d28686260a779a1036ce9b80fb44df Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 19 Aug 2023 09:48:01 +0200 Subject: [PATCH 4/4] update locales and swap button with checkbox --- admin/src/locales/de.json | 9 +++------ admin/src/locales/en.json | 9 +++------ admin/src/pages/CreationConfirm.vue | 17 ++++------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index 3a4f23b18..33ef36053 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -89,7 +89,6 @@ "submit": "Senden" }, "GDD": "GDD", - "hashtag_symbol": "#", "help": { "help": "Hilfe", "transactionlist": { @@ -125,10 +124,8 @@ "user_search": "Nutzersuche" }, "not_open_creations": "Keine offenen Schöpfungen", - "no_filter": "Keine Filterung", - "no_filter_tooltip": "Es wird nicht nach Hashtags gefiltert", - "no_hashtag": "Ohne Hashtag", - "no_hashtag_tooltip": "Zeigt nur Schöpfungen ohne Hashtag im Kommentar an", + "no_hashtag": "#Hashtags verbergen", + "no_hashtag_tooltip": "Zeigt nur Beiträge ohne Hashtag im Text", "open": "offen", "open_creations": "Offene Schöpfungen", "overlay": { @@ -221,7 +218,7 @@ "tabTitle": "Nutzer-Rolle" }, "user_deleted": "Nutzer ist gelöscht.", - "user_memo_search": "Nutzer-Kommentar-Suche", + "user_memo_search": "Benutzer- und Text-Suche", "user_recovered": "Nutzer ist wiederhergestellt.", "user_search": "Nutzer-Suche" } diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index 5a3c78a0f..6c8b36f15 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -89,7 +89,6 @@ "submit": "Send" }, "GDD": "GDD", - "hashtag_symbol": "#", "help": { "help": "Help", "transactionlist": { @@ -125,10 +124,8 @@ "user_search": "User search" }, "not_open_creations": "No open creations", - "no_filter": "No Filter", - "no_filter_tooltip": "It is not filtered by hashtags", - "no_hashtag": "No Hashtag", - "no_hashtag_tooltip": "Displays only contributions without hashtag in comment", + "no_hashtag": "Hide #hashtags", + "no_hashtag_tooltip": "Shows only contributions without hashtag in text", "open": "open", "open_creations": "Open creations", "overlay": { @@ -221,7 +218,7 @@ "tabTitle": "User Role" }, "user_deleted": "User is deleted.", - "user_memo_search": "User and Memo search", + "user_memo_search": "User and text search", "user_recovered": "User is recovered.", "user_search": "User search" } diff --git a/admin/src/pages/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue index 7d52c4ce2..bd4c58983 100644 --- a/admin/src/pages/CreationConfirm.vue +++ b/admin/src/pages/CreationConfirm.vue @@ -2,12 +2,10 @@